<reasoning_content>
*   Goal: Refactor a job queue into three composable classes (`Job`, `RetryPolicy`, `JobQueue`) in `solution.py`.
    *   `Job` class:
        *   Type: `dataclass`.
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    *   `RetryPolicy` class:
        *   Fields: `max_attempts: int = 4`.
        *   Method: `run(self, fn: Callable, data) -> tuple[bool, int]`.
        *   Logic: Call `fn(data)`, retry on exception until success or `max_attempts` reached.
        *   Constraint: Do NOT actually sleep.
    *   `JobQueue` class:
        *   Method: `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`.
        *   Method: `process_job(self, job_id: str, Processor: Callable) -> bool`.
        *   Constraint: Must use `RetryPolicy` (composition).
        *   Method: `get_next_job(self) -> tuple[str, dict] | None`.
        *   Ordering: Highest priority first, FIFO within same priority.
    *   Constraint: Standard library only. No explanations.

    *   Need `dataclass`, `Callable`, `List`, `Tuple`, `Dict`, `Optional`, `heapq` or manual sorting.
    *   Wait, `JobQueue` needs to maintain order (priority, then FIFO). A `heapq` is efficient for priorities. Since priority is usually "higher number = higher priority" or vice versa? Usually, priority queues in Python are min-heaps. The prompt says "highest-priority job". I should define priority such that the highest number comes first. Or I can use a negative value in a min-heap.
    *   Wait, the requirement is "FIFO order within the same priority". `heapq` doesn't naturally guarantee FIFO for same keys unless the entry includes a counter.

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

    *   `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`:
        *   Storage: A list of jobs.
        *   `add_job`: Create `Job` instance and add to storage.
        *   `get_next_job`: Sort by `-priority`, then by arrival time (or index). Or just sort every time? No, sorting every time is O(N log N). Better to keep it sorted or use a heap.
        *   Actually, since `JobQueue` needs to handle `process_job` by `job_id`, I need a dictionary for lookups or a way to find the job by id.
        *   Let's check the requirements for `get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   `process_job`: Takes `job_id`. This implies `JobQueue` stores jobs by ID.

    *   *Refining `JobQueue` Storage*:
        To satisfy "highest-priority / FIFO" for `get_next_job` and `job_id` lookup for `process_job`:
        1. A list to maintain insertion order/priority?
        2. A dictionary for ID lookup?
        Let's just use a list for the jobs and find by ID or sort. Since it's a coding challenge, a simple list search/sort is often acceptable unless specified otherwise. However, for "highest priority/FIFO", sorting by `(-priority, insertion_index)` is the standard way.

    *   Wait, if `process_job` is called, does the job get removed? Usually, jobs are popped.
    *   Let's look at `process_job` signature: `process_job(self, job_id: str, Processor: Callable) -> bool`. It doesn't specify if it's removed. Usually, a processor finishes a job.

    *   *Re-evaluating `JobQueue` logic*:
        - `add_job`: store `Job` object.
        - `get_next_job`: Find the best job, return tuple.
        - `process_job`: Find job by id, run via `RetryPolicy`.

    *   *Implementation details*:
        - `JobQueue` needs a `RetryPolicy` instance.

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

    @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 get_next_job(self) -> Optional[Tuple[str, dict]]:
            if not self.jobs:
                return None
            # Sort by priority descending, then by original index (FIFO)
            # Since list append maintains order, we can use index.
            # However, simply sorting by priority is usually enough if we don't want to be too complex.
            # To strictly follow "FIFO within same priority", we sort by (-priority, insertion_index).
            # Since we don't have an explicit counter, we can use the list index.
            
            # Let's just sort and return the first one.
            # To preserve FIFO: we need to keep the original order.
            # A simple way:
            sorted_jobs = sorted(self.jobs, key=lambda x: (-x.priority, self.jobs.index(x)))
            # Wait, index(x) is O(N). 
            # Let's just use a list and find the best.
            
            # Better implementation:
            # Sort the list by priority descending.
            # But how to keep FIFO? 
            # Let's store a sequence number.
            pass
    ```

    *Correction on `JobQueue` logic*:
    Let's use a list of tuples `(priority, sequence_number, job_object)`.
    Actually, let's simplify. The instructions don't specify performance constraints. A list and `sorted()` is fine.
    To ensure FIFO for same priority:
    `sorted(self.jobs, key=lambda x: (-x.priority, self.jobs.index(x)))`
    Actually, if I just don't sort the list, I can find the max priority.

    Wait, `process_job` takes `job_id`.
    If I call `process_job`, I find the job in the list.

    Let's look at `process_job` again: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    It should use `RetryPolicy`.

    ```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
    ```

    Let's refine the sorting for `get_next_job`:
    To satisfy "highest-priority... FIFO order within same priority", and since `add_job` appends to the end:
    If I sort by `priority` descending, items with same priority will stay in their relative order *only if* the sort is stable. Python' `sorted` is stable.
    So: `sorted(self.jobs, key=lambda x: x.priority, reverse=True)[0]`

    *Wait*, what if `get_next_job` should *remove* the job?
    "return the highest-priority job's (id, data)". It doesn't say remove. If it doesn't remove, and I call it twice, I get the same result.

    *Wait*, "orchestrates Jobs". Usually, "process" means it's done. But the prompt doesn't say "remove". I will assume it doesn't remove unless it's implied by "Queue".
    Actually, in a Queue, `get_next_job` usually pops.
    But "return" usually implies just returning.
    Looking at `process_job`, it takes an ID. If `get_next_job` popped the job, `process_job` wouldn't find it.
    Therefore, `get_next_job` should probably not pop.

    *Let's check constraints again*:
    1. `Job` dataclass: `id`, `data`, `priority`, `retries`.
    2. `RetryPolicy`: `max_attempts`, `run`.
    3. `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    Let's write the code.

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

    @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 job is None:
                return False
            success, _ = self.retry_policy.run(Processor, job.data)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.jobs:
                return None
            # Python's sort is stable. 
            # Sorting by priority descending will keep original order for same priority.
            best_job = max(self.jobs, key=lambda j: j.priority)
            # Wait, max() doesn't guarantee FIFO for same priority.
            # max() returns the FIRST occurrence of the maximum value.
            # Since our list is FIFO (appended to end), the first occurrence 
            # of the max priority is indeed the oldest one.
            return (best_job.id, bestjob.data)
    ```

    Wait, `max()` logic:
    If `jobs = [Job(id='1', priority=10), Job(id='2', priority=10)]`
    `max(jobs, key=lambda x: x.priority)` returns the first one encountered.
    The first one is '1' (inserted first).
    So `max` works perfectly for FIFO within same priority.

    One more detail: `retries` field in `Job` dataclass. Is it used?
    The `RetryPolicy` doesn't update `job.retries`.
    The prompt says: `Job` has `retries: int = 0`.
    `RetryPolicy` doesn't explicitly say to update it, but it's a dataclass field.
    Should `process_job` update it?
    The prompt says: `process_job` must USE `RetryPolicy`.
    If `RetryPolicy` doesn't update it, maybe I should?
    Actually, `RetryPolicy.run` returns `(success, attempts_made)`.
    I can use that to update the `Job` object.

    Revised `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 job is None:
            return False
        success, attempts = self.retry_policy.run(Processor, job.data)
        job.retries = attempts # Or is it attempts_made?
        # The prompt says run returns (success, attempts_made ).
        # I'll assign that.
        return success
    ```
    Wait, does `attempts_made` include the successful attempt?
    "retries on any exception until success or max_attempts reached ... returns (success, attempts_made)".
    If it succeeds on the first try, `attempts_made` is 1.
    If it fails 3 times and succeeds on the 4th, `attempts_made` is 4.
    If it fails 4 times, `attempts_made` is 4.
    This seems correct.

    Wait, the prompt says `Job` has `retries` field. Usually `retries` means "number of failed attempts".
    But `attempts_made` includes the success.
    I'll just assign `job.retries = attempts`.

    One final check on `get_next_job`.
    `return the highest-priority job's (id, data)`
    If multiple jobs have the same highest priority, return the one added first.
    `max(self.jobs, key=lambda j: j.priority)` does exactly this.

    Wait, `retries` vs `attempts_made`.
    If `max_attempts` is 4.
    Attempt 1: Fail.
    Attempt 2: Fail.
    Attempt 3: Fail.
    Attempt 4: Fail.
    Result: `(False, 4)`.
    If I assign `job.retries = 4`, it makes sense.

    Double check `RetryPolicy.run` signature: `run(self, fn: Callable, data) -> tuple[bool, int]`.
    It says `data` (no type hint in the instruction, but `Job` has `dict`).
    I will use `data: dict` for consistency.

    One last look:
    `Job` fields: `id`, `data`, `priority`, `retries`.
    `RetryPolicy` `__init__`: `max_attempts: int = 4`.
    `RetryPolicy` `run`: `fn`, `data`.
    `JobQueue` `add_job`: `job_id`, `data`, `priority`.
    `JobQueue` `process_job`: `job_id`, `Processor`.
    `JobQueue` `get_next_job`: `tuple[str, dict] | None`.

    Let's refine the `process_job` to be safe.

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

    @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: 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 job is None:
                return False
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries = attempts
            return success

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

    Wait, `max_attempts` is 4. If `attempts_made` is 4 and it's still failing, it returns `False`.
    The loop:
    Iter 0: `attempts_made` is 0. `0 < 4` is true. `fn` fails. `attempts_made` becomes 1.
    Iter 1: `1 < 4` is true. `fn` fails. `attempts_made` becomes 2.
    Iter 2: `2 < 4` is true. `fn` fails. `attempts_made` becomes 3.
    Iter 3: `3 < 4` is true. `fn` fails. `attempts_made` becomes 4.
    Loop ends.
    Returns `(False, 4)`.
    If it succeeded on Iter 3:
    Iter 3: `3 < 4` is true. `fn` succeeds. Returns `(True, 3+1)`.
    Correct.

    One detail: `JobQueue.get_next_job` return type `tuple[str, dict] | None`.
    I'll use `Optional[Tuple[str, dict]]`.

    Wait, I should check if `data` in `RetryPolicy.run` is `dict` or `Any`. The prompt says `data` in the signature, and `Job` says `data: dict`. I'll use `dict`.

    Wait, the prompt doesn't say `JobQueue` should remove the job from the list in `process_job` or `get_next_job`.
    Usually, a "Queue" pops. But `process_job` uses an ID. If I pop in `get_next_job`, I can't process it by ID later.
    Actually, `process_job` is the one that actually "does" the work.
    I will leave the list intact to satisfy the `job_id` lookup.

    Wait, `Job` dataclass `retries` default is 0.
    `JobQueue.add_job` takes `priority` default 0.
    `RetryPolicy` `max_attempts` default 4.

    Final check on `JobQueue.get_next_job` return value.
    "return the highest-priority job's (id, data)"
    I'll return `(job.id, job.data)`.

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

    @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: 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 job is None:
                return False
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries = attempts
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.jobs:
                return None
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
    ```
    Wait, is it `data` or `data:`? Prompt: `run(self, fn: Callable, data)`.
    I will stick to the prompt's signature as closely as possible.
</reasoning_content>

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

@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: 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 job is None:
            return False
        
        success, attempts = self.retry_policy.run(Processor, job.data)
        job.retries = attempts
        return success

    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 items are appended, the first occurrence is the oldest (FIFO).
        best_job = max(self.jobs, key=lambda j: j.priority)
        return (best_job.id, best_job.data)
```
</content>