<reasoning_content>
*   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   `Job` class: Dataclass with `id` (str), `data` (dict), `priority` (int, default 0), `retries` (int, default 0).
    *   `RetryPolicy` class:
        *   `__init__(max_attempts: int = 4)`
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`: Executes `fn(data)`, retries on exception. No actual sleeping. Returns `(success, attempts_made)`.
    *   `JobQueue` class:
        *   `add_job(self, job_id: str, data: dict, priority: int = 0)`
        *   `process_job(self, job_id: str, processor: Callable) -> bool`: Uses `RetryPolicy` to execute the processor for a specific job.
        *   `get_next_job(self) -> tuple[str, dict] | None`: Returns highest priority job (FIFO for same priority).
    *   Constraints: Standard library only, no explanations, only Python code.

    *   Imports: `dataclasses`, `typing.Callable`, `heapq` (or a simple list sort if needed, but `heapq` is standard for priority queues). Since the prompt mentions "FIFO order within the same priority", a simple list sorted by priority (descending) or a `heapq` with a counter for tie-breaking is best.

    *   `Job` Dataclass:
        ```python
        from dataclasses import dataclass, field
        from typing import Callable

        @dataclass
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
            # To handle FIFO with heapq, we need a sequence number.
            # However, the prompt doesn't explicitly ask for sequence,
            # but describes FIFO within priority.
        ```

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

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

    *   `JobQueue` Class:
        *   Storage: A list of `Job` objects.
        *   `add_job`: Appends job.
        *   `get_next_job`: Sort by priority (desc) and then order of insertion (FIFO). Since I need to remove it, I should probably find the best one and remove it.
        *   `process_job`: Find job by ID, use `RetryPolicy`.

    Wait, the instructions for `JobQueue` mention `get_next_job` returning the highest priority job. This implies storage.
    For `get_next_job` to handle FIFO correctly, I should probably keep a counter or just maintain a list and sort/pop.

    Let's look at the requirements again.
    `JobQueue` needs to:
    1. `add_job`
    2. `process_job` (uses `RetryPolicy`)
    3. `get_next_job` (highest priority, FIFO)

    Implementation details:
    - `Job` dataclass.
    - `RetryPolicy` - strictly no sleep.
    - `JobQueue`:
        - `jobs`: list of `Job` objects.
        - `process_job`: Find the job in the internal storage by id. Actually, `process_job` takes an ID and a `processor`. It doesn't say it removes it from the queue. It just says it must *use* `RetryPolicy`.
        - `get_next_job`: Returns highest priority.

    Actually, `JobQueue` management:
    If `get_next_job` is supposed to be a queue, `process_job` might be expected to work on the job that was just "gotten", or it might be an arbitrary ID search. The prompt says `process_job(self, job_id: str, processor: Callable)`. This implies searching by ID.

    Sorting logic for `get_next_job`:
    Highest priority first. If priority ties, earliest added first.
    I'll use a list and a counter for the insertion order.

    *   `Job`: `id`, `data`, `priority`, `retries`.
    *   `RetryPolicy`: `max_attempts`, `run` (returns `(bool, int)`).
    *   `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    Refining `JobQueue`:
    - `self.jobs = []`
    - `self.counter = 0`
    - `add_job`: `self.jobs.append(Job(..., counter=self.counter)))`. Wait, `Job` doesn't have a counter field in the requirement.
    - Okay, if `Job` doesn't have a counter, I can store a tuple in the list: `(priority, insertion_index, job_object)`. Or just sort the `Job` objects by priority (descending) and then by arrival order. Since `Job` is a dataclass, I can't easily add a counter unless I add it to the fields.
    - Better: Use a list of `Job` objects. When `get_next_job` is called, find the max priority. If multiple have max priority, pick the first one in the list.

    Wait, the prompt says `Job` dataclass has specific fields: `id`, `data`, `priority`, `retries`. I shouldn't add extra fields like `arrival_time`.
    So, in `JobQueue`, I can maintain the list and `get_next_job` will just find the max priority index and return it. To make it FIFO, I'll iterate and find the first occurrence of the max priority.

    Wait, if I `get_next_job`, should it be removed? Usually "get next" implies popping from a queue. But the prompt doesn't say "remove". However, `process_job` takes an ID. If I don't remove it, `get_next_job` will keep returning the same job.
    Actually, I'll implement `get_next_job` to remove the item from the list to behave like a queue.

    Wait, `process_job` says `process_job(self, job_id: str, ...)`. If I remove it in `get_next_job`, `process_job` won't find it.
    Maybe `get_next_job` doesn't remove it? Or maybe `process_job` removes it?
    Usually, in these prompts:
    `add_job` adds to internal state.
    `get_next_job` finds the best one.
    `process_job` executes it.
    If `get_next_job` is a "peek" type operation, I'll leave it. But "queue" usually implies consumption.
    Let's look at the signatures again.
    `process_job(self, job_id: str, ...)`
    `get_next_job(self) -> tuple[str, dict] | None`

    If I don't remove from the list in `get_next_job`, it's not really a queue being emptied. If I do remove it, `process_job` won't work unless the user calls `get_next_job` then `process_job`. But `process_job` takes an ID.
    Actually, a common pattern is that `get_next_job` returns the job and `process_job` is a separate way to interact.
    Let's assume `get_next_job` returns the highest priority job but *doesn't* remove it, or I'll just pick the best and return it. If it's a queue, I'll remove it.
    Given "FIFO order", removing makes sense. If I remove it, `process_job` would need to be called *before* `get_next_job` or `get_next_job` shouldn't remove.
    Actually, I'll have `get_next_job` return the highest priority job and *not* remove it, because `process_job` allows processing by ID regardless of position.

    Let's re-read: "return the highest-priority job's (id, data); FIFO order within the same priority."
    If I don't remove it, `get_next_job` will always return the same job. This is usually bad for a queue.
    Perhaps `process_job` should remove the job?
    No, the prompt doesn't say.
    I'll make `get_next_job` remove the job from the internal list. If the user wants to process it, they have to have the ID.
    But `process_job` takes an ID. If it's gone from the list, `process_job` can't find it.
    Okay, let's assume `get_next_job` finds the highest priority job and `process_job` also operates on the internal list. I will not remove anything unless specified.
    Actually, I'll just find the max priority and the first occurrence.

    Wait, if I don't remove, it's not a "Queue" in the sense of "consuming".
    I'll check the prompt again. "Refactor the job queue".
    Usually, `get_next_job` in a queue means "pop".
    If I pop, `process_job` must be called before `get_next_job`? No, that's backwards.
    Let's assume `process_job` is the action that removes it? No, signature doesn't suggest it.
    Let's assume `get_next_job` is a "peek" but the prompt says "Job Queue".
    I'll just implement it such that `get_next_job` finds the best one and `process_job` finds the one by ID. I won't remove them to ensure both methods work.

    Actually, looking at `process_job(self, job_id: str, ...)`, it implies the job is still in the queue.
    I'll just keep it simple:
    `add_job`: append to list.
    `process_job`: find by ID, use `RetryPolicy`.
    `get_next_job`: find max priority, then first index.

    Wait, `RetryPolicy` needs to be composed in `process_job`.
    ```python
    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = next((j for j in self.jobs if j.id == job_id), None)
        if not job:
            return False
        success, _ = self.retry_policy.run(Processor, job.data)
        return success
    ```

    Wait, `RetryPolicy.run` signature is `run(self, fn: Callable, data)`.
    So: `self.retry_policy.run(Processor, job.data)`.

    Wait, one more thing: `Job` dataclass needs `retries`. Does `RetryPolicy` update `retries`?
    The prompt says `RetryPolicy` returns `(success, attempts_made)`. It doesn't say it updates the `Job` object. However, `Job` has a `retries` field.
    I'll stick to the instructions: `RetryPolicy` returns the tuple, `JobQueue` uses it.

    Let's refine `JobQueue.get_next_job`:
    ```python
    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.jobs:
            return None
        # Find max priority
        max_p = max(j.priority for j in self.jobs)
        # Find first job with that priority
        for j in self.jobs:
            if j.priority == max_p:
                return (j.id, j.data)
    ```

    One last check on `RetryPolicy.run`:
    "calls `fn(data)`, retries on any exception until success or `max_attempts` reached, returns `(success, attempts_made)`."
    It doesn't say to update the `Job` dataclass's `retries` field, but since that field exists, I should probably think if I should. The prompt doesn't explicitly say to. I'll stick to the return value.

    One more detail: `RetryPolicy` - "Do NOT actually sleep."

    Let's double check the `Job` dataclass fields:
    `id`, `data`, `priority`, `retries`.

    Final check on `JobQueue.process_job`:
    "must USE RetryPolicy (compose, don't reimplement retry logic)".

    Wait, if `process_job` finds the job, and `RetryPolicy` is used, I should probably update `job.retries` if it fails? The prompt doesn't specify. I'll stick to the prompt.

    - `from dataclasses import dataclass`
    - `from typing import Callable`
    - `Job` dataclass.
    - `RetryPolicy` class.
    - `JobQueue` class.

    Actually, `JobQueue` needs a `RetryPolicy` instance. I'll initialize it in `__init__`.

    Wait, `get_next_job` return type: `tuple[str, dict] | None`.

    ```python
    from dataclasses import dataclass
    from typing import Callable

    @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: dict) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs = []
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            self.jobs.append(Job(id=job_id, data=data, priority=priority))

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if not job:
                return False
            success, _ = self.retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            max_priority = max(j.priority for j in self.jobs)
            for j in self.jobs:
                if j.priority == max_priority:
                    return (j.id, j.data)
            return None
    ```

    Wait, the prompt says `RetryPolicy` should be "composed". Does this mean `JobQueue` should take `RetryPolicy` as an argument?
    "orchestrates Jobs using RetryPolicy". Usually, this means it holds an instance of it.
    "compose, don't reimplement retry logic" in `process_job` means `process_job` should call `RetryPolicy.run`.

    Checking the `RetryPolicy` signature again:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    My signature matches.

    Checking `JobQueue` signatures:
    `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
    `process_job(self, job_id: str, processor: Callable) -> bool`
    `get_next_job(self) -> tuple[str, dict] | None`
    All match.

    One detail: "FIFO order within the same priority".
    My `get_next_job` implementation:
    ```python
    max_priority = max(j.priority for j in self.jobs)
    for j in self.jobs:
        if j.priority == max_priority:
            return (j.id, j.data)
    ```
    Since `self.jobs` is a list, the first item with `max_priority` is the one added earliest. This correctly handles FIFO for same priority.

    Final check on `Job` dataclass:
    `id`, `data`, `priority`, `retries`. Correct.

    Wait, `RetryPolicy` `__init__` signature: `__init__(self, max_attempts: int = 4)`. Correct.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable

@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: dict) -> tuple[bool, int]:
        attempts = 0
        while attempts < self.max_attempts:
            try:
                fn(data)
                return True, attempts + 1
            except Exception:
                attempts += 1
        return False, attempts

class JobQueue:
    def __init__(self):
        self.jobs = []
        self.retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        self.jobs.append(Job(id=job_id, data=data, priority=priority))

    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = next((j for j in self.jobs if j.id == job_id), None)
        if job is None:
            return False
        
        success, _ = self.retry_policy.run(Processor, job.data)
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        max_priority = max(j.priority for j in self.jobs)
        for j in self.jobs:
            if j.priority == max_priority:
                return (j.id, j.data)
        return None
```
</content>