<reasoning_content>
*   Objective: Refactor a job queue into three composable classes in `solution.py`.
    *   Required Classes:
        1.  `Job` (dataclass): `id`, `data`, `priority`, `retries`.
        2.  `RetryPolicy`: Logic for retry-with-backoff (max attempts, call function, return `(success, attempts_made)`). No sleeping.
        3.  `JobQueue`: Logic for adding and processing jobs. Must use `RetryPolicy`. `get_next_job` returns highest priority (FIFO within priority).
    *   Constraints: Standard library only. No explanations. Pure Python code.

    *   Imports: `dataclass`, `Callable`, `List`, `Tuple`, `Optional`, `heapq`.

    *   `Job` Dataclass:
        ```python
        from dataclasses import dataclass
        @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 = 0
                while attempts < self.max_attempts:
                    try:
                        fn(data)
                        return True, attempts + 1
                    except Exception:
                        attempts += 1
                return False, attempts
        ```

    *   `JobQueue`:
        *   Need a way to store jobs and retrieve the highest priority. A heap is best for priority, but since it's FIFO within priority, we need an insertion counter or similar.
        *   Wait, "highest-priority" usually means highest number or lowest number? Conventionally, priority 10 is higher than 1. Let's look at `heapq`. `heapq` is a min-heap. To make it a max-heap for priority, I can use `-priority`.
        *   To maintain FIFO for same priority: `(priority_value, sequence_number, job_id)`.
        *   Wait, the prompt says "highest-priority job's (id, data)". Usually, higher number = higher priority in systems, but in heaps, lower number = priority. I'll assume higher integer = higher priority and use `-priority` in a min-heap. Or just use a list and sort? No, a heap is better for `get_next_job`.
        *   Wait, the requirement says `get_next_job` returns `(id, data)`.
        *   The requirement says `process_job` must use `RetryPolicy`.

        ```python
        import heapq
        from typing import List, Tuple, Optional

        class JobQueue:
            def __init__(self):
                self.jobs = [] # Heap
                self.counter = 0 # For FIFO
                self.retry_policy = RetryPolicy() # Default policy or should it be injected? Requirement says "orchestrates Jobs using RetryPolicy".

            def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
                # Max-heap behavior: negate priority.
                # Sequence counter ensures FIFO for same priority.
                heapq.heappush(self.jobs, (-priority, self.counter, job_id, data))
                self.counter += 1

            def process_job(self, job_id: str, processor: Callable) -> bool:
                # This is tricky. Does it find a specific job_id or the next one?
                # prompt: process_job(self, job_id: str, processor: Callable) -> bool
                # It takes a specific job_id. I need to find the job with that ID.
                # If not found, what happens?
                # Actually, usually these queues have a way to find by ID.
                # Let's find the job in the internal list.
                for i, (p, c, j_id, data) in enumerate(self.jobs):
                    if j_id == job_id:
                        success, _ = self.retry_policy.run(processor, data)
                        # Should I remove it? The prompt doesn't say.
                        # Usually, processing happens to the next job, but the signature
                        # specifies job_id.
                        return success
                return False

            def get_next_job(self) -> Tuple[str, dict] | None:
                if not self.jobs:
                    return None
                p, c, j_id, data = heapq.heappop(self.jobs)
                return j_id, data
        ```

    *   *Self-Correction on `process_job`*: If `process_job` takes a `job_id`, it implies finding that specific job. But `get_next_job` implies a queue behavior.
    *   *Refining `JobQueue` storage*: If I need to find by ID and also pop by priority, a list + heap isn't enough unless I remove from heap. Removing from middle of heap is $O(N)$.
    *   *Refining `JobQueue` internal structure*: Since the instructions don't specify how to handle removal during `process_job`, and `get_next_job` is a standard queue operation, I'll keep a dictionary of jobs by ID for $O(1)$ lookup and a heap for priority. But then how do I remove from the heap?
    *   Actually, a simple list + `sort` on `get_next_job` or `heapq` is usually expected in these prompts unless performance is specified.
    *   Let's look at the instructions again: "orchestrates Jobs using RetryPolicy".
    *   `process_job(self, job_id: str, processor: Callable) -> bool`.
    *   To implement `process_job` correctly, I need to access the data for that specific ID.
    *   Let's use a dictionary `self.jobs = {}` to store `(priority, counter, data)` and a list `self.heap = []`.
    *   When `get_next_job` is called, pop from heap.
    *   When `process_job` is called, find in dict.

    *Wait*, the prompt doesn't say `process_job` removes the job. If it doesn't remove, then `get_next_job` will eventually return jobs that have been processed.
    Usually, `process_job` implies the consumer takes the job, tries to process it, and then it's done. But the signature asks for a specific `job_id`.

    Let's look at `RetryPolicy.run`: `fn(data)`.
    If `process_job` finds a job, runs it, and then the job is still in the queue, that's weird.
    However, if I use a dictionary to store jobs, `add_job` is easy. `get_next_job` needs a heap.
    Actually, simple is better. A dictionary `jobs: Dict[str, Job]` and a heap `heap: List[Tuple]`.
    When `add_job`: Add to dict and heap.
    When `get_next_job`: Pop from heap. If the popped job is no longer in dict (meaning it was processed via `process_job`?), skip.
    But `process_job` doesn't say it removes it.

    Let's stick to a simpler interpretation:
    1. `jobs` = dict of `id -> Job`.
    2. `heap` = list of `(-priority, counter, id)`.
    3. `add_job`: Add to both.
    4. `process_job`: Get from dict, run policy, return result. (Don't remove).
    5. `get_next_job`: Pop from heap. (If popped ID is not in dict, it was removed? But `process_job` doesn't remove).

    Wait, "orchestrates Jobs". Maybe `process_job` is meant to be called *on* the result of `get_next_job`? But the signature is `process_job(self, job_id: str, ...)`. This means the user provides the ID.

    Let's try to keep it simple:
    - `self.jobs` is a dictionary mapping `id` to `Job`.
    - `self.heap` is a list for `heapq`.
    - `process_job` finds the job in the dictionary, uses the policy, and returns success.
    - `get_next_job` pops from heap and returns `(id, data)`.

    *Wait*, if `get_next_job` pops it, it's gone from the queue. If `process_job` is called on that ID, it won't be in the dictionary anymore?
    Ah, the instructions don't say `process_job` removes the job. But if `get_next_job` removes it, then `process_job` cannot be called on it unless it's still in the dict.

    Revised logic:
    - `self.all_jobs` is a dictionary `{id: Job}`.
    - `self.priority_heap` is a list of `(-priority, counter, id)`.
    - `add_job`: Store in both.
    - `process_job`: Look up in `all_jobs`, run `RetryPolicy.run`, return success.
    - `get_next_job`: Pop from `priority_heap`. Return `(id, data)`.

    One detail: `RetryPolicy.run` needs `fn` and `data`.
    The `Job` dataclass has `retries`. Does `RetryPolicy` need to update `job.retries`?
    The prompt says `RetryPolicy` returns `(success, attempts_made)`. It doesn't say it updates the `Job` object.

    One more check: "highest-priority job's (id, data); FIFO order within the same priority."
    `heapq` is a min-heap. To get highest priority (highest number), use negative priority.
    To get FIFO, use an incrementing counter.

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

    @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: Dict[str, Job] = {}
            self.heap: List[Tuple[int, int, str]] = []
            self.counter = 0
            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.heap, (-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.retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] | None:
            # Actually, if process_job doesn't remove the job, 
            # get_next_job will eventually return a job that was already "processed".
            # However, if get_next_job pops it, it is removed from the queue.
            # This is the standard behavior for a queue.
            while self.heap:
                p, c, j_id = heapq.heappop(self.heap)
                if j_id in self.jobs:
                    job = self.jobs[j_id]
                    # If I want to "remove" it when popped for get_next_job:
                    # del self.jobs[j_id] 
                    # But process_job needs it to exist.
                    # The prompt doesn't specify if get_next_job removes it.
                    # In most job queues, get_next_job (or pop) removes it.
                    # But process_job takes an ID. 
                    # Let's assume get_next_job removes it from the "pending" set.
                    # But then process_job would fail.
                    # Let's assume they are independent operations.
                    # If I call get_next_job, I get the highest priority.
                    # If I call process_job, I process a specific ID.
                    return j_id, job.data
            return None
    ```

    Re-reading: "orchestrates Jobs".
    Usually, `process_job` would be called *after* `get_next_job`.
    If `get_next_job` pops it, the `JobQueue` object no longer "has" it in the queue.
    If I don't delete it from `self.jobs`, `process_job` still works.
    This seems the safest way to satisfy both signatures.

    Wait, the `Job` class has a `retries` field. The `RetryPolicy` doesn't use it (it has its own `max_attempts`). Should I update `job.retries`?
    The prompt says `RetryPolicy` returns `(success, attempts_made)`. It doesn't say to update the `Job` object. I'll leave it alone to stay strictly to the instructions.

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

    Implementation details:
    - `Job` fields: `id`, `data`, `priority`, `retries`.
    - `RetryPolicy` fields: `max_attempts`.
    - `JobQueue` fields: `jobs`, `heap`.

    One small detail: `heapq.heappush(self.heap, (-priority, self.counter, job_id))`.
    The `counter` ensures that if priorities are equal, the one added first (lower counter) comes out first. This satisfies "FIFO order within the same priority".

    Wait, `process_job` should probably remove the job from the `jobs` dict? No, instructions don't say. I'll leave it.

    Actually, let's refine `get_next_job` slightly. If a job was removed by some other logic, the heap might have a stale ID.
    But `add_job` is the only thing adding to `self.jobs`.
    Is there a way to remove from `self.jobs`? Not mentioned.
    So `get_next_job` popping and returning is correct.

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

    @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[[dict]], 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.heap: List[Tuple[int, int, str]] = []
            self.counter = 0
            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.heap, (-priority, self.counter, job_id))
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable[[dict]]) -> bool:
            job = self.jobs.get(job_id)
            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:
            while self.heap:
                _, _, j_id = heapq.heappop(self.heap)
                if j_id in self.jobs:
                    job = self.jobs[j_id]
                    return j_id, job.data
            return None
    ```

    Wait, `subprocess` is not allowed. `Callable` is from `typing`.
    `tuple` and `dict` are built-in.
    The prompt asks for `tuple[bool, int]` syntax which implies Python 3.9+ style.

    One small adjustment: `self.retry_policy = RetryPolicy()` inside `JobQueue.__init__`.
    The prompt says "orchestrates Jobs using RetryPolicy".
    It doesn't say `RetryPolicy` must be passed in, but it's good practice. However, the prompt says `RetryPolicy` encapsulates the logic.
    I'll just instantiate it inside `JobQueue`.

    Refining `get_next_job` logic:
    If `get_next_job` is called, it removes the highest priority job from the queue.
    If `process_job` is then called on that same ID, should it work?
    If `get_next_job` removes the job from `self.jobs`, then `process_job` will return `False`.
    Usually, `get_next_job` implies the job is "taken" from the queue.
    But `process_job` is a specific instruction.
    Most interview/coding tasks like this assume `get_next_job` removes it from the "available" list.
    However, if I don't remove it from `self.jobs`, `process_job` can always be called.
    Let's look at the prompt again. It doesn't say "remove".
    Actually, if `get_next_job` is meant to be the way to retrieve a job to be processed, then it *should* remove it from the queue's internal storage so it's not returned again.

    Let's look at `JobQueue.process_job(self, job_id: str, ...)`.
    If I call `get_next_job`, I get the ID. Then I call `process_job(id)`.
    If `get_next_job` removes it from `self.jobs`, `process_job` fails.
    Therefore, `get_next_job` should only remove it from the *heap* (the priority structure), and `process_job` should still be able to find it in a registry.

    Wait, if `get_next_job` removes it from the heap, but not the registry, then the registry grows infinitely.
    If `process_job` is the intended way to complete a job, maybe it should remove it?
    No, the prompt is simple. I will keep the registry (`self.jobs`) as the source of truth for `process_job` and the `heap` as the source of truth for `get_next_job`.
    To prevent `get_next_job` from returning the same job twice, `get_next_job` *must* pop from the heap.

    Wait, if `get_next_job` pops from the heap, it's gone from the heap. It won't be returned again. This is correct.
    The registry `self.jobs` doesn't *need* to be deleted unless we want to free memory.

    Final check: `JobQueue` must "orchestrate". `RetryPolicy` must be used.

    Code structure:
    `solution.py`
    - `Job`
    - `RetryPolicy`
    - `JobQueue`

    One detail: "do NOT actually sleep". `RetryPolicy` must not have `time.sleep`. Correct.
</reasoning_content>

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

@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[[dict]], 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.heap: List[Tuple[int, int, str]] = []
        self.counter = 0
        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 (higher number = higher priority)
        # Use counter to ensure FIFO order for equal priorities
        heapq.heappush(self.heap, (-priority, self.counter, job_id))
        self.counter += 1

    def process_job(self, job_id: str, processor: Callable[[dict]]) -> bool:
        job = self.jobs.get(job_id)
        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:
        while self.heap:
            _, _, job_id = heapq.heappop(self.heap)
            if job_id in self.jobs:
                job = self.jobs[job_id]
                return job_id, job.data
        return None
```
</content>