<reasoning_content>
*   Goal: Refactor a job queue into three specific classes (`Job`, `RetryPolicy`, `JobQueue`) in `solution.py`.
    *   Constraints:
        *   Use `@dataclass`.
        *   `Job`: `id`, `data`, `priority`, `retries`.
        *   `RetryPolicy`: `__init__` (max_attempts=4), `run` (calls `fn(data)`, retries on exception, returns `(success, attempts_made)`, no actual sleep).
        *   `JobQueue`: `add_job`, `process_job` (must use `RetryPolicy`), `get_next_job` (highest priority, then FIFO).
        *   Library: Standard library only.
        *   Output: Only Python code, no explanation.

    *   `from dataclasses import dataclass`
    *   `from typing import Callable, Dict, Tuple, Optional`
    *   `import heapq` (for the priority queue? Or just a list sorted by priority? `heapq` is standard and efficient for priority).

    ```python
    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0
    ```
    *Wait, the prompt says `priority: int = 0` and `retries: int = 0`.*

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

    *   Storage: Needs to handle priority (higher number = higher priority? Or lower?). Usually "highest-priority" implies the largest number or a specific ordering. Let's assume higher number = higher priority.
    *   FIFO order within priority: A list of jobs sorted by priority (descending) and insertion order.
    *   `add_job`: Store job.
    *   `process_job`: Use `RetryPolicy`.
    *   `get_next_job`: Extract the best job.

    *Design for JobQueue:*
    To handle FIFO within the same priority, I can use a list of lists, or a list of jobs and sort them.
    Actually, a `heapq` implementation would need a unique counter to ensure FIFO for equal priorities. Or just a sorted list since the queue size isn't specified as massive.
    The prompt says "return the highest-priority job's (id, data)".

    Let's look at the requirements again:
    - `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`

    Wait, if `process_job` takes a `job_id`, I need a way to look up the job by ID.
    If `get_next_job` removes the job (it says "return", but usually "get next" in a queue context implies removal/consumption). Let's assume it removes it.

    *Logic for JobQueue:*
    - `self.jobs`: a dictionary `{id: Job}` for lookup by ID.
    - `self.queue`: a list of `(priority, order_index, job_id)` to manage the order.
    - `self.counter`: to handle FIFO.

    *Wait, simpler approach:*
    Just store a list of `Job` objects.
    `add_job`: append to list.
    `get_next_job`: find max priority, then smallest insertion index. Or just sort every time? No, let's keep it efficient.

    Actually, `process_job` needs to know the data. If `get_next_job` returns the data, then `process_job` needs the data.
    The signature of `process_job` is `process_job(self, job_id: str, Processor: Callable)`.
    Since `process_job` only takes `job_id`, it must fetch the job from the internal storage.

    *Revised JobQueue Logic:*
    - `self.jobs`: dict of `id -> Job`.
    - `self.order`: a list of `(priority, count, id)`.
    - `self.counter`: integer.

    *Wait, let's look at the priority definition:* "highest-priority job's (id, data)".
    Usually, higher number = higher priority.
    FIFO means if priorities are equal, the one added first comes out first.

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


    @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: Dict[str, Job] = {}
            self.priority_queue = [] # Elements: (-priority, insertion_index, 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
            # Use negative priority for max-heap behavior with heapq
            heapq.heappush(self.priority_queue, (-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]]:
            if not self.priority_queue:
                return None
            
            neg_priority, _, job_id = heapq.heappop(self.priority_queue)
            job = self.jobs.pop(job_id)
            return (job.id, job.data)
    ```

    Check constraints:
    1. `Job` dataclass: Correct.
    2. `RetryPolicy`: `max_attempts` init, `run` method with `fn` and `data`, retries, returns `(success, attempts_made)`, no sleep. Correct.
    3. `JobQueue`:
        - `add_job`: signature matches.
        - `process_job`: signature matches, uses `RetryPolicy`.
        - `get_next_job`: returns `tuple[str, dict] | None`, highest priority (max), FIFO.

    Wait, the `process_job` signature: `process_job(self, job_id: str, Processor: Callable)`.
    If I call `get_next_job` first, the job is popped from the queue. Does `process_job` still have access to it?
    If `get_next_job` removes it from the internal `jobs` dict, `process_job` will fail.
    Usually, in a queue, `get_next_job` is the consumer.
    However, if `process_job` is supposed to work with `job_id`, it implies the job must still be in the queue or a registry.

    Let's re-read:
    `process_job` must use `RetryPolicy`.
    `get_next_job` returns the highest-priority job.

    If `get_next_job` is called, and then `process_job` is called, the code needs to work.
    If `get_next_job` removes the job from the internal storage, `process_job` cannot find it.
    Perhaps `get_next_job` shouldn't remove it? But the word "Queue" usually implies removal.
    Let's look at the return type of `get_next_job`: `tuple[str, dict] | None`.
    If it doesn't remove the job, it's more like a "peek".
    But if it's a queue, usually you pop.

    Let's look at the `JobQueue` requirements again:
    - `add_job`: adds to queue.
    - `process_job`: executes the job using `RetryPolicy`.
    - `get_next_job`: returns the highest priority job.

    If `process_job` is called with a `job_id` that was returned by `get_next_job`, but `get_next_job` removed it from the queue's internal tracking, then `process_job` would fail to find it.

    Alternative: `get_next_job` does NOT remove it. But then calling it twice returns the same job. That's not a queue, that's a priority list.
    Alternative: `process_job` removes it? No, `process_job` takes a `job_id`.

    Let's assume:
    1. `add_job` puts it in a dict and a priority list.
    2. `get_next_job` pops it from the priority list and the dict.
    3. `process_job` looks it up in a *different* storage? No, that's overcomplicating.

    Actually, most likely, `process_job` is intended to be called on a job still in the queue. If it's already been "gotten", it's gone.
    Or, `get_next_job` doesn't remove it.
    Let's look at standard queue patterns. `get_next_job` usually removes.
    If `get_next_job` removes it, then `process_job` is likely meant to be called *before* `get_next_job` or `process_job` is intended to be the one that "takes" it? No, `process_job` takes an ID.

    Wait, if `process_job` is called, it processes the job. If it succeeds, we probably want it out of the queue.
    If I keep it in the `jobs` dict until `process_job` is called, then `get_next_job` would keep returning the same job until `process_job` is called. That's bad.

    Let's reconsider:
    Maybe `get_next_job` returns the `(id, data)` but *doesn't* remove it from the internal dictionary `self.jobs`. It only removes it from the `priority_queue` (the ordering mechanism).
    That way, `process_job` can still find it by ID.
    But `get_next_job` would then return the same job every time until it is processed.
    This is a common pattern in some systems where you "peek" at the next job to decide what to do with it.

    Let's look at the prompt again: "return the highest-priority job's (id, data)".
    It doesn't say "remove".
    However, a "Job Queue" usually implies consumption.

    Let's try a simple approach:
    `self.jobs`: a dict `{id: Job}`.
    `self.priority_queue`: a list.
    `add_job`: adds to both.
    `get_next_job`: pops from `priority_queue`. (So it is consumed).
    `process_job`: This becomes tricky. If `get_next_job` consumed it, `process_job` can't find it.

    Wait! If `process_job` is called, it might be the user's responsibility to call `get_next_job` first? No, `process_job` takes an ID.
    What if `process_job` removes it?
    If `process_job` is called, it finds the job in the dictionary, processes it, and then removes it from the dictionary.
    If `get_next_job` is called, it finds the highest priority job in the dictionary, and removes it from the priority list? No, that doesn't work because you need to know which one is highest priority.

    Let's go with the most standard interpretation for these types of coding challenges:
    1. `add_job`: Store job in a dictionary and a priority list.
    2. `get_next_job`: Pop the highest priority job from the priority list and the dictionary. Return the ID and data.
    3. `process_job`: If the user wants to process a job, they must provide the ID. If it's already been "gotten", it's no longer in the queue. This is fine. It's the caller's responsibility.
    *Wait*, if `process_job` is called on a job that hasn't been "gotten" yet, it should still work.

    Let's refine `JobQueue`:
    - `self.jobs`: `dict[str, Job]`
    - `self.priority_queue`: `list` (heap)
    - `add_job`: `self.jobs[id] = job`, `heapq.heappush(...)`
    - `get_next_job`: `heapq.heappop(...)` -> `id`. `job = self.jobs.pop(id)`. Return `(id, job.data)`.
    - `process_job`: `job = self.jobs.get(id)`. If `job` exists, `policy.run(...)`. If `job` exists, `self.jobs.pop(id)`. (Optional: `process_job` could also remove it from the heap, but finding the right item in a heap to remove is O(N)).

    Actually, most likely, `process_job` is meant to be the primary way to interact with a specific job, and `get_next_job` is for consumption.
    If `process_job` is used, the job should be removed.

    Let's simplify:
    `self.jobs`: `dict`
    `self.priority_queue`: `list`
    `add_job`: Add to both.
    `get_next_job`: Pop from `priority_queue`. Remove from `jobs` dict.
    `process_job`: Look up in `jobs` dict. If found, run and remove from `jobs` dict.

    This ensures that once a job is "gotten" OR "processed", it's gone from the queue.

    Wait, if `get_next_job` removes it from `jobs` dict, `process_job` won't find it. This makes the two methods mutually exclusive for a single job. That's perfectly fine for a Queue.

    One more check: `priority`. Highest priority first. `heapq` is a min-heap. So I'll use negative priority.

    *    `Job`: `@dataclass`, `id`, `data`, `priority`, `retries`.
    *    `RetryPolicy`: `max_attempts`, `run(fn, data)`.
    *    `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    Let's double check the `process_job` requirements: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    Does it need to remove the job? The prompt doesn't say. But usually, if you process it, it's done.
    I'll make `process_job` remove the job from the dictionary to be safe.

    Wait, the prompt says:
    `process_job(self, job_id: str, Processor: Callable) -> bool` — must USE RetryPolicy.
    It doesn't mention removing it.
    I will stick to:
    - `add_job`: Add to dict and heap.
    - `get_next_job`: Pop from heap and dict.
    - `process_job`: Get from dict, run, and then remove from dict.

    Is there any chance `get_next_job` should *not* remove?
    "return the highest-priority job's (id, data)"
    If it doesn't remove, `get_next_job` would return the same thing until `process_job` is called.
    But `process_job` takes a `job_id`.
    If `get_next_job` doesn't remove, and `process_job` doesn't remove, the queue would never empty. That's not a queue.
    I'll stick with the "removal" logic.

    Wait, if `get_next_job` removes it, `process_job` won't find it.
    If the user wants to use both, they can't.
    Maybe `process_job` should be the one that removes it?
    If `process_job` removes it, `get_next_job` would keep returning the same job until `process_job` is called.
    This makes more sense for a "Job Queue" where you want to know what's next, then you process it.

    Let's re-read: `get_next_job` returns "the highest-priority job".
    If I have jobs A (priority 10), B (priority 5), C (priority 5).
    `get_next_job` returns A.
    If I call `get_next_job` again, it should return B.
    This means `get_next_job` *must* remove the job from the priority ordering.

    If `get_next_job` removes the job from the priority ordering, `process_job` *must* be able to find it.
    Therefore, the `jobs` dictionary should *not* be popped by `get_next_job`.
    Only the `priority_queue` should be popped.

    *Revised Strategy:*
    1. `self.jobs`: `dict[str, Job]` (The master registry)
    2. `self.priority_queue`: `list` (The heap for ordering)
    3. `add_job`: Add to both.
    4. `get_next_job`: Pop from `priority_queue`. Return `(id, data)`.
    5. `process_job`: Get from `self.jobs`. Run. Remove from `self.jobs`.

    Actually, if `process_job` removes it from `self.jobs`, then `get_next_job` will fail to return it next time? No, `get_next_job` looks at the `priority_queue`.
    If `process_job` removes it from `self.jobs`, but `get_next_job` doesn't check `self.jobs`? That's bad.

    Let's try again.
    The simplest, most robust implementation for a "Job Queue" is:
    - `add_job`: Add to a list of `Job` objects.
    - `get_next_job`: Sort list by priority (desc), then insertion order (asc). Pop first.
    - `process_job`: Find job in list by ID. Run it. Remove it.

    But sorting every time is O(N log N). `heapq` is better.
    If I use `heapq`:
    - `add_job`: `heappush` `(-priority, counter, job_id)`
    - `get_next_job`: `heappop` -> `job_id`. `job = self.jobs.pop(job_id)`.
    - `process_job`: `job = self.jobs.get(job_id)`. If `job`, `run`, then `self.jobs.pop(job_id)`.

    This is the cleanest. It means `process_job` and `get_next_job` are two different ways to consume a job. One is "I want to process this specific ID", the other is "Give me the best one".

    One detail: "FIFO order within the same priority".
    `heapq` with `(-priority, counter, job_id)` handles this perfectly because `counter` is unique and increasing.

    `RetryPolicy.run(self, fn: Callable, data) -> tuple[bool, int]`
    The prompt says `data` as a parameter, not `job_id`.
    So `process_job` calls `retry_policy.run(processor, job.data)`.

    Let's double check types.
    `retry_policy.run` returns `tuple[bool, int]`.
    `JobQueue.process_job` returns `bool`.

    Wait, one small thing: "Do NOT actually sleep."
    This is clear.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, 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_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: Dict[str, Job] = {}
            self.priority_queue = []
            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.priority_queue, (-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)
            if success:
                del self.jobs[job_id]
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.priority_queue:
                return None
            
            _, _, job_id = heapq.heappop(self.priority_queue)
            job = self.jobs.pop(job_id, None)
            if job:
                return (job.id, job.data)
            return None
    ```
    Wait, if `process_job` succeeds, I delete it from `self.jobs`.
    If `get_next_job` is called, it pops from `priority_queue` and then tries to pop from `self.jobs`.
    If `process_job` was called first, `self.jobs.pop(job_id)` will return `None`.
    This is correct.

    Wait, one more thing. `get_next_job` should return `None` if nothing's there.
    If `heappop` succeeds but `self.jobs.pop` returns `None` (because `process_job` already removed it), I should probably handle that so `get_next_job` doesn't crash.
    Current code: `job = self.jobs.pop(job_id, None)` handles it.

    Check `retry_policy.run`:
    `attempts_made` starts at 0.
    If success on first try: `attempts_made` is 0, returns `(True, 1)`. Correct.
    If fail on 4th try: `attempts_made` becomes 4, loop ends, returns `(False, 4)`. Correct.

    Actually, let me check the requirement `max_attempts`:
    "retries on any exception until success or max_attempts reached".
    If `max_attempts` is 4.
    Try 1: Fail
    Try 2: Fail
    Try 3: Fail
    Try 4: Fail
    Return `(False, 4)`. Correct.

    Wait, I see a potential bug in `process_job`:
    If `process_job` succeeds, it removes the job from `self.jobs`.
    However, the job ID still exists in `self.priority_queue`.
    So `get_next_job` would eventually pop that ID, try to `pop` it from `self.jobs`, fail, and return `None`.
    This is fine. It's a bit messy but functionally correct.

    One small change:
    In `process_job`, if I don't remove the job from the `priority_queue`, it stays there.
    But removing from a heap is hard.
    Alternatively, I could make `process_job` also remove from `self.jobs` and `get_next_job` also remove from `self.jobs`.
    Actually, if I only have one way to "consume" a job, it's easy.
    But there are two: `process_job` and `get_next_job`.

    Let's refine `JobQueue` one more time to be cleaner:
    ```python
    class JobQueue:
        def __init__(self):
            self.jobs = {}
            self.priority_queue = []
            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.priority_queue, (-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)
            if success:
                self.jobs.pop(job_id)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            while self.priority_queue:
                _, _, job_id = heapq.heappop(self.priority_queue)
                job = self.jobs.pop(job_id, None)
                if job:
                    return (job.id, job.data)
            return None
    ```
    The `while` loop in `get_next_job` is good because it cleans up "stale" IDs that were already removed by `process_job`.

    Wait, `process_job` signature is `process_job(self, job_id: str, Processor: Callable) -> bool`.
    The `RetryPolicy.run` signature is `run(self, fn: Callable, data) -> tuple[bool, int]`.
    Wait, the prompt says `run(self, fn: Callable, data)`. It does *not* specify the type of `data`. I'll assume `dict` or `Any`.

    Final check on `Job` dataclass:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    Yes.

    Wait, `RetryPolicy.run` return type: `tuple[bool, int]`.
    Does it need to be `Tuple` or `tuple`? Python 3.9+ allows `tuple`. I'll use `tuple` for simplicity as it's standard now.

    One more look: `process_job` signature.
    `process_job(self, job_id: str, Processor: Callable) -> bool`
    I must use `RetryPolicy`.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, 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_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: Dict[str, Job] = {}
            self.priority_queue = []
            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.priority_queue, (-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)
            if success:
                self.jobs.pop(job_id)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self.priority_queue:
                _, _, job_id = heapq.heappop(self.priority_queue)
                job = self.jobs.pop(job_id, None)
                if job:
                    return (job.id, job.data)
            return None
    ```
    Wait, the prompt asks for `tuple[str, dict] | None`. I should make sure the typing is correct. Python 3.10+ allows `|`. If not, `Optional`. Since the prompt uses `|`, I'll use that.

    One last check: `RetryPolicy.run` does not actually sleep.
    I'm not using `time.sleep`. Correct.

    Wait, `JobQueue.get_next_job` return type `tuple[str, dict] | None`.
    I'll ensure `Job` is imported/used correctly.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, 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_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: Dict[str, Job] = {}
            self.priority_queue = []
            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.priority_queue, (-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)
            if success:
                self.jobs.pop(job_id)
            return success

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

    Wait, the type hint `tuple[str, dict] | None` is Python 3.10+. I should probably stick to that as it's in the prompt.

    Final check on `Job` fields: `id`, `data`, `priority`, `retries`.
    Correct.

    Wait, one tiny detail: "Do NOT actually sleep".
    If the `RetryPolicy` was supposed to have a `sleep_time` parameter, it's not mentioned. So I won't add it.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Dict, 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_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: Dict[str, Job] = {}
        self.priority_queue = []
        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.priority_queue, (-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)
        if success:
            self.jobs.pop(job_id)
        return success

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