<reasoning_content>
*   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   `Job`: Dataclass (`id`, `data`, `priority`, `retries`).
    *   `RetryPolicy`: Handles retries with backoff (logic-wise, though no actual sleep is requested). `run(fn, data)` returns `(success, attempts_made)`.
    *   `JobQueue`: Orchestrates jobs.
        *   `add_job`: Adds a job.
        *   `process_job`: Uses `RetryPolicy` to execute a processor.
        *   `get_next_job`: Returns highest-priority job (FIFO for same priority).
    *   Constraint: Use only standard library. No explanations.

    *   `id: str`
    *   `data: dict`
    *   `priority: int = 0`
    *   `retries: int = 0`

    *   `__init__(max_attempts: int = 4)`
    *   `run(fn: Callable, data) -> tuple[bool, int]`
    *   Logic: Try `fn(data)`. If exception, retry until `max_attempts`. Return `(True, attempts)` if success, `(False, attempts)` if all attempts failed.
    *   Note: "Do NOT actually sleep".

    *   Needs a way to store jobs by priority and then by order of arrival (FIFO).
    *   A priority queue (`heapq`) is standard, but `heapq` is a min-heap. To get "highest priority" (usually higher number = higher priority), we might need to negate priority or just treat lower numbers as higher priority. Usually, in these coding prompts, higher integer = higher priority. Let's check: "highest-priority job... FIFO order within the same priority".
    *   To handle FIFO within the same priority in a heap: use a counter (sequence number) as a tie-breaker.
    *   `add_job(job_id, data, priority)`: Creates a `Job` and pushes it to the queue.
    *   `get_next_job()`: Pops the job and returns `(id, data)`.
    *   `process_job(job_id, processor)`:
        1.  Find the job by ID (Wait, the prompt says `process_job(self, job_id, processor)`). This implies we need a way to look up a job by ID.
        2.  Wait, if `get_next_job` returns the job, and `process_job` takes a `job_id`, how are they linked?
        3.  Usually, in these patterns:
            *   `add_job` puts it in the queue.
            *   `get_next_job` retrieves it for processing.
            *   `process_job` executes it.
        4.  Wait, the signature `process_job(self, job_id, processor)` suggests the job is already known or stored in a map. If it's in a heap, searching by ID is $O(N)$.
        5.  Let's re-read carefully:
            *   `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`
        6.  This implies we need a dictionary to store jobs by ID for `process_job` and a priority queue for `get_next_job`.
        7.  However, if `get_next_job` removes the job from the queue, `process_job` might be acting on a job that was just "gotten".
        8.  Let's assume:
            *   `add_job` adds to both a dict (for lookup) and a heap (for ordering).
            *   `get_next_job` pops from the heap and returns info. It should probably also remove it from the dictionary to avoid leaks, OR `process_job` handles the removal.
            *   If `process_job` takes `job_id`, it must exist in the dict.
            *   Wait, if `process_job` uses `RetryPolicy`, it's doing the work.
            *   Actually, a cleaner implementation for `get_next_job` in a production system is to "lease" the job. But for a coding test:
                *   `add_job`: `self.jobs[job_id] = Job(...)`, `heapq.heappush(self.queue, (-priority, counter, job_id))`
                *   `get_next_job`: `heapq.heappop`, return info.
                *   `process_job`: Get job from `self.jobs`, run `RetryPolicy.run(processor, job.data)`, return boolean.
            *   Wait, if `get_next_job` is used to *pick* the next job, does it remove it? Usually, "get" implies removal in simple queue implementations. If it doesn't remove it, `get_next_job` would return the same job forever. If it *does* remove it, then `process_job(job_id, ...)` is problematic if the job is already gone.
            *   Let's look at the signature again. `process_job(self, job_id, processor)`. This implies the job is identified by ID.
            *   Let's assume the workflow is:
                1. `add_job`
                2. `get_next_job` -> returns (id, data).
                3. `process_job(id, processor)` -> executes.
            *   This is slightly contradictory. If `get_next_job` returns the job, why would `process_job` take the `job_id`? Why not just `process_job(job)`?
            *   Maybe `get_next_job` *doesn't* remove the job, but marks it? Or maybe `process_job` is the one that removes it?
            *   If `get_next_job` pops from heap, it returns the ID. Then `process_job` is called with that ID.
            *   If `process_job` fails, does it put it back? The prompt doesn't say.
            *   Let's assume:
                - `add_job`: Stores in `self._jobs = {id: Job}` and `self._queue = []` (heap).
                - `get_next_job`: Pops from `self._queue`. Returns `(id, data)`. Note: to handle the "same priority FIFO", we use a counter.
                - `process_job`: Looks up `job = self._jobs[job_id]`. Uses `RetryPolicy.run`. If successful, `del self._jobs[job_id]`. If failed, maybe leave it? The prompt doesn't specify cleanup on failure.
            *   Wait, if `get_next_job` removes it from the heap, but `process_job` is what actually processes it, how do we handle the case where `get_next_job` is called but `process_job` isn't?
            *   Standard pattern:
                - `add_job`: `self._jobs[id] = Job(...)`, `heappush(self._heap, (-priority, count, id))`
                - `get_next_job`: `heappop` -> `id`. Return `(id, job.data)`.
                - `process_job`: `job = self._jobs.pop(job_id)`. `policy.run(processor, job.data)`.
            *   Wait, if `get_next_job` pops the ID from the heap, `process_job` needs to be able to find the job. If `get_next_job` pops it, it's no longer in the heap. This is fine. The ID is returned.
            *   Is there a conflict if `get_next_job` returns the job but doesn't remove it? If it doesn't remove it, the next call to `get_next_job` returns the same one. So `get_next_job` *must* remove it from the heap.
            *   If `get_next_job` removes it from the heap, it's effectively "taken".
            *   The `Job` dataclass has a `retries` field. The `RetryPolicy.run` returns `attempts_made`. It is logical that `Job.retries` should be updated, but the prompt says `RetryPolicy` "encapsulates retry-with-backoff" and `JobQueue` "orchestrates Jobs using RetryPolicy". It doesn't explicitly say `JobQueue` must update the `retries` field in the `Job` object, but it's good practice. However, the `process_job` signature is `(job_id, processor)`.

    *   `dataclasses.dataclass`
    *   `heapq` (for priority queue)
    *   `typing.Callable`, `typing.Tuple`, `typing.Dict`, `typing.Optional`, `typing.Any`
    *   `itertools.count` (for FIFO tie-breaking)

    *   `Job`: `dataclass(id, data, priority, retries)`
    *   `RetryPolicy`: `run(fn, data)`:
        ```python
        attempts = 0
        for attempts in range(1, self.max_attempts + 1):
            try:
                fn(data)
                return True, attempts
            except Exception:
                pass
        return False, attempts
        ```
    *   `JobQueue`:
        *   `self._jobs: dict[str, Job]`
        *   `self._queue: list` (heap of `(-priority, sequence, job_id)`)
        *   `self._counter = count()`
        *   `self._retry_policy = RetryPolicy()` (Note: Prompt says `JobQueue` orchestrates jobs using `RetryPolicy`. It doesn't say I should pass the policy in `__init__`, but `RetryPolicy` is a class. I'll instantiate it in `__init__` or create one. Actually, the prompt says `JobQueue` orchestrates using `RetryPolicy`, implying it has access to one.)
        *   `add_job`: `job = Job(job_id, data, priority)`. `self._jobs[job_id] = job`. `heappush(self._queue, (-priority, next(self._counter), job_id))`.
        *   `get_next_job`: `if not self._queue: return None`. `p, s, jid = heappop`. `job = self._jobs.get(jid)`. If `job` is None (could happen if deleted), `get_next_job` might need to loop? No, let's assume `job_id` is unique and not deleted until processed. Wait, if I `pop` from heap in `get_next_job`, I need to return the data.
        *   `process_job`: `job = self._jobs.get(job_id)`. `success, attempts = self._retry_policy.run(processor, job.data)`. `if success: del self._jobs[job_id]`. (Wait, if I delete from `_jobs` in `process_job`, but `get_next_job` already popped from the heap, that works).

    *   "FIFO order within the same priority":
        *   Priority 10, then Priority 10.
        *   Heap should store `(-priority, sequence, job_id)`.
        *   Lower sequence number comes first.
    *   `RetryPolicy.run` returns `(success, attempts_made)`.
    *   `process_job` returns `bool`.
    *   `get_next_job` returns `tuple[str, dict] | None`.

    Wait, one detail: `process_job` uses `RetryPolicy`. Does `process_job` remove the job from the queue?
    If `get_next_job` is used to "get" the job to work on it, and `process_job` is used to "run" it, the job is essentially removed from the "available" queue by `get_next_job`.

    Refined `JobQueue` state:
    - `self.pending_jobs`: A heap of `(-priority, sequence, job_id)`.
    - `self.all_jobs`: A dictionary `job_id -> Job`.

    `add_job(id, data, priority)`:
    - `job = Job(id, data, priority)`
    - `all_jobs[id] = job`
    - `heappush(pending_jobs, (-priority, count, id))`

    `get_next_job()`:
    - `if not pending_jobs: return None`
    - `_, _, jid = heappop(pending_jobs)`
    - `job = all_jobs[jid]`
    - `return jid, job.data`

    `process_job(jid, processor)`:
    - `job = all_jobs.get(jid)`
    - `if not job: return False`
    - `success, attempts = retry_policy.run(processor, job.data)`
    - `if success: del all_jobs[jid]`
    - `return success`

    Wait, if `get_next_job` is called, the job is popped from `pending_jobs`. If `process_job` is then called, and it fails, where does the job go? The prompt doesn't specify re-queuing. It just says "orchestrates Jobs using RetryPolicy". I will stick to the logic that `process_job` is the final step.

    Wait, looking at the `Job` definition: `retries: int = 0`. This field is never used in the requirements other than being in the dataclass. I'll include it in the dataclass as requested.

    *   `Job` dataclass: `id`, `data`, `priority`, `retries`.
    *   `RetryPolicy`: `run(fn, data)` returns `(bool, int)`. No sleep.
    *   `JobQueue`: `add_job`, `process_job` (uses policy), `get_next_job`.
    *   Priority: Highest priority first. FIFO for same priority.
    *   `get_next_job` returns `(id, data)` or `None`.

    One detail: `get_next_job` returning `(id, data)` means the user gets the info. `process_job` then takes the `id`. This fits the structure.

    *   `from dataclasses import dataclass`
    *   `from typing import Callable, Tuple, Dict, Optional, Any`
    *   `import heapq`
    *   `from itertools import count`

    Wait, `RetryPolicy.run` signature: `run(self, fn: Callable, data) -> tuple[bool, int]`.

    `JobQueue.process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.

    `JobQueue.get_next_job` signature: `get_next_job(self) -> tuple[str, dict] | None`.

    Let's double check the `RetryPolicy` requirements: "retries on any exception until success or `max_attempts` reached".

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

        def run(self, fn: Callable, data: Any) -> tuple[bool, int]:
            attempts_made = 0
            for i in range(self.max_attempts):
                attempts_made += 1
                try:
                    fn(data)
                    return True, attempts_made
                except Exception:
                    continue
            return False, attempts_made
    ```

    Wait, `attempts_made` should be correct. If `max_attempts` is 4, and it fails 4 times, `attempts_made` is 4. If it succeeds on 1st try, `attempts_made` is 1.

    ```python
    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._queue = []
            self._counter = count()
            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[job_id] = job
            heapq.heappush(self._queue, (-priority, next(self._counter), job_id))

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, attempts = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self._queue:
                return None
            _, _, job_id = heapq.heappop(self._queue)
            job = self._jobs.get(job_id)
            if not job: # Handle case where job might have been deleted
                return self.get_next_job()
            return job_id, job.data
    ```

    Wait, there is a potential issue. If `get_next_job` pops a job, but `process_job` is never called, the job is lost from the heap but stays in `self._jobs`. If `get_next_job` is called again, it works fine. But the job is no longer "in queue". This is standard for a "pop" operation.

    However, there is one edge case: if `get_next_job` pops a job that was already deleted from `_jobs` (e.g. by another process calling `process_job`?), it should skip. Added a check.

    Is `max_attempts` used correctly?
    If `max_attempts` is 1:
    Loop `range(1)` -> `i=0`.
    `attempts_made = 1`.
    `try fn(data)` -> succeeds -> `return True, 1`.
    `try fn(data)` -> fails -> loop ends -> `return False, 1`.
    Correct.

    "FIFO order within the same priority".
    Priority: 10, 10, 5.
    Heap: `(-10, 0, 'id1'), (-10, 1, 'id2'), (-5, 2, 'id3')`.
    Pop 1: `(-10, 0, 'id1')`.
    Pop 2: `(-10, 1, 'id2')`.
    Pop 3: `(-5, 2, 'id3')`.
    This correctly implements highest priority first, then FIFO.

    "Do NOT actually sleep".
    My `RetryPolicy` does not sleep.

    "Return the highest-priority job's (id, data)".
    `get_next_job` returns `job_id, job.data`.

    One final check on `process_job`: "must USE RetryPolicy (compose, don't reimplement retry logic)".
    My implementation: `success, attempts = self._retry_policy.run(processor, job.data)`. This is composition.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any, Tuple, Dict, Optional
    import heapq
    from itertools import count

    @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: Any) -> tuple[bool, int]:
            attempts_made = 0
            for _ in range(self.max_attempts):
                attempts_made += 1
                try:
                    fn(data)
                    return True, attempts_made
                except Exception:
                    continue
            return False, attempts_made

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._queue = []
            self._counter = count()
            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[job_id] = job
            heapq.heappush(self._queue, (-priority, next(self._counter), job_id))

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._queue:
                _, _, job_id = heapq.heappop(self._queue)
                job = self._jobs.get(job_id)
                if job:
                    return job_id, job.data
            return None
    ```

    Wait, the `get_next_job` logic: If I pop from the heap, the item is removed from the queue. If `process_job` is called later, the item is already gone from the heap. This means `get_next_job` acts as the "fetch/pop" mechanism. This is the only way to satisfy the requirement that `get_next_job` returns the highest priority job and `process_job` uses the `job_id`.

    Wait, what if the user calls `get_next_job`, gets `(id, data)`, and then *doesn't* call `process_job`? The job is gone from the queue (because it was popped) but stays in `_jobs`. This is fine for this scope.

    Also, if `process_job` is called on a job that was already "popped" by `get_next_job`, it still works because the job remains in `self._jobs` until `process_job` succeeds.

    Actually, one detail: `process_job` takes `job_id`. If `get_next_job` is called and the job is popped, and then `process_job` is called, it works. If `process_job` is called with an ID that hasn't been "popped" yet, it also works.

    Wait, if `process_job` is called with an ID that is *still in the heap*, the job is deleted from `_jobs`. Then, if `get_next_job` is called later, it will pop that ID, see it's missing from `_jobs`, and move to the next one. This is robust.

    Final check on `Job` fields: `id`, `data`, `priority`, `retries`.
    In `add_job`, I am not updating `retries`. That's fine, as the `retries` field in `Job` might be intended for something else or the requirement just asks for the field to exist.

    Wait, one small fix: `process_job` needs to return `bool`. My code does that.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any, Tuple, Dict, Optional
    import heapq
    from itertools import count

    @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: Any) -> tuple[bool, int]:
            attempts_made = 0
            for _ in range(self.max_attempts):
                attempts_made += 1
                try:
                    fn(data)
                    return True, attempts_made
                except Exception:
                    continue
            return False, attempts_made

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._queue = []
            self._counter = count()
            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[job_id] = job
            heapq.heappush(self._queue, (-priority, next(self._counter), job_id))

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._queue:
                _, _, job_id = heapq.heappop(self._queue)
                job = self._jobs.get(job_id)
                if job:
                    return job_id, job.data
            return None
    ```

    One minor consideration: `job_id` might be an integer or string, but the signature says `job_id: str`. The `Job` dataclass says `id: str`. `get_next_job` returns `tuple[str, dict] | None`. Everything matches.

    The `priority` in `heapq` is a min-heap. To make it a max-priority queue (higher number = higher priority), I use `-priority`.
    For same priority, `next(self._counter)` ensures FIFO.

    One last thing: `from typing import ...`
    The user asked for `tuple[bool, int]`. In Python 3.9+, `tuple` is subscriptable. In older versions, it's `Tuple`. Since I'm writing for a modern environment, `tuple[bool, int]` and `dict` in the type hints are fine, but I should be careful. The prompt uses `tuple[bool, int]` and `tuple[str, dict] | None` in the requirements, so I will use exactly that syntax.

    Wait, the requirements say: `get_next_job(self) -> tuple[str, dict] | None`. This is Python 3.10+ syntax (Union type using `|`). I will use that.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Any
import heapq
from itertools import count

@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: Any) -> tuple[bool, int]:
        attempts_made = 0
        for _ in range(self.max_attempts):
            attempts_made += 1
            try:
                fn(data)
                return True, attempts_made
            except Exception:
                continue
        return False, attempts_made

class JobQueue:
    def __init__(self):
        self._jobs: dict[str, Job] = {}
        self._queue = []
        self._counter = count()
        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[job_id] = job
        # Use negative priority for max-heap behavior and counter for FIFO tie-breaking
        heapq.heappush(self._queue, (-priority, next(self._counter), job_id))

    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = self._jobs.get(job_id)
        if not job:
            return False
        
        success, _ = self._retry_policy.run(processor, job.data)
        if success:
            del self._jobs[job_id]
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        while self._queue:
            _, _, job_id = heapq.heappop(self._queue)
            job = self._jobs.get(job_id)
            if job:
                return job_id, job.data
        return None
```
</content>