<reasoning_content>
*   Goal: Refactor a job queue into three composable classes in `solution.py`.
    *   Class 1: `Job` (dataclass)
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    *   Class 2: `RetryPolicy`
        *   `__init__(self, max_attempts: int = 4)`
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`
        *   Logic: Call `fn(data)`, retry on exception up to `max_attempts`. No sleeping.
        *   Return: `(success, attempts_made)`.
    *   Class 3: `JobQueue`
        *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
        *   `process_job(self, job_id: str, processor: Callable) -> bool` (must use `RetryPolicy`).
        *   `get_next_job(self) -> tuple[str, dict] | None` (highest priority, FIFO within same priority).
    *   Constraints: Standard library only, no external packages, write only code.

    *   `Job`: `dataclass`.
    *   `RetryPolicy`: Use a `while` loop and a `try-except` block.
    *   `JobQueue`:
        *   Needs a way to store jobs sorted by priority.
        *   `priority` (higher value or lower value? Usually, priority 0 is lowest, but standard priority queues often use min-heap where lower number = higher priority. However, the prompt says "highest-priority job". Usually, in task queues, higher integer = higher priority, or lower integer = higher priority. Let's check the prompt: `priority: int = 0`. Standard convention in `heapq` is a min-heap. If I want highest priority first, I can store `(-priority, counter, job)` where counter maintains FIFO.
        *   Wait, let's look at `get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   Storage structure: `heapq` is ideal. To handle FIFO within the same priority, I need a secondary key (a sequence number).
        *   Data structure for `JobQueue`:
            *   `self.jobs`: A priority queue (list + `heapq`).
            *   `self.job_map`: A dictionary mapping `id` to `Job` objects (needed for `process_job` where `job_id` is provided).
            *   Wait, `process_job` takes a `job_id`. Does this mean the job is already in the queue? Or does it look up the job? The prompt says "orchestrates Jobs using RetryPolicy". `process_job` uses `job_id`. This implies I need to find the job by ID.
            *   `process_job` returns `bool` (success).
            *   `get_next_job` returns `tuple[str, dict] | None`.

    *   *Step 1: Imports*
        `from dataclasses import dataclass`
        `from typing import Callable, Any`
        `import heapq`

    *   *Step 2: `Job` class*
        ```python
        @dataclass
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
        ```

    *   *Step 3: `RetryPolicy` class*
        ```python
        class RetryPolicy:
            def __init__(self, max_attempts: int = 4):
                self.max_attempts = max_attempts

            def run(self, fn: Callable, data: Any) -> tuple[bool, int]:
                attempts = 0
                while attempts < self.max_attempts:
                    attempts += 1
                    try:
                        fn(data)
                        return True, attempts
                    except Exception:
                        continue
                return False, attempts
        ```

    *   *Step 4: `JobQueue` class*
        *   Need a counter for FIFO.
        *   `self.queue`: `[]` (heap)
        *   `self.jobs`: `dict[str, Job]` (to allow lookup by ID for `process_job`)
        *   `self.counter`: `int` (for FIFO)
        *   `self.retry_policy`: `RetryPolicy()` (default or internal?)
            *   The prompt doesn't say `RetryPolicy` is passed to `JobQueue`'s init, but `JobQueue` "orchestrates Jobs using RetryPolicy". I'll initialize it in `__init__`.

        *   `add_job(job_id, data, priority)`:
            1. Create `Job`.
            2. `self.jobs[job_id] = job`.
            3. `heapq.heappush(self.queue, (-priority, self.counter, job_id))`.
            4. `self.counter += 1`.

        *   `get_next_job()`:
            1. While `self.queue`:
               `prio, cnt, jid = heapq.heappop(self.queue)`
               Check if `jid` is still in `self.jobs`. (If a job was processed/removed, the heap might contain stale IDs).
               If exists: return `(job.id, job.data)`.
               Wait, if `get_next_job` is called, should it remove the job? Usually "get" implies removal in queue terminology.
               If it removes, I should `del self.jobs[jid]`.

        *   `process_job(job_id, processor)`:
            1. Find job in `self.jobs`.
            2. If not found, return `False`.
            3. Use `self.retry_policy.run(processor, job.data)`.
            4. If success: `del self.jobs[job_id]`. (Actually, if it's in the queue, I need to make sure it's removed from the heap too? The heap logic with stale IDs is better: `get_next_job` cleans up the heap).
            5. If failure: Update `job.retries`.
            6. Return `success`.

    *   *Refining `JobQueue` logic:*
        If I `add_job`, I put it in the heap and the map.
        If I `process_job(id)`, I look it up in the map. If successful, I remove from map.
        When `get_next_job` is called, I pop from heap. If the popped ID is not in the map, it means that job was already processed or removed, so I pop again.

        *Problem with `process_job`:* If `process_job` is called on a `job_id` that is currently in the queue, but `get_next_job` is the only way to "consume" it, does `process_job` remove it? The prompt says `process_job` orchestrates. Usually, a job queue works by: `add` -> `get` -> `process`. But the method signature `process_job(job_id, ...)` implies we might process a specific job.

        Wait, the prompt says `process_job` "must USE RetryPolicy".
        If I process a job by ID, and it succeeds, I should probably remove it from the queue so `get_next_job` doesn't return it again.

        *Priority Queue logic details:*
        Python's `heapq` is a min-heap.
        To get "highest-priority" (where higher number = higher priority):
        Heap element: `(-priority, sequence_number, job_id)`.

        *Wait, the requirement for `get_next_job`*: "return the highest-priority job's (id, data); FIFO order within the same priority."
        Standard priority queue behavior.

    *   *Let's re-read:*
        `process_job(self, job_id: str, processor: Callable) -> bool`
        If I call `process_job(id)`, it uses the policy. If successful, the job is done. If not, it's still in the queue? Or do we increment `retries`?
        The `Job` class has a `retries` field. The `RetryPolicy` returns `attempts_made`.
        The prompt says `Job` has `retries: int = 0`.
        In `process_job`, if `RetryPolicy.run` returns `False`, the job failed all attempts. Should I update `job.retries`?
        Actually, `RetryPolicy` handles the *immediate* retries. The `Job` object's `retries` field might be for tracking total lifetime retries or just a state. Given the requirements, I will update `job.retries` if the policy fails.

    *   *Wait*, if `process_job` is called, it's not necessarily the "next" job from the queue. It's a specific job.
        If `process_job` is successful, I should remove it from the `jobs` map.

    *   `Job`: dataclass.
    *   `RetryPolicy`: `run` method.
    *   `JobQueue`:
        *   `__init__`: `self.jobs = {}`, `self.queue = []`, `self.counter = 0`, `self.retry_policy = RetryPolicy()`.
        *   `add_job`: creates `Job`, adds to `self.jobs`, pushes `(-priority, self.counter, job_id)` to `self.queue`.
        *   `get_next_job`: `heappop` until a `job_id` is found in `self.jobs`.
        *   `process_job`: `self.jobs.get(job_id)`. Use `retry_policy.run`. If success, `del self.jobs[job_id]`.

    `get_next_job` returns `(id, data)`. Does it remove it?
    "return the highest-priority job's (id, data)". In queue implementations, "get" usually means "pop". If it didn't remove it, it would be a "peek". I'll implement it as a pop.

    *Wait*, if `get_next_job` pops the job, then `process_job` can't be called on it via `job_id` unless `process_job` is called *before* `get_next_job`. But `process_job` takes a `job_id`. This implies `job_id` is known.
    If `get_next_job` pops the job and removes it from `self.jobs`, then `process_job` (which looks up `self.jobs`) won't find it.

    Let's look at the typical flow:
    1. `add_job`
    2. `get_next_job` -> returns `id, data`.
    3. `process_job(id, processor)` -> processes it.

    If `get_next_job` removes it from the `jobs` dictionary, then `process_job` won't find it.
    Therefore, `get_next_job` should *not* remove it from the `jobs` dictionary, but only from the heap? No, that's not how heaps work.
    The heap *is* the queue.
    If `get_next_job` removes it from the heap, the `jobs` dict still has it.
    Then `process_job` can find it in the dict, process it, and *then* remove it from the dict.
    But if `process_job` removes it from the dict, and then `get_next_job` is called again, `get_next_job` will pop the heap, see the ID is gone from the dict, and continue. This is a standard "lazy removal" pattern for heaps.

    So:
    `add_job`: `self.jobs[id] = job`, `heapq.heappush(self.queue, (-priority, self.counter, job_id))`
    `get_next_job`:
    ```python
    while self.queue:
        p, c, jid = heapq.heappop(self.queue)
        if jid in self.jobs:
            job = self.jobs[jid]
            return job.id, job.data
    return None
    ```
    Wait, if `get_next_job` pops the job from the heap, it's no longer in the heap. If `process_job` is called *after* `get_next_job`, it's fine. But if `process_job` is called *before* `get_next_job`?
    If `process_job` is called first, it removes it from `self.jobs`. Then `get_next_job` pops it from heap, sees it's not in `self.jobs`, and moves on. This works.

    *Crucial Detail*: Does `get_next_job` remove the job from the queue?
    "get_next_job(self) -> tuple[str, dict] | None — return the highest-priority job's (id, data); FIFO order within the same priority."
    Usually, `get` in a queue context means "remove and return". If I don't remove it, I'm just peeking. I will assume it removes the job from the priority queue (heap) but leaves it in the `jobs` dict so it can be processed.

    *Wait*, if `get_next_job` removes it from the heap, and `process_job` removes it from the dict, then the job is successfully processed and removed from both.

    *Let's refine `process_job`:*
    ```python
    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = self.jobs.get(job_id)
        if not job:
            return False
        success, attempts = self.retry_policy.run(processor, job.data)
        if success:
            del self.jobs[job_id]
            return True
        else:
            job.retries += attempts # or job.retries += 1? Prompt says "retries: int = 0". Let's use attempts or just increment.
            # The prompt says "retries: int = 0". It doesn't specify how to update it.
            # Let's just use the `attempts` from RetryPolicy.
            return False
    ```

    *Wait, one more check on `get_next_job`:*
    If `get_next_job` pops the job from the heap, the job is *no longer in the queue* to be retrieved by another `get_next_job` call. This is correct.

    *Wait, what if `process_job` is called on a job that hasn't been "gotten" via `get_next_job`?*
    That's fine, the logic handles it.

    *What about the `Job` data and `priority`?*
    `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
    `Job` dataclass: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.

    `RetryPolicy.run(self, fn: Callable, data) -> tuple[bool, int]`
    It returns `(success, attempts_made)`.
    If `fn(data)` succeeds on 1st attempt, return `(True, 1)`.
    If `fn(data)` fails 4 times, return `(False, 4)`.

    *Self-Correction on `process_job` and `retries`:*
    If `RetryPolicy` fails, should `job.retries` be updated? The `Job` class has a `retries` field. It's good practice to update it. I'll update it with the number of attempts made.

    *Wait*, looking at `get_next_job` again:
    If I `heappop` the job from the queue, and then I call `process_job` on that same job, I need to ensure that the job remains "available" for processing.
    If `get_next_job` pops it from the heap, it's gone from the queue. If the user then calls `process_job`, the `job_id` is still in `self.jobs`, so it works.

    *One more edge case:* `get_next_job` returns the highest priority. If I call `get_next_job` twice, I should get two different jobs. My current logic (popping from heap) does this.

    *Implementation Details:*
    `import heapq`
    `from dataclasses import dataclass`
    `from typing import Callable, Any`

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    import heapq

    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data: Any) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    continue
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs = {}
            self.queue = []
            self.counter = 0
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self.jobs[job_id] = job
            # Use negative priority for max-heap behavior with heapq (min-heap)
            # Use self.counter for FIFO within same priority
            heapq.heappush(self.queue, (-priority, self.counter, job_id))
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self.jobs.get(job_id)
            if not job:
                return False
            
            success, attempts = self.retry_policy.run(processor, job.data)
            if success:
                del self.jobs[job_id]
                return True
            else:
                job.retries += attempts
                return False

        def get_next_job(self) -> tuple[str, dict] | None:
            while self.queue:
                _, _, job_id = heapq.heappop(self.queue)
                if job_id in self.jobs:
                    job = self.jobs[job_id]
                    return job.id, job.data
            return None
    ```

    *Refining `get_next_job`*:
    If `get_next_job` is called, it pops from the heap. If the job is in the `jobs` dict, it returns the ID and Data.
    But wait, if `get_next_job` is called, the job is now *out* of the heap.
    If the caller then fails to process it and calls `process_job`, it still works.
    If the caller then calls `get_next_job` again, they get the *next* job.
    This seems to be a valid implementation of a queue.

    *Wait*, there is a potential issue. If `get_next_job` is called, and then `process_job` is called and *fails*, the job is no longer in the heap (because `get_next_job` popped it), so it can't be "gotten" again via `get_next_job`.
    Is that desired? Usually, in a job queue, if a job fails, it might be re-queued.
    However, the prompt doesn't mention re-queueing. It says:
    - `add_job`: adds job.
    - `process_job`: uses `RetryPolicy`.
    - `get_next_job`: returns highest priority.

    If the user wants to re-queue a failed job, they would have to call `add_job` again.
    Given the constraints, the simplest interpretation is:
    1. `add_job` puts it in the queue.
    2. `get_next_job` retrieves it (and removes it from the priority order).
    3. `process_job` executes it.

    Actually, the prompt for `process_job` says "orchestrates Jobs using RetryPolicy". It doesn't say it's called *after* `get_next_job`. It takes a `job_id`.
    If I call `process_job(job_id)` *without* calling `get_next_job` first, the job is still in `self.jobs` and the heap.
    If `process_job` succeeds, it removes it from `self.jobs`.
    If `get_next_job` is called later, it pops the heap, sees the ID is gone from `self.jobs`, and skips it.
    This is robust.

    What if `process_job` fails? The job stays in `self.jobs` (with updated `retries`) but is *not* in the heap (if it was already popped by `get_next_job`).
    If the job is still in the heap (because `get_next_job` wasn't called), then `process_job` failing doesn't change its heap status.
    This means `get_next_job` could eventually pick it up again.

    Wait, if `process_job` fails, should the job be re-queued? The `RetryPolicy` handles the *internal* retries (up to `max_attempts`). If `RetryPolicy.run` returns `False`, it means all attempts failed.
    If the job is still in `self.jobs` but not in the heap, it's effectively "lost" from the queueing mechanism unless `add_job` is called again. This is standard for many queue implementations where "getting" a job moves it to an "in-progress" state.

    One minor detail: `get_next_job` return type hint `tuple[str, dict] | None`. In Python < 3.10, `|` is not supported for types. But since the prompt uses it in the description, I will use it in the code.

    Let's double check the `process_job` logic.
    If `process_job` is called *before* `get_next_job`:
    1. `add_job("1", {"a": 1}, 10)` -> `jobs["1"] = Job`, `heap = [(-10, 0, "1")]`.
    2. `process_job("1", func)` -> `func` succeeds, `del jobs["1"]`.
    3. `get_next_job()` -> `heappop` returns `(-10, 0, "1")`. `job_id in jobs` is False. Returns `None`. Correct.

    If `process_job` is called *after* `get_next_job`:
    1. `add_job("1", {"a": 1}, 10)` -> `jobs["1"] = Job`, `heap = [(-10, 0, "1")]`.
    2. `get_next_job()` -> `heappop` returns `(-10, 0, "1")`. `job_id in jobs` is True. Returns `("1", {"a": 1})`.
    3. `process_job("1", func)` -> `func` succeeds, `del jobs["1"]`. Correct.

    *One detail*: The prompt says `get_next_job` should return `(id, data)`. My code does this.

    *Final check on `Job` dataclass fields*: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`. (Matches).

    *Final check on `RetryPolicy.run`*: `run(self, fn: Callable, data) -> tuple[bool, int]`. (Matches).

    *Final check on `JobQueue.add_job`*: `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`. (Matches).

    *Final check on `JobQueue.process_job`*: `process_job(self, job_id: str, processor: Callable) -> bool`. (Matches).

    *Final check on `JobQueue.get_next_job`*: `get_next_job(self) -> tuple[str, dict] | None`. (Matches).

    The prompt says "Do NOT actually sleep" in `RetryPolicy`. My implementation has no `time.sleep()`.

    *Wait, one small detail*: In `process_job`, if `success` is `False`, I update `job.retries`. Should I? The prompt doesn't explicitly say to update the `retries` field in `Job`, but it includes the field in the dataclass, and it's the only logical place to track it. I'll include it.

    *Wait*, `get_next_job` returns `tuple[str, dict] | None`. In Python 3.10+, this is valid. In earlier versions, you'd use `Optional[Tuple[str, dict]]`. Given the instruction, I'll use the modern syntax.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    import heapq

    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data: Any) -> tuple[bool, int]:
            attempts_made = 0
            for _ in range(self.max_attempts):
                attempts_made += 1
                try:
                    fn(data)
                    return True, attempts_made
                except Exception:
                    continue
            return False, attempts_made

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._queue: list[tuple[int, int, str]] = []
            self._counter: int = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            # Use negative priority for max-priority behavior in min-heap
            heapq.heappush(self._queue, (-priority, self._counter, job_id))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if job is None:
                return False
            
            success, attempts = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
                return True
            else:
                job.retries += attempts
                return False

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._queue:
                _, _, job_id = heapq.heappop(self._queue)
                if job_id in self._jobs:
                    job = self._jobs[job_id]
                    return job.id, job.data
            return None
    ```

    Wait, the prompt says `get_next_job(self) -> tuple[str, dict] | None`. In the implementation, I'll use `Any` or `dict` for data. The prompt specifies `data: dict`.

    *Check*: Is `self.counter` needed? Yes, to ensure FIFO for same priority.
    `heapq.heappush(self.queue, (-priority, self.counter, job_id))`
    If two jobs have same `priority`, the one with the smaller `counter` will be popped first. This is correct for FIFO.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Any
import heapq


@dataclass
class Job:
    id: str
    data: dict
    priority: int = 0
    retries: int = 0


class RetryPolicy:
    def __init__(self, max_attempts: int = 4):
        self.max_attempts = max_attempts

    def run(self, fn: Callable, data: Any) -> tuple[bool, int]:
        attempts_made = 0
        for _ in range(self.max_attempts):
            attempts_made += 1
            try:
                fn(data)
                return True, attempts_made
            except Exception:
                continue
        return False, attempts_made


class JobQueue:
    def __init__(self):
        self._jobs: dict[str, Job] = {}
        self._queue: list[tuple[int, int, str]] = []
        self._counter: int = 0
        self._retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        job = Job(id=job_id, data=data, priority=priority)
        self._jobs[job_id] = job
        # heapq is a min-heap. Use -priority to simulate a max-heap for priority.
        # Use self._counter to ensure FIFO order for jobs with the same priority.
        heapq.heappush(self._queue, (-priority, self._counter, job_id))
        self._counter += 1

    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = self._jobs.get(job_id)
        if job is None:
            return False

        success, attempts = self._retry_policy.run(processor, job.data)
        if success:
            del self._jobs[job_id]
            return True
        else:
            job.retries += attempts
            return False

    def get_next_job(self) -> tuple[str, dict] | None:
        while self._queue:
            _, _, job_id = heapq.heappop(self._queue)
            if job_id in self._jobs:
                job = self._jobs[job_id]
                return job.id, job.data
        return None

```
</content>