<reasoning_content>
*   Goal: Refactor a job queue into three specific composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Environment: `solution.py`.
    *   Constraints: Use standard library only. No external packages. No explanations.

    *   `Job`:
        *   Dataclass.
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    *   `RetryPolicy`:
        *   `__init__(self, max_attempts: int = 4)`.
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`.
        *   Logic: Call `fn(data)`, retry on exception until success or `max_attempts`.
        *   Note: No actual sleep.
    *   `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`.
        *   Logic: `process_job` must use `RetryPolicy`. `get_next_job` returns highest priority (FIFO within same priority).

    *   `Job`: Standard dataclass.
    *   `RetryPolicy`:
        ```python
        def run(self, fn, data):
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts + 1
                except:
                    attempts += 1
            return False, attempts
        ```
    *   `JobQueue`:
        *   Storage: Needs to handle priority (highest first) and FIFO. A list sorted by priority (desc) then insertion order, or a `heapq`. Since `heapq` is min-priority, we'd use negative priority. However, `heapq` doesn't guarantee FIFO for same priority unless we add an entry count.
        *   Let's use a simple list and sort/pop, or a `heapq`. Given "FIFO within same priority", a `heapq` with `(-priority, insertion_index, job_id, data)` is standard.
        *   Wait, `add_job` takes `job_id`, `data`, `priority`.
        *   `process_job` takes `job_id` and a `processor`. This implies looking up a job by ID.
        *   If `process_job` finds the job, runs it through `RetryPolicy`, and `get_next_job` retrieves the next one, I need a way to store jobs accessible by both ID and priority.

    *   *Revised `JobQueue` State*:
        *   A dictionary `self.jobs = {}` (id -> Job).
        *   A list or heap for priority ordering. Let's use `heapq` with a counter for FIFO.

    *. Actually, let's re-read*:
    *   `add_job(job_id, data, priority)` -> adds to internal storage.
    *   `process_job(job_id, processor)` -> finds job by ID, runs it.
    *   `get_next_job()` -> finds highest priority job.

    Wait, if I call `process_job(id)`, does it stay in the queue? Usually, processing removes it. But `get_next_job` also suggests looking for the next one.
    If `process_job` is called by ID, it likely targets a specific job. If `get_next_job` is called, it gets the "best" one.

    *Refinement on `JobQueue` implementation*:
    To handle "FIFO within same priority" correctly with `heapq`:
    Use a counter `self._count = 0`.
    Heap elements: `(-priority, self._count, job_id)`.
    Store actual `Job` objects in a dict: `self._jobs = {}`.

    *Implementation Details*:
    - `RetryPolicy.run`:
      ```python
      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.process_job`:
      ```python
      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)
          return success
      ```

    Wait, the prompt doesn't say `JobQueue` should remove the job after `process_job`. But usually, it does. However, I'll stick to the requirements exactly.
    Requirement for `get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."

    If I use a Heap, I need to make sure `get_next_job` behaves correctly.
    If I use a list and sort it every time, it's easier to manage but slower. But for a coding challenge, either is usually fine.

    Let's look at `process_job` again. "must USE RetryPolicy".

    ```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 = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    import heapq

    class JobQueue:
        def __init__(self):
            self.jobs = {} # id -> Job
            self.heap = [] # list of (-priority, counter, job_id)
            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) -> tuple[str, dict] | None:
            if not self.heap:
                return None
            # Note: Requirements don't say to remove it, but "get_next" 
            # usually implies popping. However, if I don't pop, 
            # it returns the same one every time.
            # In most job queues, get_next removes it. 
            # But let's see the instruction: "return the highest-priority job's (id, data)"
            # Let's assume we pop it to keep it "next". 
            # If I don't pop, I'll just peek. 
            # Standard queue behavior is pop.
            # "FIFO order within the same priority" is handled by the counter in the heap.
            
            priority_neg, _, job_id = heapq.heappop(self.heap)
            job = self.jobs[job_id]
            return (job.id, job.data)
    ```

    One detail: `process_job` doesn't say to remove it. `get_next_job` implies a queue operation (removal). If `process_job` is called by ID, it stays in the queue? That's weird for a job queue, but I should follow the method signatures.
    If I don't remove it in `get_next_job`, it's not really a "queue". I'll pop it.

    Actually, `get_next_job` signature is `tuple[str, dict] | None`.
    If I pop it, then `process_job` might be called on a job that was already "gotten".
    Most likely, `add_job` puts it in. `get_next_job` retrieves it (removes it). `process_job` is a way to run a specific one.

    Wait, if `get_next_job` removes it, then `process_job` (which takes an ID) might fail if it was already popped.
    Is there a conflict?
    Requirement 3: `process_job` must use `RetryPolicy`.
    Requirement 3: `get_next_job` returns the highest priority.

    Let's look at the `Job` object. It has a `retries` field. Should I increment `retries` in `RetryPolicy`? The prompt says `RetryPolicy` returns `(success, attempts_made)`. It doesn't say to update the `Job` object.

    Actually, looking at `Job` fields: `retries: int = 0`. This suggests that the `retries` count might be intended to be tracked *on the job object*.
    But `RetryPolicy`'s `run` method signature is `fn(data)`. It doesn't take the `Job` object.
    I will stick to the prompt exactly.

    One final check on `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 I use `heapq.heappop`, it's a queue.
    If I use a dictionary for `process_job`, I need to make sure `process_job` works even if the job was "gotten" but not "processed".
    Wait, usually in these patterns:
    1. `get_next_job` is for the worker loop.
    2. `process_job` is for manual triggering.

    I'll use a dictionary to store all jobs currently in the queue. When `get_next_job` is called, I pop from heap and remove from dict? No, if I remove from dict, `process_job` by ID fails.
    If I *don't* remove from dict, but I *do* pop from heap? Then `get_next_job` will always return the same item if I don't pop, or different items if I do.
    Standard queue: `get_next` pops.
    I'll pop from heap and leave it in the dict? No, then it's not a queue.
    I'll pop from heap and remove from dict. If `process_job` is called on an ID that isn't in the dict, it returns `False` (or nothing).

    Wait, I'll just use a list and sort it. It's safer for small logic.
    Actually, `heapq` is best for priority.
    To satisfy both `process_job(id)` and `get_next_job()`:
    If `get_next_job` is called, the item is removed from the queue.
    If `process_job` is called, it processes the item.

    Let's refine:
    - `self.jobs`: dict `id -> Job`.
    - `self.heap`: list of `(-priority, counter, id)`.
    - `add_job`: adds to both.
    - `process_job`: looks up in `self.jobs`. Does NOT remove.
    - `get_next_job`: pops from `self.heap` AND removes from `self.jobs`.

    This seems the most logical interpretation of "Queue" and "process by ID".

    *RetryPolicy logic check*:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    `retries on any exception until success or max_attempts reached`
    `Do NOT actually sleep.`

    ```python
    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
    ```

    *Wait*: The `Job` dataclass has `retries: int = 0`.
    Should I update `job.retries`? The prompt says `RetryPolicy` returns the count. It doesn't say to update the object. I'll stick to the return value.

    *Final check on types*:
    `tuple[bool, int]` -> Python 3.9+ syntax.
    `tuple[str, dict] | None` -> Python 3.10+ syntax.
    I should ensure the code is valid Python 3.10+.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, List, Tuple, Optional
    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 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.heap:
                return None
            neg_priority, _, job_id = heapq.heappop(self.heap)
            job = self.jobs.pop(job_id)
            return (job.id, job.data)
    ```

    Wait, if I `pop` from `self.jobs` in `get_next_job`, then `process_job` will return `False` for that ID. This is standard for a queue.

    One minor thing: `max_attempts` = 4. If it fails 4 times, `attempts` is 4.
    If it succeeds on the 1st try, `attempts` is 1.
    If it succeeds on the 4th try, `attempts` is 4.
    If it fails 4 times, the loop ends, `attempts` is 4. `run` returns `(False, 4)`.
    This matches "until success or max_attempts reached".

    Wait, `retries` field in `Job`?
    If the user expects `job.retries` to be updated, they might be annoyed. But the instruction says `RetryPolicy` returns the count. It doesn't say to update the `Job` instance. However, a common pattern is to update the object.
    Since the instructions are very specific about what `RetryPolicy.run` does (returns a tuple), I will not modify the `Job` object inside `RetryPolicy`.

    Let's double check the `RetryPolicy` return type. `tuple[bool, int]`. Yes.

    Actually, `get_next_job` return type: `tuple[str, dict] | None`.
    In Python < 3.10, `|` isn't supported. But usually these prompts assume modern Python. I'll use `Optional` or just `|` if I'm sure. The prompt uses `|` in the description: `tuple[str, dict] | None`. I will use that.

    One detail: `JobQueue.add_job` arguments: `job_id: str`, `data: dict`, `priority: int = 0`.
    My code: `job = Job(id=job_id, data=data, priority=priority)`. Correct.

    Wait, if `process_job` is called, and it succeeds, should it be removed from the queue?
    The prompt doesn't say. Usually, `process_job` is a manual action. If it's manual, it probably shouldn't be removed automatically unless specified. But if `get_next_job` is called, it is removed.

    Final code structure check:
    - `Job` dataclass.
    - `RetryPolicy` class.
    - `JobQueue` class.
    - Standard library only.
    - No explanations.

    ```python
    from dataclasses import dataclass
    from typing import Callable
    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 = {}
            self.heap = []
            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 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.heap:
                return None
            _, _, job_id = heapq.heappop(self.heap)
            job = self.jobs.pop(job_id)
            return (job.id, job.data)
    ```
    Small syntax error in `heappush` (extra bracket). Fixed.

    Wait, `Job` dataclass fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    My code: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`. Correct.

    One minor thing: `RetryPolicy` uses `Callable`. I need to import it.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable
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
c        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 = {}
        self.heap = []
        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 and counter for FIFO
        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 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.heap:
            return None
        _, _, job_id = heapq.heappop(self.heap)
        job = self.jobs.pop(job_id)
        return (job.id, job.data)
```
</content>