<reasoning_content>
*   Task: Refactor a job queue into three composable classes in `solution.py`.
    *   Classes: `Job`, `RetryPolicy`, `JobQueue`.
    *   `Job`: Dataclass with `id`, `data`, `priority`, `retries`.
    *   `RetryPolicy`: Encapsulates retry-with-backoff logic.
        *   `run` method: Calls a function, retries on exceptions, returns `(success, attempts_made)`. No actual sleeping.
    *   `JobQueue`: Orchestrates `Job`s using `RetryPolicy`.
        *   `add_job` method.
        *   `process_job` method: Must use `RetryPolicy`.
        *   `get_next_job` method: Priority order (highest priority first), then FIFO for ties.
    *   Constraints: Standard library only, no explanations, only code.

    *   `Job`:
        ```python
        from dataclasses import dataclass
        from typing import Dict

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

    *   `RetryPolicy`:
        ```python
        from typing import Callable, Tuple

        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_made = 0
                while attempts_made < self.max_attempts:
                    try:
                        fn(data)
                        return True, attempts_made + 1
                    except Exception:
                        attempts_made += 1
                return False, attempts_made
        ```

    *   `JobQueue`:
        Need to store jobs. A list is fine, but `get_next_job` needs to find the highest priority and FIFO.
        FIFO logic: If two jobs have same priority, the one added first comes first.
        Wait, if I use a list and sort every time, or keep it sorted?
        Actually, a standard approach for priority + FIFO is `heapq` but `heapq` is min-heap and doesn't inherently handle FIFO for equal priorities unless we include an entry counter.
        Alternatively, a list can be sorted by `(-priority, entry_index)`.

        Actually, `get_next_job` requirements:
        - Highest priority.
        - FIFO within same priority.

        Let's use a list `self.jobs = []` and an `entry_count`.
        Or just keep a list and find the max priority, then the first index.

        Wait, `process_job` takes `Job_id`. This implies I need to store jobs in a way that I can look them up by ID too, or just find them in the list.

        `JobQueue` methods:
        - `add_job(self, job_id, data, priority)`: Creates a `Job` object and stores it.
        - `process_job(self, job_id, Processor)`: Finds the job, uses `RetryPolicy`. If success, maybe remove it? The prompt doesn't say remove, but usually `process_job` implies completion. However, `get_next_job` usually removes it.
        Let's look at the prompt again.
        `get_next_job` -> `tuple[str, dict] | None`.
        `process_job` -> `bool`.

        Let's refine `JobQueue` storage:
        - `self.jobs`: List of `Job` objects.
        - `self.retry_policy`: An instance of `RetryPolicy`.

        Wait, `process_job` says "must USE RetryPolicy".
        Where does `RetryPolicy` get initialized? Usually inside `JobQueue` or passed in. The prompt doesn't specify the constructor for `JobQueue`, but standard composition suggests it has a `RetryPolicy` instance.

        Let's write the logic for `get_next_job`:
        1. Find max priority.
        2. Among those with max priority, find the one with the smallest index (FIFO).
        3. Return `(id, data)` and remove it? Or just return? Usually `get_next_job` removes. Let's assume removal since it's a queue.

        Wait, `process_job(self, job_id, processor)`:
        Does it remove it? Does it modify the job?
        If `process_job` is called by `job_id`, the job must still be in the queue.
        If `get_next_job` returns it, is it removed?
        If I have a queue, `get_next_job` usually pops it. But `process_job` takes an ID. If it's popped, the ID is gone.
        Maybe `process_job` doesn't pop.
        Let's look at the signature: `process_job(self, job_id: str, Processor: Callable) -> bool`.
        It takes an ID. This suggests jobs stay in the list until... what?
        Maybe `get_next_job` doesn't remove?
        "Return the highest-priority job's (id, data)".
        If it's a queue, it should be removed. But if it's removed, `process_job` needs to be called before or after?
        Usually:
        1. `get_next_job` -> returns `(id, data)` and removes it from internal storage.
        2. `process_job` -> finds job by `job_id` and processes it.
        But if `get_next_job` removes it, `process_job` won't find it.

        Alternative interpretation:
        `get_next_job` looks at the queue.
        `process_job` is the main action.
        Maybe `process_job` removes it upon success?

        Let's look at standard patterns.
        If `process_job` takes a `job_id`, the queue must contain a mapping of `job_id` -> `Job`.
        Let's use a dictionary for storage and a list for ordering?
        Actually, let's just use a list and `remove` when done.

        Revised `JobQueue` logic:
        - `self.jobs`: List of `Job`.
        - `add_job`: append to list.
        - `get_next_job`:
            - find max priority.
            - find first occurrence.
            - return values.
            - (Optional but logical) Remove from list.
        - `process_job`:
            - find job in list.
            - call `RetryPolicy.run`.
            - if success, remove? Or just return bool?
            - Let's assume it doesn't remove unless it succeeds, but the prompt doesn't specify removal.
            - However, `get_next_job` returning a value usually implies it's "taken".
            - Let's try: `get_next_job` returns the `(id, data)` and *doesn't* remove it. `process_job` finds it and *does* remove it upon success.
            - No, that's weird.

        Let's try a simpler interpretation:
        `get_next_job` returns the highest priority job.
        `process_job` takes a `job_id`, uses the `RetryPolicy`, and returns `True` if successful.
        I will store them in a list. I'll remove the job from the list in `process_job` only if it succeeds? Or just let it stay there?
        Usually, a queue consumes.
        Let's check the wording: "orchestrates Jobs".
        Actually, if `get_next_job` doesn't remove it, it's not really a "queue" in the destructive sense, but "peek".
        If I Peek, then Process, then what?
        Let's assume `get_next_job` removes the item from the "pending" list and returns it.
        Then `process_job` takes a `job_id`. This means the job must still be in the queue.

        Wait! If `get_next_job` removes it, `process_job(job_id)` will fail because it's no longer in the queue.
        Therefore, `get_next_job` must *not* remove it, OR `process_job` does not require the job to be in the queue.
        But `process_job` needs the `data`. If `get_next_job` returns `(id, data)`, the caller has the data.
        But `process_job(self, job_id, ...)` implies the `JobQueue` instance is doing the processing.
        If `JobQueue` does the processing, it needs access to the `data`.

        Okay, let's look at the signatures again.
        `process_job(self, job_id: str, Processor: Callable) -> bool`
        This signature implies that `JobQueue` still has the job inside it.
        So `get_next_job` must be a "peek" operation.
        OR, `process_job` is meant to be called *instead* of `get_next_job`?
        No, they are separate methods.

        Let's try this:
        1. `add_job` adds to a list.
        2. `get_next_job` finds the highest priority, returns it, but *does not* remove it.
        3. `process_job` finds the job by ID, executes via `RetryPolicy`, and *removes* it from the list upon success.

        Actually, let's look at `RetryPolicy.run`. It takes `data`.
        So `process_job` needs the `data` from the job.

        Wait, let's look at `Job` dataclass: `retries` is a field.
        `RetryPolicy.run` should probably update the `retries` field? No, it returns `attempts_made`.

        Let's re-read: "orchestrates Jobs using RetryPolicy".
        This implies `JobQueue` has a `RetryPolicy` instance.

    *   `RetryPolicy`:
        ```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`:
        ```python
        class JobQueue:
            def __init__(self):
                self.jobs = [] # List of Job objects
                self.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 get_next_job(self) -> tuple[str, dict] | None:
                if not self.jobs:
                    return None
                # Find highest priority (max value)
                # Then earliest index (FIFO)
                best_job = max(self.jobs, key=lambda j: j.priority)
                # Actually max() returns the first occurrence of the max value.
                # That's perfect for FIFO.
                return (best_job.id, best_job.data)

            def process_job(self, job_id: str, processor: Callable) -> bool:
                # Find job
                job = next((j for j in self.jobs if j.id == job_id), None)
                if not job:
                    return False
                
                success, attempts = self.policy.run(processor, job.data)
                if success:
                    # Remove job? Usually processes are removed on success.
                    self.jobs.remove(job)
                    return True
                return False
        ```

    Wait, if `get_next_job` doesn't remove it, and `process_job` removes it only on success, then what happens if `process_job` fails? It stays in the queue. This seems like a valid "orchestration" logic.

    One detail: "FIFO order within the same priority".
    `max(self.jobs, key=lambda j: j.priority)` will return the *first* job with the maximum priority.
    Example:
    Jobs: `[(id=1, p=10), (id=2, p=10), (id=3, p=5)]`
    `max` will return `id=1`. Correct.

    Wait, `RetryPolicy.run` says "retries on any exception until success or max_attempts reached".
    The code:
    ```python
    attempts = 0
    while attempts < self.max_attempts:
        try:
            fn(data)
            return True, attempts + 1
        except Exception:
            attempts += 1
    return False, attempts
    ```
    If `max_attempts` is 1, and it fails, `attempts` becomes 1, `1 < 1` is false, returns `(False, 1)`. Correct.
    If `max_attempts` is 1, and it succeeds, returns `(True, 1)`. Correct.

    Wait, `Job` dataclass:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    Do I need to update `retries`?
    The instructions don't say I have to update the `retries` field in the `Job` object, but the `RetryPolicy` returns `attempts_made`. Since `Job` has a `retries` field, maybe I should update it?
    "orchestrates Jobs using RetryPolicy".
    If `process_job` fails, should I increment `retries`?
    The `RetryPolicy` logic is encapsulated. `process_job` calls `policy.run`.
    If `run` returns `(False, attempts)`, I could update `job.retries = attempts`.
    However, the instructions don't explicitly say to update the dataclass field. I'll stick to the requirements strictly.

    One more check on `get_next_job`:
    "return the highest-priority job's (id, data); FIFO order within the same priority."
    If I use `max()`, it's $O(n)$.
    Is there a better way? `heapq` is $O(\log n)$ for push/pop.
    With `heapq`, to get max priority and FIFO, I'd need:
    `heapq.heappush(self.jobs, (-priority, entry_count, job_id, data))`
    But since I need to look up by `job_id` in `process_job`, I need a dictionary too.
    Given the requirements don't specify performance, a simple list is fine.

    Wait, I should check if `process_job` should remove the job regardless of success?
    Most job queues remove the job after an attempt.
    But `process_job` is often used to retry a specific job.
    If `process_job` is called, and it fails, usually it stays in the queue to be tried again.
    If `get_next_job` is a "peek", then `process_job` is the "executor".
    If `get_next_job` were a "pop", `process_job` would need the data passed into it. But `process_job` only takes `job_id`.
    So `get_next_job` *must* be a peek (or at least not a destructive pop).

    Let's double check `RetryPolicy` `run` signature:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    Wait, the prompt says `data`. I'll assume `dict` as per `Job` definition.

    Refining `JobQueue`:
    Should `JobQueue` have a `RetryPolicy` instance?
    "orchestrates Jobs using RetryPolicy: ... process_job ... must USE RetryPolicy".
    I will initialize it in `__init__`.

    One detail: `Job` `retries` field.
    If I don't use it, why is it there?
    Maybe `RetryPolicy` should update it? But `RetryPolicy` only takes `fn` and `data`. It doesn't take the `Job` object.
    So `JobQueue` must update it?
    If `process_job` is called, and `run` returns `(False, attempts)`, I can set `job.retries = attempts`.
    But the prompt doesn't ask for that. I'll leave it.

    Let's refine `JobQueue.process_job`:
    If `success` is `True`, should I remove it?
    If `process_job` is called on a `job_id`, and it succeeds, it's usually done.
    If it fails, it stays in the queue?
    If I don't remove it on success, the queue grows infinitely.
    So I will remove it on success.

    Wait, what if `process_job` is called, and `RetryPolicy` retries 3 times and fails?
    It stays in the queue.
    What if `get_next_job` is called? It still shows up.
    This seems like a robust worker pattern.

    Final check on types:
    `JobQueue.get_next_job` -> `tuple[str, dict] | None`
    `JobQueue.process_job` -> `bool`
    `RetryPolicy.run` -> `tuple[bool, int]`

    Wait, `RetryPolicy` `run` signature in prompt: `run(self, fn: Callable, data) -> tuple[bool, int]`.
    It doesn't specify the type of `data`. I'll use `dict` or `Any`.

    Let's write the code.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, List, Optional

    @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: List[Job] = []
            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)
            if success:
                self.jobs.remove(job)
                return True
            return False

        def get_next_job(self) -> Optional[Tuple[str, dict]]:
            if not self.jobs:
                return None
            # max() returns the first occurrence of the maximum value.
            # Since we append to the list, the first occurrence is the oldest (FIFO).
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
    ```

    Wait, I should use `Dict` instead of `dict` if I want to be very strict with types, but `dict` is fine in Python 3.9+. The prompt uses `dict` in the signature `data: dict`.

    One small fix: `max()` logic.
    If `self.jobs` is `[Job(id='a', p=10), Job(id='b', p=10)]`.
    `max(..., key=lambda j: j.priority)` will return `Job(id='a', p=10)`.
    This is correct for FIFO.

    Double check `RetryPolicy.run` `attempts_made`.
    If `max_attempts` = 1.
    Loop 1: `attempts` is 0. `0 < 1` is true.
    Try call.
    If fail: `attempts` becomes 1.
    Loop 2: `1 < 1` is false.
    Returns `(False, 1)`. Correct.

    Wait, `JobQueue.add_job` is supposed to take `priority` and `data`.
    Signature: `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`. Correct.

    One last thing: `from typing import ...`
    The prompt says "Use only the standard library". `typing` is standard library.

    Let's verify the `process_job` usage of `RetryPolicy`.
    "must USE RetryPolicy (compose, don't reimplement retry logic)".
    My code uses `self.retry_policy.run`. Correct.

    One detail: "Write only the Python code, no explanations."
    I will provide just the `solution.py` content.

    Is `tuple[bool, int]` valid syntax? In Python 3.9+, yes. The prompt uses `tuple[...]` notation, so I should use that instead of `Tuple[...]` from `typing` if I want to match the style, but I'll use `Tuple` from `typing` to be safe or just the builtin.
    The prompt uses `tuple[bool, int]` in the text but `dict` as well. I will stick to the style provided in the prompt.

    Actually, I'll use `typing` for `Callable` and `Optional` because they are not built-in as types in older versions, though `tuple` and `dict` are.

    Wait, `Job` fields: `id`, `data`, `priority`, `retries`.
    The prompt says: `priority: int = 0`, `retries: int = 0`.
    I will make sure the `Job` class exactly matches this.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional

    @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_made = 0
            while attempts_made < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts_made + 1
                except Exception:
                    attempts_made += 1
            return False, attempts_made

    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)
            if success:
                self.jobs.remove(job)
                return True
            return False

        def get_next_job(self) -> Optional[Tuple[str, dict]] | None:
            # wait, the prompt says tuple[...] | None. 
            # In Python 3.10+ | is valid.
            # Let's use the requested syntax.
            if not self.jobs:
                return None
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
    ```
    Wait, `tuple[str, dict] | None` is Python 3.10+. I'll use that.

    One small correction: `self.jobs.remove(job)`
    If there are multiple jobs with the same ID (unlikely but possible), `remove` removes the first occurrence. That's fine.

    Final check: `RetryPolicy.run`
    `fn(data)` is called. If it succeeds, `return True, attempts_made + 1`.
    If `max_attempts` is 4.
    Attempt 1 (0 < 4): fail -> `attempts_made` = 1.
    Attempt 2 (1 < 4): fail -> `attempts_made` = 2.
    Attempt 3 (2 < 4): fail -> `attempts_made` = 3.
    Attempt 4 (3 < 4): fail -> `attempts_made` = 4.
    Loop ends. Returns `(False, 4)`.
    Perfect.

    Wait, `max_attempts` is the limit.
    If `max_attempts` = 1.
    Loop 1 (0 < 1): fail -> `attempts_made` = 1.
    Loop ends. Returns `(False, 1)`. Correct.

    One final check on `Job` dataclass fields:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    Matches exactly.
</reasoning_content>

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

@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_made = 0
        while attempts_made < self.max_attempts:
            try:
                fn(data)
                return True, attempts_made + 1
            except Exception:
                attempts_made += 1
        return False, attempts_made

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)
        if success:
            self.jobs.remove(job)
            return True
        return False

    def get_next_job(self) -> Tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        # max() returns the first occurrence of the maximum value, 
        # satisfying the FIFO requirement for equal priorities.
        best_job = max(self.jobs, key=lambda j: j.priority)
        return (best_job.id, best_job.data)

```
</content>