<reasoning_content>
*   Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Requirement 1: `Job` dataclass with `id`, `data`, `priority`, and `retries`.
    *   Requirement 2: `RetryPolicy` class for retry-with-backoff logic.
        *   `__init__(max_attempts=4)`.
        *   `run(fn, data)`: call `fn(data)`, retry on exception. Return `(success, attempts_made)`. No actual sleeping.
    *   Requirement 3: `JobQueue` class.
        *   `add_job(id, data, priority)`.
        *   `process_job(id, processor)`: Use `RetryPolicy`.
        *   `get_next_job()`: Return `(id, data)` of highest priority (FIFO within same priority).
    *   Constraints: Standard library only, no explanations, just the code in `solution.py`.

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

        @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_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 a way to store jobs and sort them by priority (highest first) then FIFO.
        *   `PriorityQueue` from `queue` or `heapq`? Or just a sorted list. Since I need FIFO within same priority, and `Job` has priority.
        *   Wait, standard `PriorityQueue` is a min-heap. If priority is higher value = higher priority, I need to handle that.
        *   Actually, let's use a list and sort it or a `heapq`.
        *   To handle FIFO for same priority: `heapq` needs a secondary sort key (like a counter).
        *   However, the prompt says `get_next_job` returns the highest priority job.
        *   Let's use a simple list and sort or keep it sorted.
        *   Wait, `add_job` adds a job. `get_next_job` retrieves it.
        *   `process_job` takes a `Job_id`. This implies I need a mapping of ID to the actual job object or a list.

    *   *Revised structure for `JobQueue`:*
        ```python
        from dataclasses import dataclass, field
        from typing import Callable, List, Tuple, Optional
        import heapq

        @dataclass
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
            # To handle FIFO in heapq, we need a tie-breaker
            # But the prompt doesn't say to add a sequence number to Job.
            # I'll use a sequence counter inside JobQueue.
        ```

    *   *Wait, let's look at the prompt again.*
        `JobQueue`:
        - `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`

        If `process_job` is called by ID, does it remove the job? Usually, processing implies completion.
        `get_next_job` returns the highest priority.

    *   *Refining `JobQueue` internal storage:*
        I need to store the `Job` objects.
        When `get_next_job` is called, it should probably remove it from the queue? Or just peek? Usually, "get next" implies popping.
        Let's assume `get_next_job` removes the job.
        But `process_job` takes an ID. This suggests `JobQueue` holds a collection of jobs.

    *   *Actually, let's rethink `JobQueue`:*
        1. `jobs`: A dictionary `{id: Job}` to quickly find by ID for `process_job`.
        2. `sorted_jobs`: A list or heap to handle `get_next_job`.

    *   *Wait, if I process a job via ID, does it stay in the queue?*
        If `process_job` is called, it shouldn't be "next" anymore.
        If `get_next_job` is called, it shouldn't be "next" anymore.

    *   *Standard approach for these types of prompts:*
        Keep a list of `Job` objects.
        `add_job`: Add to list.
        `get_next_job`: Find highest priority (max value), return it, and remove from list.
        `process_job`: Find job by id, run with policy, then (maybe) remove? Or just return success.
        The prompt doesn't say to remove after `process_job`, but it's usually implied. However, `get_next_job` definitely implies removal.

    *   *Let's use a simple list for `JobQueue` and sort it by priority.*
        To handle FIFO, I'll use a counter.

    *   *RetryPolicy check:*
        `run(self, fn: Callable, data) -> tuple[bool, int]`
        "Do NOT actually sleep."

    *   *Imports:*
        `from dataclasses import dataclass`
        `from typing import Callable, Tuple, Dict, Optional, List`
        `import heapq` (Good for priority + tie-breaking).

    *   *Wait, I don't want to overcomplicate.*
        Let's use a list and `sort` for `get_next_job`. Or just `max()`.

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

    @dataclass
        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: Dict[str, Job] = {}
            self.order: List[Tuple[int, int, str]] = [] # (-priority, sequence, id)
            self.counter = 0
            self.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.heappush(self.order, (-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, _ = self.policy.run(processor, job.data)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]]:
            if not self.order:
                return None
            _, _, job_id = heapq.heappop(self.order)
            job = self.jobs.pop(job_id)
            return (job.id, job.data)
    ```

    *Self-Correction on `process_job`:*
    If `process_job` is called, should it remove from the queue? Usually, `process_job` happens after `get_next_job`.
    If I call `get_next_job`, the job is removed. If I call `process_job` directly, it stays in the priority heap? That's weird.
    Actually, `process_job` is usually used in conjunction with `get_next_job`.
    Let's look at the prompt: `process_job` must use `RetryPolicy`.
    Let's assume `process_job` doesn't remove the job, but `get_next_job` does. Or vice-versa.
    Actually, if `process_job` is called via ID, and `get_next_job` is called to get the next one, they are two different ways to interact.

    *Wait, `JobQueue.process_job` takes `job_id`.*
    If I call `get_next_job`, I get the `id`. Then I can call `process_job(id, ...)`.
    If `get_next_job` removes the item, `process_job` won't find it in `self.jobs`.
    So `get_next_job` should NOT remove it? Or `process_job` should not be called after `get_next_job`?
    "Return the highest-priority job's (id, data)". Usually, "Get" implies "Pop".

    Let's check the logic again:
    1. `add_job` -> adds to queue.
    2. `get_next_job` -> returns highest priority, FIFO.
    3. `process_job` -> calls `RetryPolicy` on a specific `job_id`.

    To make them work together:
    - `add_job` puts it in a list.
    - `get_next_job` finds the best one, removes it, and returns it.
    - `process_job` finds the job by ID and runs it. If it's already been "gotten", it's gone.

    Wait, if `get_next_job` removes it, `process_job` can't be used on it.
    Maybe `get_next_job` shouldn't remove it? No, that's "peek".
    Maybe `process_job` should be the one that removes it?
    Actually, the most standard implementation for this prompt's structure is:
    - `add_job` adds to a storage.
    - `get_next_job` returns the best one and **removes** it from the storage.
    - `process_job` processes a job currently in the storage.

    Wait, if `process_job` is called, does it remove it? The prompt doesn't say.
    Let's make `get_next_job` remove it to satisfy the "Queue" behavior.

    *Re-checking `RetryPolicy.run`:*
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    Returns `(success, attempts_made)`.

    *Refining `Job` class:*
    `priority` is `int`. Higher value = higher priority? Standardly, in a priority queue, higher number = higher priority.
    `heapq` is a min-heap. So I store `-priority`.

    *Refining `JobQueue` storage:*
    If I use a list and `sort()` it every time `get_next_job` is called, it's simpler to implement and handles the FIFO requirement easily (sort by `-priority`, then `index`).

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, 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:
            new_job = Job(id=job_id, data=data, priority=priority)
            self.jobs.append(new_job)

        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) -> Optional[Tuple[str, dict]] -> None:
            if not self.jobs:
                return None
            # Sort by priority (desc), then keep original order (FIFO)
            # Since Python' Ison is stable, we only need to sort by priority desc.
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            job = self.jobs.pop(0)
            return (job.id, job.data))
    ```

    *One detail:* `get_next_job` return type: `tuple[str, dict] | None`.
    The syntax `|` is Python 3.10+. `Optional` is safer if version isn't specified, but `|` is fine for modern Python.

    *Wait, FIFO check:*
    If I have:
    Job A: Priority 10, added at T1
    Job B: Priority 10, added at T2
    `sort(key=lambda x: x.priority, reverse=True)`
    Because `sort` is stable, A will still be before B. This satisfies FIFO.

    *Wait, `RetryPolicy.run` return type:*
    `tuple[bool, int]`.

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

    *Wait, one more thing:* `Job` dataclass.
    `priority` is `int`, `retries` is `int`. Default values `0`.

    *Double Check:*
    - `Job`: `id`, `data`, `priority`, `retries`. Correct.
    - `RetryPolicy`: `max_attempts=4`. `run` returns `(success, attempts_made)`. No sleep. Correct.
    - `JobQueue`: `add_job`, `process_job` (uses `RetryPolicy`), `get_next_job` (highest priority, FIFO). Correct.

    One minor detail: `RetryPolicy.run` says `attempts_made`. If it succeeds on first try, `attempts_made` is 1. If it fails 4 times, `attempts_made` is 4.

    Let's refine the `JobQueue` code slightly to be cleaner.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, 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, _ = self.retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] -> None:
            if not self.jobs:
                return None
            
            # Stability of sort ensures FIFO for same priority
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            job = self.jobs.pop(0)
            return (job.id, job.data))
    ```
    Wait, I used `Optional` then `|` in my head. I'll stick to `Optional` or just the `|` syntax since the prompt used `|`. Actually, I will stick to the prompt's style where possible.
    Wait, the prompt says: `get_next_job(self) -> tuple[str, dict] | None`.

    Wait, the `process_job` logic:
    If `get_next_job` pops the job, `process_job` won't find it.
    Usually, in these patterns, `process_job` is an alternative to `get_next_job`.
    Or, `get_next_job` doesn't pop.
    But "Queue" usually implies popping.
    If I call `get_next_job`, I get the job. If I then want to `process_job` it, I can't because it's gone from `self.jobs`.
    Maybe `process_job` shouldn't check `self.jobs`? But it needs the `data`.
    Let's assume the user wants `process_job` to find it in the list. If they call `get_next_job`, it's removed. If they want to process it, they should do it via `process_job` *before* calling `get_next_job`? Or `get_next_job` shouldn't pop.
    However, "Queue" usually implies popping.
    Let's look at `JobQueue` requirements again.
    1. `add_job`
    2. `process_job`
    3. `get_next_job`
    If `get_next_job` doesn't pop, it's a `peek`.
    If `process_job` doesn't pop, it's just a runner.
    If I make `get_next_job` pop, then `process_job` is only for jobs still in the queue.
    If I make `get_next_job` NOT pop, then the queue doesn't actually progress.
    Most likely, the intention is:
    - `add_job`: add to list.
    - `process_job`: find in list, run, (optional) remove.
    - `get_next_job`: find best, remove, return.
    
    I'll make `process_job` remove the job if successful (or just leave it). Actually, let's just follow the instructions strictly. Instructions don't say to remove in `process_job`. Instructions say `get_next_job` returns the job. I'll make `get_next_job` pop.

    Wait, one more look at `RetryPolicy.run`:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    "Do NOT actually sleep."
    The code I wrote doesn't sleep.

    ```python
    from dataclasses import dataclass
    from typing import Callable, 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, _ = self.retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> Tuple[str, dict] | None:
            if not self.jobs:
                return None
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            job = self.jobs.pop(0)
            return (job.id, job.data))
    ```
    Wait, `Tuple` is `tuple` in 3.9+. The prompt uses `tuple[...]`. I'll use `tuple` and `dict` as requested.

    Final check on `Job` fields: `id`, `data`, `priority`, `retries`.
    `priority` defaults to `0`. `retries` defaults to `0`.
    Yes.

    Wait, "FIFO order within the same priority".
    `sort(key=lambda x: x.priority, reverse=True)`
    In Python, `list.sort()` is stable.
    Example:
    Jobs: `[J1(p=10), J2(p=10)]` (added in this order)
    `sort(reverse=True)` -> `[J1(p=10), J2(p=10)]`
    `pop(0)` -> `J1`.
    Correct.

    One tiny thing: `process_job` should probably not find the job if it's already been popped by `get_next_job`. My code handles this (it returns `False` if not found). This is correct.

    ```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_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 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
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            job = selfs.jobs.pop(0) # Typo check: self.jobs
            return (job.id, job.data))
    ```
    Wait, `selfs` -> `self`.

    Is `tuple[bool, int]` allowed? Yes, in 3.9+.
    The prompt uses `tuple[str, dict] | None` which is 3.10+.
    I will stick to the prompt's style.
</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_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:
        new_job = Job(id=job_id, data=data, priority=priority)
        self.jobs.append(new_job)

    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
        
        # Python's sort is stable, preserving FIFO order for equal priorities
        self.jobs.sort(key=lambda x: x.priority, reverse=True)
        job = self.jobs.pop(0)
        return (job.id, job.data))
```
</content>