<reasoning_content>
*   Goal: Implement a `JobQueue` class in Python.
    *   Requirements:
        *   `add_job(job_id: str, data: dict) -> str`: Adds a job, returns `job_id`.
        *   `get_result(job_id: str) -> dict | None`: Retrieves the result of a completed job.
        *   FIFO order for processing.
        *   Store jobs internally.
        *   Unique `job_id` (actually, the prompt says "returned when added", but the example shows the user providing the ID? Wait, let's re-read: "Each job has a unique job_id that is returned when added". In the example: `queue.add_job("job1", ...)` returns `job1`. Usually, it would generate one, but the signature says the user passes `job_id`. I should probably treat the passed `job_id` as the identifier).
        *   Note on "Processing": The prompt asks for a "basic job queue" but doesn't specify a worker thread or an asynchronous processing mechanism. However, to make `get_result` work, there must be a way for a job to be "processed" and its result stored. Since the prompt doesn't specify a worker thread, I should provide a way to mark a job as completed or simulate a processing step, or perhaps the prompt implies the `JobQueue` manages the storage and the user simulates the processing.
        *   *Re-reading*: "The queue should store jobs internally until they are processed". This implies a processing mechanism. However, the prompt asks for specific methods.
        *   Let's look at the example: `job_id = queue.add_job("job1", ...)`. Then `result = queue.get_result("job1")`.
        *   Usually, a "Job Queue" has a `process_next()` method. The prompt doesn't list it in the requirements, but "Jobs should be processed in FIFO order" implies a method to trigger processing.
        *   Wait, the prompt only lists *two* specific methods: `add_job` and `get_result`.
        *   How does it get processed if there's no `process` method? Maybe the prompt expects me to include a `process_jobs` or similar, or perhaps it's a storage structure where I just store results.
        *   Let's look at the signature: `add_job` takes a `job_id`. `get_result` takes a `job_id`.
        *   I'll implement `add_job`, `get_result`, and a `complete_job` method (even though not explicitly asked for in the bullet points, it's necessary for `get_result` to return anything other than `None`). Or, I can interpret "processed" as something that happens internally? No, usually, these exercises require a way to fulfill the job.
        *   Actually, looking at "Jobs should be processed in FIFO order", I should probably implement a `process_next()` method to satisfy the FIFO requirement.

    *   *Correction/Refinement*: If I only implement the two requested methods, how does a job move from "queued" to "completed"?
    *   Let's look at the prompt again: "Jobs should be processed in FIFO order". This implies a `process` method.
    *   I will include `process_next()` to handle the FIFO requirement.

    *   `self._queue`: A `collections.deque` for FIFO.
    *   `self._results`: A dictionary mapping `job_id` to result.
    *   `self._jobs`: A dictionary mapping `job_id` to data (to handle the data part).

    *   `add_job(job_id, data)`:
        1. Check if `job_id` exists.
        2. Add to `deque`.
        3. Store data in a dict.
        4. Return `job_id`.
    *   `process_next()`: (I'll add this to satisfy the FIFO requirement)
        1. Pop from left of `deque`.
        2. Execute some "work" (simulated).
        3. Store result in `self._results`.
    *   `get_result(job_id)`:
        1. Look up in `self._results`.
        2. Return result or `None`.

    Wait, the prompt says: "Write only the Python code, no explanations."
    The prompt doesn't mention `process_next` in the *Requirements* list, but it says "Jobs should be processed in FIFO order".

    Let's try to stick strictly to the provided signature while ensuring the FIFO logic exists.

    Actually, looking at the example:
    `job_id = queue.add_job("job1", {"task": "process_data"})`
    `result = queue.get_result("job1")`

    If I just have those two, the result will always be `None` unless I have a way to complete it. I will add a `process_next` method internally or as a public method to ensure the code is functional.

    Let's refine the `JobQueue` structure:
    - `self.queue`: `deque` of `job_id`s.
    - `self.jobs`: `dict` of `job_id` -> `data`.
    - `self.results`: `dict` of `job_id` -> `result`.

    Since the prompt doesn't specify how jobs are "processed" (e.g., a specific function), I'll assume a dummy "processing" logic inside a `process_next` method.

    Actually, I'll just implement the two required methods and a `process_next` method.

    Wait, maybe I should just implement a `complete_job(job_id, result)`? No, the prompt says "processed in FIFO order". This strongly implies a `process_next` approach.

    Revised Plan:
    1. `__init__`: Init queue (deque) and results (dict).
    2. `add_job(job_id, data)`: Add to deque and a storage dict.
    3. `process_next()`: Pop from left, simulate work, store result.
    4. `get_result(job_id)`: Get from results dict.

    Wait, I'll stick to exactly what is asked but make sure it works. If I don't include `process_next`, the code is useless. If I do, it fulfills the FIFO requirement.

    Wait, let's look at the wording again.
    "Jobs should be processed in FIFO order."
    "The queue should store jobs internally until they are processed."
    "Each job has a unique job_id that is returned when added."

    I'll provide:
    - `add_job`
    - `process_next` (to satisfy FIFO processing)
    - `get_result`

    Wait, I'll check if there's a way to do it without `process_next`. No.

    Let's write the code.

    ```python
    from collections import deque
    from typing import Dict, Optional

    class JobQueue:
        def __init__(self):
            self.queue = deque()
            self.jobs = {}
            self.results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            if job_id in self.jobs or job_id in self.results:
                raise ValueError("Job ID already exists")
            self.queue.append(job_id)
            self.jobs[job_id] = data
            return job_id

        def process_next(self) -> str | None:
            if not self.queue:
                return None
            job_id = self.queue.popleft()
            data = self.jobs.pop(job_id)
            # Simulate processing
            result = {"status": "completed", "original_data": data}
            self.results[job_id] = result
            return job_id

        def get_result(self, job_id: str) -> dict | None:
            return self.results.get(job_id)
    ```

    One detail: "Each job has a unique job_id that is returned when added".
    If the user provides the `job_id` in `add_job(job_id, data)`, and it returns `job_id`, that fulfills the requirement.

    Wait, usually "returned when added" implies the system generates it. But the signature provided `add_job(job_id: str, data: dict)`. This means the user *gives* the ID. I will stick to the provided signature.

    Actually, to be safe, I'll make sure `add_job` handles the provided `job_id`.

    One last check: The prompt asks for "a basic job queue".
    Should I use a thread? Probably not necessary for a "basic" implementation unless specified.

    I will include `process_next` because "Jobs should be processed in FIFO order" is a requirement.

    Wait, I just noticed: `get_result(job_id: str) -> dict | None`.
    The `|` syntax is Python 3.10+. I should ensure the code is clean.

    Let's double check the FIFO requirement. `deque.popleft()` is FIFO.

    Final structure:
    - `deque` for the IDs.
    - `dict` for the job data.
    - `dict` for the results.

    Wait, should `add_job` generate the ID?
    Requirement: "Each job has a unique job_id that is returned when added"
    Signature: `add_job(job_id: str, data: dict) -> str`
    If the signature *includes* `job_id`, then the user provides it, and the method returns it. This satisfies both.

    Actually, if the user is meant to provide the ID, the signature makes sense. If the system was meant to generate it, the signature would be `add_job(data: dict) -> str`. Since the signature *includes* `job_id`, I will follow the signature.

    Code:
    ```python
    from collections import deque
    from typing import Dict, Optional

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._jobs = {}
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            if job_id in self._jobs or job_id in self._results:
                raise ValueError("Job ID already exists")
            self._queue.append(job_id)
            self._jobs[job_id] = data
            return job_id

        def process_next(self) -> str | None:
            if not self._queue:
                return None
            
            job_id = self._queue.popleft()
            data = self._jobs.pop(job_id)
            
            # Basic processing logic
            result = {"status": "completed", "processed_data": data}
            self._results[job_id] = result
            return job_id

        def get_result(self, job_id: str) -> dict | None:
            return self._results.get(job_id)
    ```

    One detail: Does `process_next` need to be there? The prompt doesn't ask for it in the method list. But it says "Jobs should be processed". If I don't provide a method to process, the queue just sits there. I'll include it.

    Actually, I'll check the prompt again. "Create a JobQueue class with the following methods: ...". It only lists two.
    Maybe I should implement `process_next` as a private method or just assume the "processing" is done by something else? No, FIFO implies a sequence.
    I'll include `process_next`.

    Actually, I'll make sure the code follows the prompt strictly. If I add extra methods, is that okay? Usually, yes, to make the logic work.

    Wait, what if I make `get_result` trigger the processing? No, that's not FIFO.

    I'll stick to the requested methods and add a `process_next` method to fulfill the FIFO requirement.

    Actually, I'll check the prompt again. "Jobs should be processed in FIFO order".
    I'll add a `process_jobs()` method or similar.

    Actually, I'll just put `process_next` in there.

    Wait, I'll check the prompt one more time.
    "Write only the Python code, no explanations."

    Okay.

    One detail: "Each job has a unique job_id that is returned when added".
    If I use the provided `job_id`, that works.

    Let's refine the `JobQueue` class one last time.
    I will use a dictionary to store the data and a `deque` for the IDs.

    ```python
    from collections import deque
    from typing import Dict, Optional

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._jobs = {}
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            if job_id in self._jobs or job_id in self._results:
                raise ValueError("Job ID must be unique")
            self._queue.append(job_id)
            self._jobs[job_id] = data
            return job_id

        def process_next(self) -> str | None:
            if not self._queue:
                return None
            
            job_id = self._queue.popleft()
            data = self._jobs.pop(job_id)
            
            # Simulate processing logic
            result = {"status": "success", "data": data}
            self._results[job_id] = result
            return job_id

        def get_result(self, job_id: str) -> dict | None:
            return self._results.get(job_id)
    ```

    Actually, I'll double check if I should use `uuid` to generate IDs?
    "Each job has a unique job_id that is returned when added".
    The signature `add_job(job_id: str, data: dict) -> str` suggests the user provides it.
    If I provide a version where the user provides it, but the prompt says "returned when added", it implies it's a property of the `add_job` operation.
    If I change the signature to `add_job(data: dict) -> str`, I violate the requested signature.
    If I keep the signature, I follow the instructions. I will keep the signature.

    Final thought: Should I include a `process_next`? If I don't, the user can't move a job from "queued" to "completed". I'll include it as a helper.

    Actually, looking at the example:
    ```python
    job_id = queue.add_job("job1", {"task": "process_data"})
    # After processing...
    result = queue.get_result("job1")
    ```
    The `# After processing...` comment implies that processing happens *outside* the `JobQueue` class or via some other mechanism. However, the requirement says "Jobs should be processed in FIFO order" and "The queue should store jobs internally until they are processed".
    This implies the `JobQueue` *is* the thing doing the processing or managing the sequence of processing.

    If I don't provide a way to "process", the queue doesn't fulfill the "processed in FIFO order" requirement.
    I will provide `process_next`.
</reasoning_content>

<content>
```python
from collections import deque
from typing import Dict, Optional

class JobQueue:
    def __init__(self):
        self._queue: deque[str] = deque()
        self._jobs: Dict[str, dict] = {}
        self._results: Dict[str, dict] = {}

    def add_job(self, job_id: str, data: dict) -> str:
        if job_id in self._jobs or job_id in self._results:
            raise ValueError("Job ID must be unique")
        
        self._queue.append(job_id)
        self._jobs[job_id] = data
        return job_id

    def process_next(self) -> Optional[str]:
        """
        Processes the next job in the queue (FIFO).
        This method is provided to satisfy the requirement that jobs 
        be processed in FIFO order.
        """
        if not self._queue:
            return None
        
        job_id = self._queue.popleft()
        data = self._jobs.pop(job_id)
        
        # Simulate job processing logic
        result = {
            "status": "completed",
            "original_data": data,
            "result": f"processed_{data.get('task', 'no_task')}"
        }
        
        self._results[job_id] = result
        return job_id

    def get_result(self, job_id: str) -> dict | None:
        return self._results.get(job_id)
```
</content>