<reasoning_content>
*   Goal: Refactor a job queue into three composable classes in `solution.py`.
    *   Classes required:
        1.  `Job`: `dataclass` with `id`, `data`, `priority`, `retries`.
        2.  `RetryPolicy`: `max_attempts` logic, `run` method calls `fn` and returns `(success, attempts_made)`. No actual sleeping.
        3.  `JobQueue`: `add_job`, `process_job` (using `RetryPolicy`), `get_next_job` (priority-based, FIFO within same priority).
    *   Constraints: Standard library only, no external packages, no explanations, return only Python code.

    *   `from dataclasses import dataclass, field`
    *   `from typing import Callable, Any`
    *   `import heapq` (for priority queue)

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

    *   `__init__(self, max_attempts: int = 4)`
    *   `run(self, fn: Callable, data) -> tuple[bool, int]`
        *   Loop up to `max_attempts`.
        *   Try `fn(data)`.
        *   If successful, return `(True, attempts_made)`.
        *   If exception, increment attempt.
        *   If all attempts fail, return `(False, attempts_made)`.

    *   Internal state: Needs a priority queue.
    *   To handle FIFO within same priority:
        *   In `heapq`, a tuple `(priority, counter, job)` can be used.
        *   Wait, the prompt says `priority: int = 0`. Usually, higher priority means smaller number in heaps (min-heap) or larger number?
        *   The prompt says: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   Standard `heapq` is a min-heap. If "highest priority" means higher number, I need to negate it or use a different approach. However, in many systems, "priority 0" is lowest or highest depending on convention. Let's assume "highest-priority" implies a sorting order. Usually, in task queues, a lower number often means higher priority (like Unix niceness) OR a higher number means higher priority.
        *   Let's check the requirement: `priority: int = 0`.
        *   If priority 10 is "higher" than priority 1, I should use `-priority` in the heap.
        *   To ensure FIFO within the same priority, I'll use a sequence counter.
    *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`:
        *   Create `Job` object.
        *   Push `(-priority, counter, job)` to the heap.
    *   `process_job(self, job_id: str, processor: Callable) -> bool`:
        *   This is a bit tricky. The user asks to process a *specific* `job_id`. But `get_next_job` returns the next one from the queue.
        *   How does `process_job` find the job? The prompt says `process_job(self, job_id: str, processor: Callable)`. This implies the `JobQueue` holds a registry of jobs or the queue itself.
        *   Wait, if it's a queue, you usually pop from the queue. If `process_job` takes a `job_id`, it implies looking up a job.
        *   Let's look at the `JobQueue` requirements again:
            - `add_job`: add to queue.
            - `process_job`: must USE `RetryPolicy`.
            - `get_next_job`: return next job.
        *   If `process_job` takes a `job_id`, it probably needs to find the job in its storage.
        *   However, if `get_next_job` is used to pull from the queue, does `process_job` work on a job *already* removed? Or is the queue a collection of jobs?
        *   Usually, a queue's `process` method pops the job. But the signature provided is `process_job(self, job_id: str, processor: Callable)`.
        *   If I use `get_next_job` to get the job, I don't have the `job_id` passed in from outside to match the signature `process_job(self, job_id, ...)`.
        *   Let's re-read: `process_job(self, job_id: str, processor: Callable) -> bool`.
        *   If the user provides `job_id`, the queue must know about that job. This suggests the `JobQueue` stores jobs in a way that they can be looked up by ID, *and* ordered for the priority queue.
        *   Common pattern: A dictionary for lookup `self.jobs: dict[str, Job]` and a heap for order `self.heap: list`.
        *   But `process_job` returns `bool`. This likely means "Did it succeed?".
        *   Wait, if `process_job` takes a `job_id`, it doesn't necessarily mean it's "popping" it from the queue. But usually, processing a job implies it's being handled.
        *   Let's look at `get_next_job`: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   If `get_next_job` is used, it returns the next job.
        *   If `process_job` is used, it processes a specific ID.
        *   This is slightly contradictory if it's a strict "queue". A queue is FIFO. If I process a specific ID, it's a "job registry" or "task manager".
        *   Let's assume:
            1. `add_job` adds to both a dictionary (for lookup) and a heap (for priority).
            2. `get_next_job` pops from the heap and removes from the dictionary.
            3. `process_job` looks up the job by ID in the dictionary, uses `RetryPolicy` to run it, and if successful, potentially removes it? Or maybe `process_job` is the caller's way of saying "Hey, I have this ID, run it".
        *   Actually, looking at common coding test patterns: `process_job` likely wants to find the job in the queue, run the retry logic, and if it succeeds, return True.
        *   Wait, if `get_next_job` is the way to get the "next" job, then `process_job` is an alternative way to process a specific one.
        *   Let's implement:
            - `self._jobs: dict[str, Job]`
            - `self._heap: list` (containing `(-priority, counter, job_id)`)
            - `self._counter` for FIFO.
            - `self._retry_policy = RetryPolicy()`
            - `add_job`: create `Job`, add to `_jobs` and `_heap`.
            - `get_next_job`: pop from `_heap`, get ID from `_jobs`, remove from `_jobs`, return `(id, data)`.
            - `process_job`: find `job` in `_jobs`. If found, use `_retry_policy.run(processor, job.data)`. Return success.

    *   Wait, if `process_job` is supposed to use the `RetryPolicy` to call `processor(data)`, and `RetryPolicy.run` returns `(success, attempts)`. `process_job` returns `bool`.

    *   Refined Plan for `JobQueue`:
        - `self.jobs: dict[str, Job]`
        - `self.heap: list` (to store `(-priority, sequence_id, job_id)`)
        - `self.sequence: int`
        - `self.retry_policy: RetryPolicy`
        - `add_job`: Create `Job`. Put in `jobs`. Push to `heap`.
        - `get_next_job`: While `heap`: pop `(-p, seq, jid)`. Check if `jid` in `jobs`. If yes, pop `jobs[jid]`, delete from `jobs`, return `(jid, data)`. If no (already processed), continue.
        - `process_job`: Get `job` from `jobs`. If not exists, return `False`. Run `retry_policy.run(processor, job.data)`. If success, `del jobs[job_id]`, return `True`. Else return `False`.

    *   Wait, the `process_job` implementation might be simpler: `process_job` doesn't necessarily mean it *removes* it from the queue unless it's successful? Or maybe it just executes it. Let's assume if successful, the job is considered done and removed.

    *   Let's re-read `get_next_job`: "return the highest-priority job's (id, data)". It doesn't say "and remove it". However, a "queue"'s `get` method usually removes the item. If it doesn't remove, it's a `peek`. But `get_next_job` usually implies consumption.

    *   Let's look at `process_job` again. If `process_job` is meant to be the primary way to work, and `get_next_job` is to find what's next, I'll implement `get_next_job` to remove the job.

    *   `Job` dataclass.
    *   `RetryPolicy`: `run` method loops `max_attempts`.
    *   `JobQueue`:
        - `heapq` for priority.
        - `dict` for job storage.
        - `add_job`
        - `get_next_job` (removes and returns)
        - `process_job` (uses `RetryPolicy`, returns bool)

    Wait, `process_job(self, job_id: str, processor: Callable) -> bool`. If I call `process_job`, should it remove the job if it succeeds? Standard queue behavior says yes. If I call `get_next_job`, it returns the data.

    One edge case: `get_next_job` returns `(id, data)`. If I get the job via `get_next_job`, I have the ID and data. Then I can pass them to `process_job`? But `process_job` takes `job_id`.

    Let's look at the dependency: `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) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    pass
            return False, attempts
    ```

    `JobQueue`:
    ```python
    class JobQueue:
        def __init__(self):
            self._jobs = {} # id -> Job
            self._heap = [] # [(-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
            self._counter += 1
            # Using -priority for max-heap behavior with heapq (min-heap)
            # Use counter for FIFO
            heapq.heappush(self._heap, (-priority, self._counter, job_id))

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

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

    Wait, if `get_next_job` removes the job from `_jobs`, then `process_job` won't find it. This makes sense: either you get it to look at it, or you process it. But usually, in a workflow:
    1. `job = get_next_job()`
    2. `process_job(job.id, processor)`
    But in my implementation, `get_next_job` removes it. So `process_job` would fail because the job is gone.

    Let's rethink.
    If `get_next_job` is used to *peek* at the next job without removing it?
    Or if `process_job` is the way to *consume* a job, and `get_next_job` is just a way to find the next available ID/data?
    If `get_next_job` doesn't remove, it's a peek. But the prompt says "return the highest-priority job's (id, data)".

    Let's look at the signature of `process_job`: `process_job(self, job_id: str, processor: Callable)`. It takes a `job_id`. This implies the job is already known to the queue.
    If I `add_job`, the job is in the queue.
    If I `get_next_job`, I get the ID.
    If I then call `process_job(id, proc)`, the job must still be in the queue.

    So, `get_next_job` should NOT remove the job from the `_jobs` dictionary, but it should perhaps be used to identify which job is next. But if `get_next_job` doesn't remove it, the same job will always be "next". That's not a queue; that's a peek.

    Wait, if `process_job` is the consumer, it should remove the job.
    If `get_next_job` is the "next" function, it's likely used to see what's coming.

    Alternative:
    `get_next_job` pops from heap and removes from `_jobs`.
    `process_job` takes a `job_id`. But if the job was already popped by `get_next_job`, `process_job` can't find it.

    Maybe `process_job` is NOT the consumer. Maybe `process_job` is a way to process a job *by ID* regardless of its position in the queue.
    But if `process_job` is called, and it succeeds, we should probably remove it from the queue so `get_next_job` doesn't return it later.

    Let's try this:
    - `add_job`: add to `_jobs` and `_heap`.
    - `get_next_job`: pop from `_heap`. If `job_id` is in `_jobs`, return it. If not (already processed by `process_job`), pop again. **Crucially: do not remove from `_jobs`**.
    - `process_job`: find in `_jobs`. Use `RetryPolicy`. If success, `del self._jobs[job_id]`.

    Let's trace:
    1. `add_job("1", {"a": 1}, priority=1)` -> `_jobs={"1": Job1}`, `_heap=[(-1, 1, "1")]`.
    2. `get_next_job()` -> returns `("1", {"a": 1})`. `_jobs` still has `"1"`.
    3. `process_job("1", proc)` -> success -> `del _jobs["1"]`.
    4. `get_next_job()` -> pops `(-1, 1, "1")`. `"1"` not in `_jobs`. Pops again. Returns `None`.
    This works! `get_next_job` acts like a peek/generator, and `process_job` is the consumer.

    One detail: `get_next_job` says "return the highest-priority job's (id, data)". If multiple calls to `get_next_job` are made before `process_job`, it will return the same job every time. That's fine for a `peek` operation.

    However, the requirement says "return the highest-priority job's (id, data)". In a standard queue, "get" removes. But in this specific structure, if `process_job` is the one doing the work and taking an ID, the logic above is the most robust.

    Wait, one more check: `process_job` returns `bool`. If `RetryPolicy` fails, it returns `False`. `process_job` should return `False`. If it succeeds, return `True`.

    What about `retries`? The `Job` class has a `retries` field. The `RetryPolicy` handles the attempts. Does the `Job` object's `retries` field need to be updated?
    The prompt: `Job` — dataclass with fields `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    The `RetryPolicy` is a separate class. It doesn't say the `Job`'s `retries` must be updated, but it's good practice. However, `RetryPolicy.run` doesn't have access to the `Job` object, only the `fn` and `data`. So the `Job` object's `retries` field won't be updated unless I pass the job object instead of just data, but the signature says `run(self, fn: Callable, data)`.
    Actually, I can't update `job.retries` inside `RetryPolicy.run` if I only pass `data`.
    I'll stick to the instructions: `run(self, fn, data)` -> `fn(data)`.

    Final structure:
    - `Job`: `@dataclass`
    - `RetryPolicy`: `run` method with try-except.
    - `JobQueue`:
        - `_jobs` dict
        - `_heap` list
        - `_counter` for FIFO
        - `_retry_policy` instance
        - `add_job`
        - `process_job`
        - `get_next_job`

    *Self-correction on `get_next_job`*: If I don't remove from `_jobs`, then `get_next_job` is a "peek". If the user calls `get_next_job`, they expect to get the next thing to do. If they call it again, they get the same thing. That's okay.

    `priority: int = 0`.
    If priority 10 is "higher" than priority 1.
    `heapq` is a min-heap.
    To get 10 first, I push `-10`.
    Example: `priority = [1, 5, 10]`
    `heapq` stores `[-1, -5, -10]`.
    `heappop` returns `-10`. Correct.

    *Wait, FIFO order*:
    If two jobs have same priority (e.g., 10), they have the same `-priority`.
    `heapq` then compares the next element in the tuple, which is `self._counter`.
    Job 1: `(-10, 1, "id1")`
    Job 2: `(-10, 2, "id2")`
    `heappop` returns `(-10, 1, "id1")`.
    This is FIFO. Correct.

    - `Job` dataclass: Yes.
    - `RetryPolicy`: `max_attempts`, `run(fn, data)`, returns `(bool, int)`, no sleep. Yes.
    - `JobQueue`: `add_job`, `process_job` (uses `RetryPolicy`), `get_next_job` (priority, FIFO). Yes.
    - Standard library only: Yes.
    - No explanations: Yes.

    *Refining `process_job` return value*:
    "returns `(success, attempts_made)`" for `RetryPolicy.run`.
    "`process_job` ... returns `bool`".
    So `success, _ = self._retry_policy.run(...)` and then `return success`.

    *Refining `get_next_job` return type*:
    `tuple[str, dict] | None`.

    *One detail*: `process_job` must use `RetryPolicy`.
    If `process_job` is called for a `job_id` that isn't in the queue, it should return `False`.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    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: 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._heap: list[tuple[int, int, str]] = []
            self._counter: int = 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
            self._counter += 1
            heapq.heappush(self._heap, (-priority, self._counter, job_id))

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

        def get_next_job(self) -> 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
    ```

    *Wait*, one thing: In `get_next_job`, I am popping from the heap. If I don't remove the job from `self._jobs`, then the next call to `get_next_job` will pop the same ID, see it's in `_jobs`, and return it. That's fine.
    But what if `process_job` is called on a *different* job first?
    1. Add A (p1), Add B (p2). Heap: `[(-2, 2, B), (-1, 1, A)]`.
    2. `get_next_job` -> pops B, returns B. `_jobs` still has B.
    3. `process_job(A, proc)` -> success. `del _jobs[A]`.
    4. `get_next_job` -> pops B, returns B.
    This is fine. The heap correctly tracks the order, and the dictionary tracks existence.

    Is there any issue with `get_next_job` popping from the heap and not being able to "re-insert" if not processed?
    If I `get_next_job` and it's not processed, and I call `get_next_job` again, I've already popped that item from the heap. So the second call to `get_next_job` will return the *next* job in the heap.
    This is actually a problem. `get_next_job` should probably be a `peek` if it's not consuming, or it should be the consumer.

    Let's rethink the Queue design.
    A queue's `get` usually removes. If `get_next_job` removes the job from the queue, then `process_job` can't be used with that ID unless `process_job` is the one that calls `get_next_job`.
    But the signature of `process_job` is `process_job(self, job_id, processor)`.

    Let's look at the prompt again.
    `process_job(self, job_id: str, processor: Callable) -> bool`
    `get_next_job(self) -> tuple[str, dict] | None`

    If `get_next_job` is "get the next one", and `process_job` is "process this specific one", they are two different ways to interact.
    If `get_next_job` is intended to be the way you find out what to do, and `process_job` is the way you actually do it, then `get_next_job` should not remove the job.

    But if `get_next_job` doesn't remove from the heap, it's a peek.
    If I use `heapq.nsmallest(1, self._heap)`, I can peek.

    Let's try a different `get_next_job`:
    ```python
    def get_next_job(self) -> tuple[str, dict] | None:
        # Peek at the heap without removing
        for _, _, job_id in self._heap:
            # Since it's a heap, we need to find the first one that is still valid
            # However, a heap isn't sorted, only the first element is guaranteed
            # So we might need to check the first, then if not valid, pop it.
            pass
    ```

    Wait, if `heap[0]` is the smallest, then `self._heap[0]` is the highest priority.
    If `self._heap[0]`'s `job_id` is not in `self._jobs`, then it's a "dead" job (already processed). We should pop it and then look at the new `heap[0]`.

    ```python
    def get_next_job(self) -> tuple[str, dict] | None:
        while self._heap:
            # Peek at the top
            _, _, job_id = self._heap[0]
            if job_id in self._jobs:
                job = self._jobs[job_id]
                return job.id, job.data
            else:
                # It was already processed, remove it
                heapq.heappop(self._heap)
        return None
    ```
    This is a perfect `peek`! It returns the highest priority job that hasn't been processed yet. It cleans up the heap as it goes.

    Final check:
    - `add_job`: Adds to `_jobs` and `_heap`.
    - `process_job`: Uses `RetryPolicy`. If success, removes from `_jobs`.
    - `get_next_job`: Peeks at `_heap[0]`, cleans up if `job_id` is not in `_jobs`, returns valid job.

    Wait, if `process_job` is called, it removes from `_jobs`. The next time `get_next_job` is called, it will see the `job_id` is not in `_jobs` and pop it. This is efficient and correct.

    One minor thing: `priority` is `int`. `dataclass` handles it. `RetryPolicy` is instantiated in `__init__` of `JobQueue`.

    Wait, I should make sure `RetryPolicy` is used *inside* `process_job` as requested.
    "process_job ... must USE RetryPolicy (compose, don't reimplement retry logic)."
    My code:
    ```python
    success, _ = self._retry_policy.run(processor, job.data)
    ```
    This is composition.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    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: Any) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    continue
            return False, attempts

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._heap: list[tuple[int, int, str]] = []
            self._counter: int = 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
            self._counter += 1
            heapq.heappush(self._heap, (-priority, self._counter, job_id))

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

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._heap:
                _, _, job_id = self._heap[0]
                if job_id in self._jobs:
                    job = self._jobs[job_id]
                    return job.id, job.data
                else:
                    heapq.heappop(self._heap)
            return None
    ```
    Wait, the `retry_policy` in `JobQueue` should probably be initialized with the `max_attempts` if I wanted to be flexible, but the prompt says `RetryPolicy` has `__init__(self, max_attempts: int = 4)`. It doesn't specify how `JobQueue` receives this, so a default instantiation inside `JobQueue.__init__` is the most logical interpretation.

    Final check on `Job`'s `retries` field: The prompt says "dataclass with fields ... `retries: int = 0`". It doesn't explicitly say I must increment this field during retries. However, if I only pass `data` to `RetryPolicy.run(fn, data)`, I can't easily update the `Job` object's `retries` attribute unless I pass the whole `Job` object. But the signature says `fn(data)`. So I'll just leave `retries` as a field in the dataclass as requested.

    *Self-correction*: The `JobQueue.add_job` signature is `add_job(self, job_id: str, data: dict, priority: int = 0)`. It does not take `retries`. So `retries` in `Job` must default to 0.

    *Wait*, one last thing: `get_next_job` return type. `tuple[str, dict] | None`. This is valid Python 3.10+ syntax.

    `RetryPolicy.run(self, fn: Callable, data) -> tuple[bool, int]`
    The `data` is passed to `fn(data)`.
    If `fn` is something like `lambda d: print(d['key'])`, then `data` is `d`.
    This matches.

    `get_next_job` return type: `tuple[str, dict] | None`.
    Wait, `dict` in Python 3.9+ is fine for type hinting.
    Standard library `Callable` and `Any` and `dataclass` and `heapq` are all fine.

    One detail: `JobQueue.process_job` return type: `bool`.
    Everything seems to align with the prompt.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    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: Any) -> tuple[bool, int]:
            attempts_made = 0
            while attempts_made < self.max_attempts:
                attempts_made += 1
                try:
                    fn(data)
                    return True, attempts_made
                except Exception:
                    pass
            return False, attempts_made

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._heap: list[tuple[int, int, str]] = []
            self._counter: int = 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
            self._counter += 1
            heapq.heappush(self._heap, (-priority, self._counter, job_id))

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

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

    One final check on `get_next_job`'s FIFO order:
    `heapq.heappush(self._heap, (-priority, self._counter, job_id))`
    For same priority, `-priority` is same.
    `self._counter` will be unique and increasing.
    So `heapq` will pick the one with the smallest `self._counter`.
    Smallest counter = first added.
    That is FIFO.

    Wait, `self._counter` starts at 0 and increments.
    Job 1: counter 1.
    Job 2: counter 2.
    `heapq` will return counter 1 before counter 2. Correct.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Any
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: Any) -> tuple[bool, int]:
        attempts_made = 0
        while attempts_made < 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._heap: list[tuple[int, int, str]] = []
        self._counter: int = 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
        self._counter += 1
        # Use -priority for max-heap behavior and self._counter for FIFO within same priority
        heapq.heappush(self._heap, (-priority, self._counter, job_id))

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

    def get_next_job(self) -> tuple[str, dict] | None:
        while self._heap:
            # Peek at the highest priority job
            _, _, job_id = self._heap[0]
            if job_id in self._jobs:
                job = self._jobs[job_id]
                return job.id, job.data
            else:
                # Job was already processed/removed, clean up heap
                heapq.heappop(self._heap)
        return None
```
</content>