<reasoning_content>
*   Target file: `solution.py`.
    *   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Constraints:
        *   `Job`: Dataclass with `id`, `data`, `priority`, `retries`.
        *   `RetryPolicy`: Handles retries with backoff logic (simulated, no actual sleep). `run` returns `(success, attempts_made)`.
        *   `JobQueue`: Manages jobs, uses `RetryPolicy` for processing. `get_next_job` returns highest priority (FIFO for ties).
        *   Library: Standard library only.
        *   Output: Python code only.

    *   *`Job` Class*:
        ```python
        from dataclasses import dataclass
        from typing import Callable, Tuple, Optional

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

    *   *`RetryPolicy` Class*:
        *   `__init__(self, max_attempts: int = 4)`
        *   `run(self, fn: Callable, data: dict) -> Tuple[bool, int]`
        *   Requirements: Retries on exception until success or limit. No sleep.
        ```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` Class*:
        *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
        *   `process_job(self, job_id: str, processor: Callable) -> bool` (Must use `RetryPolicy`).
        *   `get_next_job(self) -> Tuple[str, dict] | None` (Highest priority, FIFO).
        *   *Implementation detail for `get_next_job`*: Need a way to store jobs that respects priority and insertion order. A list sorted by priority (descending) or a `heapq` is good. Since FIFO is required within same priority, a simple list + sort or a tuple `(-priority, insertion_order)` in a heap works.
        ```python
        import heapq

        class JobQueue:
            def __init__(self):
                self.jobs = [] # List of tuples (priority, order, job_id, data)
                self.job_map = {} # job_id -> Job object
                self.counter = 0 # for FIFO
                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.job_map[job_id] = job
                # Heapq is min-heap. Use negative priority for max-heap behavior.
                heapq.heappush(self.jobs, (-priority, self.counter, job_id))
                self.counter += 1

            def process_job(self, job_id: str, processor: Callable) -> bool:
                job = self.job_map.get(job_id)
                if not job:
                    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
                _, _, job_id = heapq.heappop(self.jobs)
                job = self.job_map[job_id]
                return (job.id, job.data)
        ```

    *   Wait, the `Job` dataclass has a `retries` field. Does `RetryPolicy` update it? The prompt says "orchestrates Jobs using RetryPolicy", and "process_job ... must USE RetryPolicy". It doesn't explicitly say update the `retries` count in the object, but it's usually expected. However, `RetryPolicy.run` returns `(success, attempts_made)`. I'll stick to the requested method signatures exactly.

    *   Re-checking `JobQueue.get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   `heapq` is perfect here. `(-priority, insertion_index, job_id)`.

    *   Re-checking `RetryPolicy.run`: "calls `fn(data)`, retries on any exception until success or `max_attempts` reached".
        *   Need to ensure I catch `Exception` specifically (or all exceptions).

    *   Refining `JobQueue`:
        The instructions say `add_job` takes `job_id`, `data`, `priority`.
        The `Job` dataclass has `retries`.

    *   Dependency check: `typing`, `dataclasses`, `heapq`, `Callable`.

    *   Let's look at `JobQueue.process_job`. It receives `job_id`.
        If the job is popped by `get_next_job`, does it stay in the queue? Usually, `process_job` implies it's being worked on. But `get_next_job` removes it from the heap.
        Wait, usually `process_job` is called on a job fetched from the queue.
        Let's look at the method signatures again:
        `process_job(self, job_id: str, processor: Callable) -> bool`
        This implies the ID is provided. I should check if it exists in the map.

    *   Wait, `Job` has `retries`. Should `process_job` update it?
        The prompt doesn't say to update it. It just says `RetryPolicy` should return `(success, attempts_made)`. I'll calculate success and return it.

    *   One more look at `get_next_job`: "return the highest-priority job's (id, data)".
        If I use a heap, `heappop` removes it. If `process_job` is called later, the job still exists in `self.job_map`.

    *   Final structure check:
        1. `Job` (dataclass)
        2. `RetryPolicy` (methods match)
        3. `JobQueue` (methods match)

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

    ```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_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[Tuple[int, int, str]] = []
            self.job_map: Dict[str, Job] = {}
            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.job_map[job_id] = job
            # Use negative priority for max-heap behavior, counter for FIFO
            heapq.heappush(self.jobs, (-priority, self.counter, job_id))
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self.job_map.get(job_id)
            if not job:
                return False
            
            success, attempts_made = self.retry_policy.run(processor, job.data)
            # Update the job's retry count based on attempts made? 
            # Instructions don't explicitly say to update, but usually 
            # retries = total attempts - 1 (if failed). 
            # Since instructions don't specify, I will keep it simple.
            return success

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

    ```

    Wait, the prompt says "Refactor the job queue into three composable classes".
    The `JobQueue` signature for `process_job` takes `job_id`.
    If `get_next_job` pops the item, and `process_job` takes the `job_id`, the code works.

    One minor thing: `tuple[bool, int]` vs `Tuple[bool, int]`. Python 3.9+ supports lower case. I'll use the standard `Tuple` from `typing` to be safe or stick to the prompt's style. The prompt uses `tuple[bool, int]`. I will use the lowercase version to match the prompt's syntax exactly.

    Wait, the prompt says: `run(self, fn: Callable, data) -> tuple[bool, int]`.
    There is a missing type hint for `data` in the prompt's signature (`data` vs `data: dict`). I will treat it as `data: dict`.

    Final check on `RetryPolicy`: "retries on any exception until success or `max_attempts` reached".
    If `max_attempts` is 4:
    Attempt 1: Exception -> Loop
    Attempt 2: Exception -> Loop
    Attempt 3: Exception -> Loop
    Attempt 4: Exception -> Loop ends -> Return (False, 4).
    This matches the logic.

    Wait, `JobQueue.process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I don't remove the job from `job_map`, `process_job` will work even if `get_next_job` has been called. This seems correct for a "Job Queue" where items are retrieved and then processed.

    One last check on FIFO: `heapq` with `(-priority, counter, job_id)`.
    Higher priority (higher number) -> smaller negative number -> pops first.
    Same priority -> smaller counter (earlier insertion) -> pops first.
    Perfect.

    One small discrepancy: `Job` dataclass has `retries`. Should `process_job` update `job.retries`?
    The instructions: `RetryPolicy` returns `(success, attempts_made)`.
    If I want to be helpful, I'd update it, but the prompt doesn't ask. I'll leave it alone unless it's obvious. Actually, looking at the `Job` dataclass having `retries` usually implies it should be updated.
    However, `RetryPolicy.run` returns the attempts. I'll stick to the logic provided.

    One detail: `RetryPolicy.run` takes `data`. The prompt signature is `run(self, fn: Callable, data)`. It's missing the type hint for `data`. I'll add `data: dict` to be consistent with the others.

    Code structure:
    ```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_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[tuple[int, int, str]] = []
            self.job_map: dict[str, Job] = {}
            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.job_map[job_id] = job
            heapq.heappush(self.jobs, (-priority, self.counter, job_id))
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self.job_map.get(job_id)
            if not job:
                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
            _, _, job_id = heapq.heappop(self.jobs)
            job = self.job_map[job_id]
            return (job.id, job.data)
    ```

    Wait, `tuple[str, dict] | None` is Python 3.10+. `Optional` is safer but I'll use the pipe if it's standard enough. Actually, I'll use `Optional` to be safe or just stay strictly with the prompt's notation. Prompt uses `tuple[str, dict] | None`. I will use that.

    Actually, looking at the prompt again:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    I will not put a type hint on `data` to match the prompt's literal request, even if it looks slightly incomplete. Or I'll use `data: dict` to be safe. I'll use `data: dict`.

    Wait, one more detail: `JobQueue.process_job` returns `bool`.
    If the job doesn't exist, should it return `False`? Probably.

    Final Code Check.
</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, 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[Tuple[int, int, str]] = []
        self.job_map: Dict[str, Job] = {}
        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.job_map[job_id] = job
        # Use negative priority for max-heap behavior, counter for FIFO
        heapq.heappush(self.jobs, (-priority, self.counter, job_id))
        self.counter += 1

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