<reasoning_content>
*   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Constraints:
        *   `Job`: Dataclass (`id`, `data`, `priority`, `retries`).
        *   `RetryPolicy`: Handles retry with backoff (logic only, no actual sleep). Returns `(success, attempts_made)`.
        *   `JobQueue`: Orchestrates jobs. `add_job`, `process_job` (uses `RetryPolicy`), `get_next_job` (highest priority, FIFO within same priority).
        *   Library: Only standard library.
        *   Output: Python code only, no explanation.

    *   `from dataclasses import dataclass`
    *   `from typing import Callable, Tuple, Dict, List, Optional`
    *   `from heapq import heappush, heappop` (for priority queue efficiency) or just a sorted list/simple search for simplicity if small scale, but `heapq` is standard and good for priorities.
    *   Wait, `heapq` provides min-heap. Highest priority usually means higher numbers, but `heapq` gives smallest first. I'll need to store priority as negative if highest = largest, or just handle it during processing. Usually, "Highest priority" implies 10 > 1. So I'll store negative priority or sort.
    *   Wait, `Job` has a `retries` field. Should `RetryPolicy` update it? The prompt says `process_job` must use `RetryPolicy`.

    *   `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_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`:
        *   Needs to store jobs. A list or a priority queue?
        *   `add_job`: adds a job.
        *   `process_job`: retrieves job? No, the signature is `process_job(self, job_id: str, processor: Callable)`. This implies the job is already identified or part of the queue. Wait, if `process_job` takes an `id`, does it find it in the queue?
        *   `get_next_job`: returns highest priority.
        *    Let's look at the signatures again:
            *   `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` takes a `job_id`, does it remove it? Usually, processing implies completion.
        *   Wait, the requirement for `get_next_job` is "highest-priority job's (id, data); FIFO order within the same priority".

    *   Data structure for `JobQueue`:
        *   To maintain FIFO within priority, I'll use a list of jobs and sort by priority (desc) then order of insertion.
        *   Or better: A list of tuples `(-priority, insertion_index, job_id, data)`. Use `heapq`.

    *   Wait, `process_job` takes `job_id`. Does it search the internal storage? Yes, likely.

    *   Refining `JobQueue`:
        ```python
        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:
                job = Job(id=job_id, data=data, priority=priority)
                self.jobs.append(job)

            def get_next_job(self) -> tuple[str, dict] | None:
                if not self.jobs:
                    return None
                # Sort by priority desc, then original order (index)
                # Since we append, index is implied by list order.
                # Actually, if I want FIFO, I should just sort by priority only.
                # But Python' own sort is stable.
                self.jobs.sort(key=lambda x: x.priority, reverse=True)
                job = self.jobs[0]
                # Wait, get_next_job usually means 'pop' it?
                # "return the highest-priority job's (id, data)"
                # If I don't pop it, it will always return the same one.
                # Usually 'get' implies consumption in a queue context,
                # but the prompt doesn't say remove.
                # However, process_job takes an ID.
                # Let's assume it stays in the list until processed or removed.
                # But usually, get_next_job returns the one to be processed.
                # I will pop it to ensure it's a queue.
                # Wait, if I pop it in get_next_job, how does process_job find it?
                # Ah, maybe process_job doesn't remove it?
                # Let's look at the method signature again.
                # process_job(self, job_id: str, processor: Callable) -> bool
                # This implies process_job is called on a specific ID.
                # Maybe I shouldn't pop in get_next_job.
                # But then how do we prevent same job from being returned?
                # Usually, a Queue "gets" then "processes".
                # If I don't pop, I'll just return the best one and let the user decide.
                # But that's not a queue behavior.
                # I'll remove it in get_next_job.
                pass
        ```

    *   Wait, if I remove it in `get_next_job`, the user doesn't have the object, only the ID and data.
    *   If `process_job` is called with `job_id`, it needs to find the job in the collection.
    *   Actually, the prompt doesn't say `process_job` removes it.
    *   Let's try this: `get_next_job` returns the highest priority job (not removing it). No, that's a "peek".
    *   Let's look at the standard behavior of such tasks: `get_next_job` is the way to fetch the item.
    *   If I don't remove it, the user calls `get_next_job` multiple times and gets the same one. That's bad.
    *   I will remove the job in `get_next_job`. But then how does `process_job` work? `process_job` might be called on a job *still in the queue*?
    *   Actually, usually `process_job` would be called on a job that was *already* retrieved? But the signature is `process_job(self, job_id: str, ...)`.
    *   If it's in the queue, I can find it by ID.
    *   Let's look at the logic:
        1. Add Job.
        2. `get_next_job` returns highest. (Does it remove? I'll assume yes, but it returns id and data).
        3. `process_job` is called. If it was removed in step 2, `process_job` won't find it.
    *   Wait! Maybe `process_job` is meant to be used on jobs that are *still* in the queue?
    *   Let's rethink. If `process_job` takes an ID, it probably looks up the job in an internal dictionary/list.
    *   If `get_next_job` doesn't remove it, then `get_next_job` is just a peek.
    *   Let's check the wording: "return the highest-priority job's (id, data)".
    *   In many coding challenges, `get_next_job` implies "pop".
    *   If I pop it, then `process_job` being called with `job_id` won't find it.
    *   UNLESS `process_job` is used *before* `get_next_job`? No, that's weird.
    *   UNLESS the queue stores jobs in a way that `process_job` can see them even if they are "next".
    *   Let's assume `process_job` removes it if successful? No, signature returns bool.
    *   Actually, most likely:
        - `add_job` puts it in a list.
        - `get_next_job` finds the best one, removes it, and returns it.
        - `process_job` is a separate way to process a job by ID (perhaps for manual handling).
    *   Wait, if `process_job` is called with an ID, and I already popped it in `get_next_job`, it's gone.
    *   Let's assume `get_next_job` does NOT remove it, but the user is expected to call `process_job` which *does* remove it or handle the status.
    *   Actually, I'll just follow the instructions literally.
    *   `get_next_job`: Find highest priority. Return `(id, data)`.
    *   `process_job`: find job by ID. Use `RetryPolicy`. If success, remove from queue. Return `True`.
    *   Wait, if I remove it in `process_job`, but `get_next_job` doesn't remove it, then `get_next_job` will keep returning the same job until it's processed. This makes sense for a "peek" style queue.

    *   Wait, "FIFO order within the same priority".
    *   If I use a list and `sort(key=lambda x: x.priority, reverse=True)`, the stability of Python's sort ensures FIFO.

    *   Wait, `RetryPolicy` signature: `run(self, fn: Callable, data) -> tuple[bool, int]`.
    *   It doesn't take the `Job` object, just the `data`.

    *   Let's refine `JobQueue.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)
            if success:
                self.jobs.remove(job)
                return True
            return False
        ```

    *   Refining `JobQueue.get_next_job`:
        ```python
        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            # Sort by priority DESC, but stay stable for FIFO
            # Since list order is insertion order, we sort by priority only.
            # Actually, Python's sort is stable.
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            best_job = self.jobs[0]
            return (best_job.id, best_job.data)
        ```

    *   Wait, if `get_next_job` doesn't remove it, and `process_job` removes it...
    *   If `process_job` fails, does it remove it? The prompt doesn't say.
    *   Actually, if `process_job` fails (after all retries), it usually stays in the queue or goes to a dead letter queue. I'll leave it in the queue if it fails.

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

    *   Let's double check `Job` dataclass: `id`, `data`, `priority`, `retries`.
    *   `retries` field exists, but `RetryPolicy` doesn't seem to update it based on the signature `run(self, fn, data)`. I'll just ignore the `retries` field for the policy since it's not asked to be updated, but it's part of the dataclass.

    *   One more look at `get_next_job`: "FIFO order within the same priority".
    *   To guarantee FIFO, I should probably use a secondary key (insertion index) or just rely on the stable sort.
    *   If I use `self.jobs.sort(key=lambda x: x.priority, reverse=True)`, and I don't change the order of items with the same priority, it stays FIFO.

    *   Wait, I'll use a list and sort it inside `get_next_job`.

    *   Let's check imports: `dataclass`, `Callable`, `Tuple`, `Dict`, `Optional`, `List`.

    *   One detail: `RetryPolicy.run` signature: `(self, fn: Callable, data) -> tuple[bool, int]`.
    *   It takes `data`, not `Job`.

    *    Let's write the code.

    *    Re-reading `process_job`: "must USE RetryPolicy (compose, don't reimplement retry logic)".
    *    This means the loop and try/except must be inside `RetryPolicy`.

    *    Re-reading `JobQueue.get_next_job`: "return the highest-priority job's (id, data)".
    *    If I don't remove it, and someone calls it twice, they get the same thing. That's usually not what "get" does in a queue, but I'll stick to the literal description.
    *    Wait, if I don't remove it, and the user calls `process_job`, and it succeeds, it gets removed. This is a valid state machine.

    *    Actually, `JobQueue.process_job` doesn't say it removes it. If I don't remove it, `get_next_job` will always see it.
    *    Let's assume `process_job` should remove it on success. Or maybe `process_job` doesn't remove it?
    *    Let's look at the instruction: `process_job(self, job_id: str, processor: Callable) -> bool`.
    *    If I don't remove it, the job stays in the queue forever? That seems wrong for a "process" method. I'll remove it on success.

    *    Wait, I'll check if there's any specific "remove" instruction. No.
    *    Let's consider the `retries` field in `Job`. If `RetryPolicy` is supposed to work with `Job`, it would probably update `job.retries`. But the signature doesn't pass the `Job` object.
    *    So `retries` is just a field to exist.

    *    Let's refine `JobQueue.get_next_job`:
    *    To ensure FIFO, I will sort by priority. Since `list.sort` is stable, the order of jobs with the same priority remains the same as their insertion order.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, 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_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:
            job = Job(id=job_id, data=data, priority=priority)
            self.jobs.append(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)
            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
            
            # Sort by priority (highest first). 
            # Python's sort is stable, preserving FIFO for same priority.
            sorted_jobs = sorted(self.jobs, key=lambda x: x.priority, reverse=True)
            best_job = sorted_jobs[0]
            return (best_job.id, best_job.data)
    ```

    *    Wait, `sorted()` creates a new list. `self.jobs.sort()` is in-place. Either is fine.
    *    The requirement for `get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
    *    If I don't remove it in `get_next_job`, but I do remove it in `process_job` on success, this is consistent.

    *    Final check on signatures:
    *    `Job`: `id`, `data`, `priority`, `retries`. OK.
    *    `RetryPolicy`: `__init__(max_attempts)`, `run(fn, data)`. OK.
    *    `JobQueue`: `add_job(id, data, priority)`, `process_job(id, processor)`, `get_next_job()`. OK.

    *    One minor detail: `Tuple` vs `tuple`. The prompt uses `tuple[bool, int]`. This is Python 3.9+ syntax. I'll use the exact syntax provided in the prompt.

    *    Wait, the prompt says `tuple[bool, int]`. This is valid in 3.9+. I'll use `tuple` and `dict` instead of `Tuple` and `Dict` from `typing` to match the user's style, but I'll still import `Callable` as it's not built-in as a type hint in all versions. Actually, I'll stick to `typing` for `Callable` and `List` to be safe, but use `tuple` and `dict` as they are built-in.

    *    Re-checking `RetryPolicy.run`: "returns (success, attempts_made)". OK.

    *    `JobQueue.get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
    *    If I have jobs: `[ (id=1, p=1), (id=2, p=1) ]`.
    *    `sorted(..., reverse=True)` will keep `1` before `2`. Correct.

    *    One edge case: `process_job` signature is `process_job(self, job_id: str, processor: Callable) -> bool`.
    *    If the job is not found, it should return `False`.

    *    Let's ensure `RetryPolicy` is used in `process_job`. Yes.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, 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_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:
        job = Job(id=job_id, data=data, priority=priority)
        self.jobs.append(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)
        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
        
        # Python's sort is stable, so it preserves FIFO order for same priority
        # when sorted by priority descending.
        sorted_jobs = sorted(self.jobs, key=lambda x: x.priority, reverse=True)
        best_job = sorted_jobs[0]
        return (best_job.id, best_job.data)
```
</content>