<reasoning_content>
*   Refactor a job queue into three specific classes in `solution.py`.
    *   Requirement 1: `Job` (dataclass) - `id`, `data`, `priority`, `retries`.
    *   Requirement 2: `RetryPolicy` - `max_attempts`, `run(fn, data)` method.
    *   Requirement 3: `JobQueue` - `add_job`, `process_job`, `get_next_job`.
    *   Constraint 1: Use standard library only.
    *   Constraint 2: `RetryPolicy.run` should not actually sleep.
    *    Constraint 3: `JobQueue.process_job` must use `RetryPolicy`.
    *    Constraint 4: `JobQueue.get_next_job` must return the highest priority (FIFO for same priority).
    *    Constraint 5: Write only Python code.

    *   `Job`:
        ```python
        from dataclasses import dataclass
        from typing import Dict

        @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[[dict], any], 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
        ```
        *Correction*: The requirement says "calls `fn(data)`". The signature for `run` should match the instruction. The instruction says `run(self, fn: Callable, data)`.

    *   `JobQueue`:
        ```python
        import heapq
        from typing import List, Tuple, Optional, Callable

        class JobQueue:
            def __init__(self):
                self.jobs: List[Job] = [] # Since we need priority and FIFO
                self.retry_policy = RetryPolicy()

            def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
                # To handle FIFO for same priority, we need a counter
                # Or just store items and sort them?
                # Wait, standard heapq is a min-heap. Priority should be high number = high priority?
                # Or lower is better? Usually "highest priority" means max value.
                # Let's use a sequence counter to ensure FIFO.
                pass

            def process_job(self, job_id: str, Processor: Callable) -> bool:
                # Find the job...
                # Use self.retry_policy.run(Processor, job.data)
                pass

            def get_next_job(self) -> Tuple[str, dict] | None:
                # Return highest priority, FIFO.
                pass
        ```

    *   *Refining `JobQueue` logic*:
        The requirement doesn't specify how to store internal data, but it asks for `get_next_job` to return the highest priority and FIFO for same priority.
        A common way is using a list of tuples: `(priority, sequence, job_object)`.
        However, `process_job` takes a `job_id`. This suggests the queue might hold jobs by ID too.

    *   *Re-reading instructions carefully*:
        `add_job(self, job_id: str, data: dict, priority: int = 0)`
        `process_job(self, job_id: str, Processor: Callable)`
        `get_next_job(self)`

        If `process_job` takes a `job_id`, it implies the queue keeps track of jobs by ID.
        If `get_next_job` returns the highest priority, we need a way to identify it.

        Wait, `JobQueue` should probably store `Job` objects.
        Let's use a list for `jobs` and sort it or use `heapq`.
        Since it's a "Queue", `get_next_job` usually removes the item.

        Let's look at `Job` definition: `id`, `data`, `priority`, `retries`.

        Implementation details:
        - `JobQueue`:
            - `self.queue`: a list of `Job` objects.
            - `add_job`: Create a `Job` object, append to list.
            - `get_next_job`: Sort list by priority desc, then original order? Or just use a simple sort.
            - `process_job`: Find job by id in the list. Run policy. Update retries if fails? The instructions don't explicitly say to update retries, but `Job` has a `retries` field.
            - Actually, `RetryPolicy.run` returns `(success, attempts_made)`. I should probably update the `retries` field in the `Job` object if it fails? No, the instruction for `process_job` just says "must USE RetryPolicy".

        Wait, `get_next_job` usually "gets" (removes) or just "gets" (peeks)?
        "return the highest-priority job's (id, data)". Usually "get" implies removal in a queue context.

        Let's refine the sorting:
        To maintain FIFO for same priority:
        When adding a job, assign a sequence number.
        Priority: Higher is higher? Usually "Priority 10" > "Priority 1".
        Let's assume Higher number = Higher priority.

    *   `RetryPolicy` method `run(self, fn: Callable, data)`.
        The instruction says: `run(self, fn: Callable, data) -> tuple[bool, int]`.
        Note: No type hint for `data` in the instruction signature? Or is it `data: dict`? I'll use `data` as specified.

    *   `JobQueue.process_job`:
        If `process_job` is called with `job_id`, it should find the job in the internal storage.
        Does it remove it? The prompt doesn't say.
        Does it update `retries`? The prompt doesn't say. I will just call the policy.

    *   `JobQueue.get_next_job`:
        "return the highest-priority job's (id, data); FIFO order within the same priority."
        If I use `list.sort(key=lambda x: (-x.priority, x.sequence))`, that works perfectly.

    *   *Actually*, there's a conflict. If `process_job` takes a `job_id` and `get_next_job` returns the next one, `process_job` might be called on a job that hasn't been "gotten" yet, or `get_next_job` might be removing things.
    Usually, in these prompts:
    1. `add_job` puts it in the list.
    2. `get_next_job` finds the highest priority, removes it, returns it.
    3. `process_job` finds a job by ID (maybe it's already been "gotten" or still in the queue).

    Let's look at the `Job` dataclass. It has `retries`.
    `RetryPolicy` has `max_attempts`.
    When `process_job` is called, it should use the `RetryPolicy`.

    One detail: `RetryPolicy.run` signature: `run(self, fn: Callable, data)`.
    I'll use `Callable` from `typing`.

    ```python
    from dataclasses import dataclass
    from typing import Dict, Callable, Tuple, List, 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 = 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: List[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)
            # To handle FIFO for same priority, we need a way to track insertion order
            # We can add a sequence number to the Job or just use the current list order
            # But since we sort, we need a sequence.
            # Let's modify Job slightly? No, can't change the dataclass definition.
            # Let's store them in a way that preserves order.
            self.jobs.append(job)
            # We'll use a trick: use the index in self.jobs as sequence?
            # No, because we might remove items.
            # Let's just use a sequence counter and put it in a private attribute.

        # Wait, I can't modify the Job dataclass.
        # So I'll store (priority, sequence, Job object) in a list.
    ```

    *Rethinking `JobQueue` storage*:
    Requirement: `Job` dataclass has `id`, `data`, `priority`, `retries`.
    Requirement: `JobQueue` has `add_job`, `process_job`, `get_next_job`.

    If I store `Job` objects in a list:
    `add_job`: `self.jobs.append(Job(...))`
    `get_next_job`:
    Sort `self.jobs` by `priority` (descending) then by index (ascending).
    Return `(id, data)` and remove from list.

    `process_job(self, job_id: str, Processor: Callable)`:
    Find job with `id`.
    If found, `self.retry_policy.run(Processor, job.data)`.
    Return `True` if success, `False` otherwise? The instruction says `-> bool`.
    `RetryPolicy.run` returns `(success, attempts_made)`.
    So `process_job` should return `success`.

    *Wait*, if `get_next_job` removes the job, how can `process_job` find it by ID?
    Maybe `process_job` is for jobs that have been "taken" but not "processed"?
    Or maybe `process_job` is just an arbitrary operation on any job in the queue.

    Let's assume:
    - `jobs` is a list of `Job` objects.
    - `get_next_job`: returns highest priority, removes it.
    - `process_job`: finds by ID in the *remaining* jobs.
    - *OR* `process_job` is called on jobs regardless of whether they are "next".

    Let's check the prompt again.
    "return the highest-priority job's (id, data)" - does not say "remove".
    Usually, in a queue, "get" might be "peek" or "pop".
    However, "Queue" usually implies FIFO.
    If it doesn't say "remove", maybe it doesn't remove.
    But if it doesn't remove, `get_next_job` will always return the same job.
    Let's assume `get_next_job` removes it.

    Wait, let's look at `process_job` again.
    `process_job(self, job_id: str, Processor: Callable) -> bool`
    It needs to use `RetryPolicy`.
    `RetryPolicy.run` returns `(success, attempts_made)`.
    So `process_job` returns `success`.

    Let's re-evaluate `get_next_job`:
    "return the highest-priority job's (id, data); FIFO order within the same priority."
    If it doesn't remove, how do we handle "next"?
    If I have two jobs with priority 10, and I call `get_next_job` twice, I should get Job A then Job B.
    If I don't remove them, I get Job A both times.
    Therefore, it MUST remove them.

    Let's check `Job` dataclass again.
    `retries` field.
    Should `process_job` update `retries`?
    The instructions don't say to, but it's a field in the dataclass.
    Actually, `RetryPolicy.run` handles retries internally.
    So `process_job` will call `run`, and `run` will handle the loop.

    One complication: `process_job` receives a `job_id`.
    If `get_next_job` removes the job from the internal list, `process_job` won't find it.
    This suggests `process_job` might be for jobs still in the queue.
    Or, `get_next_job` does *not* remove the job.

    Let's think about standard Queue behavior.
    Actually, I'll just keep a list of `Job` objects.
    `add_job`: append to list.
    `get_next_job`: find highest priority, remove from list, return.
    `process_job`: find by id in list, run policy, return result.

    Wait, if `process_job` is meant to be the way to actually "do" the work, and `get_next_job` is just "getting" the ID/Data... then `process_job` probably shouldn't remove it? But `process_job` takes a `job_id`.

    Let's try to find a balance.
    If I use a list `self.queue` for `get_next_job` (removes item) and a dict `self.all_jobs` for `process_job` (keeps all jobs), that works.
    But the instructions don't say to maintain two structures.

    Let's look at the `Job` definition again. `retries`.
    If `RetryPolicy` is used, it manages retries internally for a single `process_job` call.
    The `retries` field in `Job` might be for *cumulative* retries?
    If so, `process_job` should update `job.retries` based on `attempts_made` from `RetryPolicy.run`.

    Let's refine:
    1. `Job` dataclass.
    2. `RetryPolicy` class.
    3. `JobQueue` class:
       - `self.jobs`: list of `Job` objects.
       - `add_job`: append `Job` to `self.jobs`.
       - `get_next_job`:
         - find job with max `priority`.
         - if multiple, find one with lowest index (FIFO).
         - remove it from `self.jobs`.
         - return `(id, data)`.
       - `process_job`:
         - find `Job` in `self.jobs` where `job.id == job_id`.
         - if not found, return `False`.
         - `success, attempts = self.retry_policy.run(Processor, job.data)`
         - `job.retries += attempts`
         - return `success`.

    Wait, if `get_next_job` removes the job, then `process_job` won't find it if it's already been "gotten".
    Maybe `get_next_job` doesn't remove?
    If `get_next_job` doesn't remove, how do we ensure "FIFO order within the same priority" for subsequent calls?
    We could track "already returned" or "processed" status.
    But "Queue" usually implies removal.

    Let's look at `process_job` again. It is a method of `JobQueue`.
    If a user wants to process a job, they usually call `get_next_job` to see what's next, then call `process_job`.
    If `get_next_job` removes it, `process_job` fails.
    Therefore, `get_next_job` probably *doesn't* remove.

    If `get_next_job` doesn't remove:
    - `add_job`: append to list.
    - `get_next_job`:
        Find all jobs with max priority.
        Of those, find the one that hasn't been "gotten" yet? No, that's complex.
        Actually, if I call `get_next_job` twice, and it doesn't remove, it will return the same job.
        Is that okay? Maybe not.

    Let's reconsider "Queue". A queue is a FIFO structure.
    If I have a list of jobs, and I want the highest priority:
    Maybe `get_next_job` removes it.
    Then `process_job` is for a job that is *still* in the queue but we want to process it specifically by ID.
    This makes sense. `process_job` is an out-of-order process command.

    Let's check the `retries` field again.
    If `RetryPolicy.run` is called, it tries `max_attempts`.
    If it succeeds on attempt 3, `attempts_made` is 3.
    Then `job.retries` becomes `current_retries + 3`.

    One final detail: `RetryPolicy.run` signature:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    The instructions don't specify `data`'s type in the `run` signature.
    I will use `data: dict`.

    Wait, `Job` dataclass `retries` is `int = 0`.
    `RetryPolicy` `max_attempts` is `int = 4`.

    Wait, the prompt says "WFIFO order within the same priority".
    To implement this efficiently:
    Store jobs in a list.
    `get_next_job`:
    1. Find max priority among all jobs in `self.jobs`.
    2. Find the first job in `self.jobs` that has that priority.
    3. Remove it from `self.jobs`.
    4. Return its `(id, data)`.

    `process_job`:
    1. Find job with `id` in `self.jobs`.
    2. If not found, return `False`.
    3. `success, attempts = self.retry_policy.run(Processor, job.data)`
    4. `job.retries += attempts`
    5. return `success`.

    Wait, if `process_job` succeeds, does it remove the job from the queue?
    The prompt doesn't say. Usually, "processing" a job in a queue system means it's done.
    But the prompt doesn't specify. I'll leave it in the queue unless I'm sure.
    Actually, most interview/coding tasks like this expect `process_job` to be the "removal" step, but `get_next_job` is just the "peek" step?
    No, "Queue" and "get_next_job" usually implies removal.
    Let's look at the name `get_next_job`. In many APIs, `get` = pop, `peek` = peek.
    If `get_next_job` is a pop:
    Then `process_job` is only for jobs still in the queue.

    Let's consider another interpretation:
    - `get_next_job` does NOT remove.
    - `process_job` DOES remove (if successful).

    This is also common. But "FIFO" is hard to maintain if `get_next_job` doesn't remove and `process_job` does.

    Let's go with the most standard interpretation:
    - `add_job`: adds to a list.
    - `get_next_job`: finds highest priority, removes it, returns it.
    - `process_job`: finds by id, executes it via `RetryPolicy`, updates `retries` count, returns success bool.

    Wait, one more thing.
    `RetryPolicy.run(self, fn: Callable, data)`.
    Is `data` always a dict? The `Job` definition says `data: dict`.
    So `fn` will be called as `fn(data)`.

    Let's write the code.

    ```python
    from dataclasses import dataclass
    from typing import Dict, Callable, Tuple, List, 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 = 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: List[Job] = []
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            self.jobs.append(Job(id=job_id, data=data, priority=priority)))

        def process_job(self, job_id: str, Processor: Callable) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if not job:
                return False
            
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries += attempts
            return success

        def get_next_job(self) -> Tuple[str, dict] | None:
            if not self.jobs:
                return None
            
            # Find highest priority
            max_priority = max(j.priority for j in self.jobs)
            
            # Find first occurrence (FIFO)
            for i, job in enumerate(self.jobs):
                if job.priority == max_priority:
                    res = (job.id, job.data)
                    self.jobs.pop(i)
                    return res
            return None
    ```

    Wait, the `RetryPolicy.run` signature in the prompt:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    The instruction says `data`, no type hint. I'll check if I should include it.
    "calls `fn(data)`".
    I'll stick to the signature exactly as written if possible, but `Callable` and `Tuple` need imports.

    One detail: `process_job` returns `bool`.
    `RetryPolicy.run` returns `(success, attempts_made)`.
    So `process_job` returns `success`.

    Another detail: `get_next_job` signature:
    `get_next_job(self) -> tuple[str, dict] | None`
    This is Python 3.10+ syntax. I'll use that.

    Wait, I should double check "FIFO order within the same priority".
    If I use `self.jobs.pop(i)`, and I always pick the first one that matches `max_priority`, that is indeed FIFO because `self.jobs` is an ordered list and I process from index 0.

    Wait, `RetryPolicy.run` signature: `run(self, fn: Callable, data)`.
    If I write `run(self, fn: Callable, data: dict)`, it's safer.

    Wait, the `Job` dataclass:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    My `add_job` signature:
    `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
    Matches perfectly.

    Final review of `RetryPolicy.run`:
    "Do NOT actually sleep." -> Checked.
    "returns (success, attempts_made)". -> Checked.

    Wait, the `process_job` method:
    "must USE RetryPolicy (compose, don't reimplement retry logic)".
    My `process_job` calls `self.retry_policy.run(Processor, job.data)`.
    This is composition.

    Check imports:
    `dataclass`, `Dict`, `Callable`, `Tuple`, `List`, `Optional`.
    Wait, the prompt says "Use only the standard library". These are all standard.

    One small detail: `tuple[str, dict] | None` is the return type for `get_next_job`.
    This requires Python 3.10+. If the environment is older, `Optional[Tuple[str, dict]]` is used. Given the syntax in the prompt `tuple[str, dict] | None`, I will use that syntax.

    Wait, `Job` dataclass:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    My code:
    ```python
    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0
    ```
    Matches exactly.

    One detail: `RetryPolicy.run` signature again:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    I will use `data` without a type hint inside the `run` method definition just to be safe, even though `data` is known to be a dict.

    Wait, should `process_job` remove the job from the queue?
    The prompt doesn't say.
    If `get_next_job` removes the job, and `process_job` doesn't, it's weird.
    If `get_next_job` doesn't remove the job, and `process_job` doesn't, it's also weird.
    If `get_next_job` doesn't remove the job, and `process_job` DOES remove it... that's a very common pattern in worker systems (Peek then Process).
    However, "FIFO order" for `get_next_job` is easier to satisfy if `get_next_job` is a Pop.

    Let's look at the `JobQueue` name. It's a Queue.
    In a Queue, `get_next_job` is the standard way to retrieve the next item to work on.
    If `process_job` is also a method on the queue, it might be an "explicit" processing command.
    I'll stick to:
    - `get_next_job` = Pop.
    - `process_job` = Process (doesn't remove, but `get_next_job` already removed it? No, that's a contradiction).

    Let's try this:
    `process_job` takes a `job_id`.
    `get_next_job` finds the best one and removes it.
    If `get_next_job` is called, the job is gone from `self.jobs`.
    If `process_job` is called with an ID that was already "gotten", it won't find it in `self.jobs`.
    This implies `process_job` is meant for jobs *still in the queue*.
    This is perfectly valid logic.

    Actually, I'll make `process_job` work on the `self.jobs` list.
    If it's not in `self.jobs`, it returns `False`.
    If it is in `selfs.jobs`, it executes it.

    Wait, if `process_job` succeeds, should it be removed?
    If I don't remove it, it stays in the queue and `get_next_job` will eventually return it again.
    This is logically consistent.

    Actually, there's a much simpler interpretation:
    - `get_next_job` is a Peek.
    - `process_job` is a Pop + Process.
    But the prompt says `get_next_job` returns the highest priority.
    If I call `get_next_job` 5 times, I should get 5 different jobs (FIFO).
    If `get_next_job` is a Peek, I only get 1 job 5 times.
    So `get_next_job` MUST be a Pop.

    If `get_next_job` is a Pop, then `process_job` is only for jobs that haven't been "gotten" yet.
    This is fine.

    Wait, one more thought. What if `process_job` is called *instead* of `get_next_job`?
    If `process_job` is called, the job is processed. If successful, should it be removed?
    I'll make it remove the job if `success` is true. No, that's guessing.
    I'll just follow the instructions literally.
    `process_job`: calls `RetryPolicy.run`, returns `success`.
    I won't remove it or anything else unless asked.

    Wait, I just realized: If `process_job` doesn't remove the job, and `get_next_job` does, then `process_job` is only usable for jobs that were never "gotten".
    If `process_job` *does* remove the job, then it's a complete operation.
    But the prompt doesn't say.
    I will just implement `process_job` to not remove the job, as the prompt doesn't mention removal.

    Actually, looking at `retries` field again.
    If `process_job` is called and fails, `retries` is incremented.
    If I don't remove the job from the queue, then the next time `get_next_job` sees it, it sees the updated `retries`? No, `get_next_job` doesn't care about `retries`.
    This logic holds up.

    One last check on `RetryPolicy.run`:
    "calls `fn(data)`, retries on any exception until success or `max_attempts` reached, returns `(success, attempts_made)`."
    My code:
    ```python
    def run(self, fn: Callable, data) -> Tuple[bool, int]:
        attempts = 0
        while attempts < self.max_attempts:
            try:
                fn(data)
                return True, attempts + 1
            except Exception:
                attempts += 1
        return False, attempts
    ```
    If `max_attempts` is 4.
    Attempts:
    0: try fn. fail. `attempts` becomes 1.
    1: try fn. fail. `attempts` becomes 2.
    2: try fn. fail. `attempts` becomes 3.
    3: try fn. fail. `attempts` becomes 4.
    Loop ends. Returns `(False, 4)`.
    This is correct. `attempts_made` is 4.

    If it succeeds on the first try:
    0: try fn. success. returns `(True, 1)`.
    Correct.

    Wait, `JobQueue.get_next_job` return type:
    `tuple[str, dict] | None`
    If I use the `|` syntax, I must ensure the environment supports it.
    Since the prompt uses it, I will use it.

    One final detail: `RetryPolicy.run(self, fn: Callable, data)`.
    I'll use exactly that signature.

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

    @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) -> 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[Job] = []
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: Dict, priority: int = 0) -> None:
            self.jobs.append(Job(id=job_id, data=data, priority=priority)))

        def process_job(self, job_id: str, Processor: Callable) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if job is None:
                return False
            
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries += attempts
            return success

        def get_next_job(self) -> Tuple[str, dict] | None:
            if not self.jobs:
                return None
            
            max_priority = max(j.priority for j in self.jobs)
            
            for i, job in enumerate(self.jobs):
                if job.priority == max_priority:
                    res = (job.id, job.data)
                    self.jobs.pop(i)
                    return res
            return None
    ```
    One fix: `data` in `Job` is `dict`, but `JobQueue` signature says `dict` too. Correct.
    One fix: `RetryPolicy.run` signature says `data`, no type hint. Correct.
    One fix: `JobQueue.get_next_job` return type is `tuple[str, dict] | None`. Correct.

    Wait, the prompt says `data: dict` in `Job` definition, but `data` in `RetryPolicy.run` signature. I'll stick to the prompt's typing where provided.

    Final structure of `solution.py`:
    Includes `dataclass`, `Callable`, `Tuple`, `Dict`, `List`.

    Wait, `JobQueue` `process_job` signature:
    `process_job(self, job_id: str, Processor: Callable) -> bool`
    Is `Processor` capitalized? Yes, in the prompt.

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

    One last check: `FIFO order within the same priority`.
    My loop:
    ```python
    for i, job in enumerate(self.jobs):
        if job.priority == max_priority:
            ...
            self.jobs.pop(i)
    ```
    Since `self.jobs` is ordered by insertion, the first `i` that matches `max_priority` is the oldest one.
    Correct.

    Actually, I should use `self.jobs.pop(i)` to remove it.
    If I don't pop it, `get_next_job` will always return the same one.
    So `pop` is necessary.
</reasoning_content>

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

@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) -> 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[Job] = []
        self.retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: Dict, priority: int = 0) -> None:
        self.jobs.append(Job(id=job_id, data=data, priority=priority)))

    def process_job(self, job_id: str, Processor: Callable) -> bool:
        job = next((j for j in self.jobs if j.id == job_id), None)
        if job is None:
            return False
        
        success, attempts = self.retry_policy.run(Processor, job.data)
        job.retries += attempts
        return success

    def get_next_job(self) -> Tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        max_priority = max(j.priority for j in self.jobs)
        
        for i, job in enumerate(self.jobs):
            if job.priority == max_priority:
                result = (job.id, job.data)
                self.jobs.pop(i)
                return result
        return None
```
</content>