<reasoning_content>
*   Refactor a job queue into three specific classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Requirement 1: `Job` dataclass (`id`, `data`, `priority`, `retries`).
    *   Requirement 2: `RetryPolicy` class (handles retry logic with `max_attempts`, returns `(success, attempts_made)`, no actual `sleep`).
    *   Requirement 3: `JobQueue` class (manages jobs, uses `RetryPolicy` for processing, `get_next_job` returns highest priority/FIFO).
    *   Constraints: Standard library only, `solution.py` filename (implied context), no explanations.

    *   `from dataclasses import dataclass`
    *   `from typing import Callable, Tuple, Dict, Optional, List`
    *   `import heapq` (for priority queue, though sorting or list might work, `heapq` is standard). *Correction*: `heapq` is standard. However, `JobQueue` needs FIFO within same priority. `heapq` is a min-heap. I'll need to handle the counter for ties.

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

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

    *   `JobQueue` class.
        *   Need a way to store jobs to maintain priority and FIFO.
        *   `add_job`: Store job.
        *   `process_job`: Call `RetryPolicy.run`.
        *   `get_next_job`: Get highest priority (highest number or lowest? Usually "highest" implies maximum value, but `heapq` is min). I'll check the prompt: "highest-priority". If priority 10 > 5, I should use a max-heap or negate values.
        *   FIFO within same priority: `heapq` handles this if I include a counter.

    *   Wait, the prompt says `Job` dataclass has `retries`. Does `JobQueue` need to update `retries`?
    *   `process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    *   `get_next_job` signature: `tuple[str, dict] | None`.

    *Refining `JobQueue` storage*:
    A list of `Job` objects.
    `add_job`: add to list.
    `get_next_job`: find max priority, then find first occurrence (FIFO).
    Actually, to keep it efficient, I can sort or use a heap. Since it's a "Job Queue", usually we pop.

    Let's refine `get_next_job`:
    1. Find max priority in the collection.
    2. Return the first one found at that priority.
    3. Remove it? The prompt doesn't explicitly say "remove", but "get next" usually implies popping or returning the one to be processed next. Usually, it removes. Let's assume it removes.

    *Wait, Priority*: High number = High priority? Usually "Priority 0" is lowest. Let's assume higher value = higher priority.
    Wait, standard `heapq` is min-heap. To get max priority, I can store `(-priority, sequence_number, job_id)`.

    *Re-reading requirements:*
    `process_job` must USE `RetryPolicy`.
    `RetryPolicy.run` returns `(success, attempts_made)`.
    `JobQueue.process_job` returns `bool`.

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

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

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data: dict) -> Tuple[bool, int]:
            attempts = 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()
            self._counter = 0 # For FIFO

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            # We use a list and sort/search or use heapq. 
            # To keep it simple and correctly handle FIFO, we'll use a heap.
            # Since heapq is min-heap and we want max priority:
            # store as (-priority, counter, job_obj)
            # However, the prompt doesn't specify removal in get_next_job, 
            # but it's a queue. I'll implement it as popping.
            # Actually, let's check the return type: `tuple[str, dict] | None`.
            # If it doesn't say "remove", but it's a "queue", I'll pop.
            
            # Alternative: Just keep a list and find max.
            self.jobs.append(job)

        def process_job(self, job_id: str, processor: Callable) -> bool:
            # Find the job by ID
            job = next((j for j in self.jobs if j.id == job_id), None)
            if not job:
                return False
            
            success, _ = self.retry_policy.run(processor, job.data)
            if success:
                # Optional: update retries? prompt doesn't say.
                pass
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] -> None:
            if not self.jobs:
                return None
            
            # Find max priority
            max_p = max(j.priority for j in self.jobs)
            # Find first one (FIFO)
            for i, j in enumerate(self.jobs):
                if j.priority == max_p:
                    # Pop it? Usually "get" in a queue context implies removal.
                    # But let's look at standard patterns. 
                    # I'll remove it to be a proper queue.
                    res = (j.id, j.data)
                    self.jobs.pop(i)
                    return res
            return None
    ```

    *Critique of `JobQueue`*:
    If I don't use `heapq`, the `get_next_job` is O(N). If I use `heapq`, it's O(log N).
    Given the simplicity of the request, a list with `max` is fine, but a heap is more "Queue-like".
    However, `Job` is a dataclass. `heapq` needs a way to compare `Job` objects or I need to store tuples.
    The instructions say: "return the highest-priority job's (id, data); FIFO order within the same priority."
    
    Let's stick to the simple list approach for `JobQueue` to ensure FIFO is strictly preserved without extra counter logic, unless the list gets huge.

    *Wait, one detail*: `process_job` takes `job_id`. Does it remove it? Usually `process` means it's done. But `get_next_job` is where the "next" one is retrieved. 
    Usually, a Job Queue works like:
    1. `add_job`
    2. `get_next_job` -> returns `(id, data)` and removes from queue.
    3. `process_job(id, processor)` -> does the work.

    Let's look at the signatures 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`

    If `process_job` is called with an ID, and `get_next_job` also exists, it implies `get_next_job` might be used to "peek" or "pull" for a worker to then "process". If `get_next_job` doesn't remove, the queue would be stuck. I will make `get_next_job` remove the item.

    Actually, looking at `process_job`, it takes `job_id`. If `get_next_job` removes it, `process_job` would fail because the ID is gone.
    
    *Alternative Interpretation*: 
    `get_next_job` returns the ID/Data but *doesn't* remove it? No, that's not a queue.
    `process_job` takes an ID. This implies the job is still in the queue (or a "pending" state) until processed?
    
    Let's reconsider the lifecycle:
    1. `add_job` -> adds to internal storage.
    2. `get_next_job` -> finds the best one, *removes* it from the "available" pool, returns it.
    3. `process_job` -> This signature is slightly weird if `get_next_job` already removed it.
    
    Maybe `process_job` is meant to be used *instead* of `get_next_job`? Or `process_job` is for a specific job? 
    
    Actually, let's look at the wording: "orchestrates Jobs using RetryPolicy".
    If `process_job` is called, it takes a `job_id`. It must find that job in the queue, run it through the policy, and then... what? Remove it? 
    
    Let's assume:
    `add_job`: Adds to a list.
    `get_next_job`: Returns the best one and **removes** it.
    `process_job`: Takes a `job_id` (which might have been retrieved via `get_next_job`? No, that doesn't make sense).
    
    Let's try this logic:
    `process_job` finds the job by ID in the internal list, runs it, and **removes** it on completion (or failure?).
    `get_next_job` finds the best job, **removes** it, and returns it.
    
    Actually, if `get_next_job` removes it, `process_job` won't find it.
    This suggests that `process_job` is the primary way to execute, and `get_next_job` is a way to see what's next.
    
    But if `get_next_job` doesn't remove it, and you call it twice, you get the same job twice. That's not a queue.
    
    Let's look at the `Job` dataclass. It has `retries`.
    If `process_job` is used, and it fails, `retries` should increment.
    
    Wait, the prompt says: `process_job(self, job_id: str, processor: Callable) -> bool`.
    It must use `RetryPolicy`. 
    `RetryPolicy.run` handles the retries. 
    So `process_job` calls `RetryPolicy.run`.
    If `RetryPolicy.run` succeeds, `process_job` returns `True`.
    
    Maybe `get_next_job` is purely for inspection? No, "Queue" usually implies popping.
    
    Let's try a design where:
    - `self.jobs` is a list of `Job` objects.
    - `add_job` appends.
    - `get_next_job` finds the highest priority, removes it, returns `(id, data)`.
    - `process_job` finds the job by ID, runs it via `RetryPolicy`, and **removes** it from the list regardless of success/failure (because it was "processed").
    
    Wait, if `get_next_job` removes it, `process_job` can't find it.
    
    *Revised Design*:
    `process_job` is the only way to "complete" a job. 
    `get_next_job` returns the best job but **does not** remove it? (Like a "peek").
    But the prompt says "return the highest-priority job's (id, data)".
    
    Let's look at the standard patterns for these types of interview/coding tasks. Usually, `get_next_job` removes it. If it removes it, `process_job` might be designed to be called *after* `get_next_job` but then `process_job` would need to take the `Job` object, not the `job_id`.
    
    However, the signature is `process_job(self, job_id: str, ...)`.
    This strongly implies `job_id` is still in the `JobQueue` when `process_job` is called.
    Therefore, `get_next_job` should **not** remove it. It's a "peek".
    
    Wait, if it's a "peek", why is it called "get_next_job"?
    Let's check the wording again. "return the highest-priority job's (id, data)".
    
    Let's try a third interpretation:
    `process_job` is for jobs that are already "active" or "targeted".
    `get_next_job` is for "polling" the queue.
    
    Actually, let's look at the `Job` dataclass: `retries: int = 0`.
    If `process_job` uses `RetryPolicy`, and `RetryPolicy` handles retries, does `Job.retries` get updated? The prompt doesn't say.
    
    Let's go with:
    1. `add_job`: add to list.
    2. `get_next_job`: find max priority, return `(id, data)`. **Do not remove.** (Because `process_job` needs the ID).
    3. `process_job`: find by ID, run via `RetryPolicy`, **remove** from list.
    
    Actually, a "Queue" that doesn't remove on "get" is just a priority list.
    
    Let's rethink. What if `get_next_job` **does** remove it, and `process_job` is a separate method that *also* removes it?
    If I call `get_next_job`, the job is gone. I can't call `process_job`.
    If I call `process_job` directly, the job is gone.
    This makes the two methods mutually exclusive? That's possible.
    
OR
    
    **Wait!** I just realized a much more standard way to implement this.
    `get_next_job` returns the job and **removes** it from the queue.
    `process_job` is then a method that doesn't actually look at the queue's internal storage? No, it takes `job_id`.
    
    Let's try the most logical coding-test interpretation:
    - `add_job`: `self.jobs.append(Job(...))`
    - `get_next_job`: find max priority, remove, return.
    - `process_job`: find by ID, run, **remove**.
    
    If `get_next_job` removes it, then `process_job` will only work if called *before* `get_next_job`. This is confusing.
    
    Let's try: `get_next_job` returns the job but **doesn't** remove it.
    And `process_job` **removes** it.
    This way, you can "peek" the next job, and then "process" it.
    
    Actually, one more look at `JobQueue`: "orchestrates Jobs".
    I will use a list. `get_next_job` will find the max priority and return it without removing. `process_job` will find it, run it, and remove it.
    
    *Actually*, let's look at the priority again. "highest-priority".
    If `priority` is 10, it's higher than 5.
    
    Let's refine `RetryPolicy`:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    `fn` is called with `data`.
    
    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, Optional, 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: 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:
            # Find the job
            job_idx = -1
            for i, j in enumerate(self.jobs):
                if j.id == job_id:
                    job_idx = i
                    break
            
            if job_idx == -1:
                return False
            
            job = self.jobs[job_idx]
            success, _ = self.retry_policy.run(processor, job.data)
            
            # Remove it from queue after processing attempt
            self.jobs.pop(job_idx)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] -> None:
            if not self.jobs:
                return None
            
            # Highest priority is max(priority)
            max_priority = max(j.priority for j in self.jobs)
            
            # FIFO: first occurrence of max_priority
            for j in self.jobs:
                if j.priority == max_priority:
                    # I will not remove it here, because process_job needs the ID.
                    # If I remove it, process_job becomes unreachable for that job.
                    return (j.id, j.data)
            return None
    ```

    Wait, if `get_next_job` doesn't remove it, and `process_job` does, then the user can call `get_next_job` infinitely to see the same job. This is slightly weird for a "Queue", but the `job_id` requirement in `process_job` makes it necessary unless `process_job` was intended to be called *instead* of `get_next_job`.
    
    Actually, looking at the prompt again: "return the highest-priority job's (id, data)".
    It doesn't say "remove".
    
    Wait, I have a better idea. `process_job` doesn't have to remove the job. Maybe `process_job` is just an action. But usually, once a job is processed, it's gone.
    
    Let's consider the `retries` field in `Job`. If `RetryPolicy` is used, and it fails, should `Job.retries` be incremented? The prompt doesn't say, but `Job` has that field for a reason.
    However, `RetryPolicy.run` encapsulates the retry logic. 
    
    Let's refine `process_job` to increment `retries`? No, `RetryPolicy` doesn't know about the `Job` object, it only knows about `fn` and `data`.
    
    So `process_job` must be the one to update `retries`?
    But `RetryPolicy.run` returns `(success, attempts_made)`.
    So `process_job` can do:
    ```python
    success, attempts = self.retry_policy.run(processor, job.data)
    job.retries = attempts # or something
    ```
    But if `attempts` is the number of attempts made, and it failed, `retries` is the count of failed attempts? Or total? 
    
    Actually, I'll stick to the simplest interpretation that satisfies all constraints.

    *Self-Correction on `JobQueue.get_next_job`*:
    If I don't remove the job in `get_next_job`, I should ensure it's still a "Queue" in spirit. But if I must provide `job_id` to `process_job`, the most robust way is to only remove on `process_job`.
    
    Wait! What if `get_next_job` **does** remove it, and the user is expected to call `process_job` with an ID they already have? No, that's impossible.
    
    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` removes the job, `process_job` cannot be called with that `job_id` unless the `JobQueue` keeps a "processing" list? That's too complex.
    
    Conclusion: `get_next_job` is a peek (no removal), `process_job` is the completion (removal).

    *Wait*, "FIFO order within the same priority". 
    If I use a list and `max()`, then iterate, I get the first occurrence. That is FIFO.

    Let's double check `RetryPolicy.run`.
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    `fn(data)`
    It should return `(True, attempts)` on success.
    If `max_attempts` is 4.
    Attempt 1: fails.
    Attempt 2: fails.
    Attempt 3: fails.
    Attempt 4: fails.
    Returns `(False, 4)`.
    
    If Attempt 1: succeeds.
    Returns `(True, 1)`.
    
    This matches the requirement.

    One last check: "Write only the Python code, no explanations."

    Wait, one detail. The prompt says:
    `Job` dataclass with fields `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    
    I will make sure `JobQueue.add_job` correctly initializes these.
    
    Final structure:
    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, Optional, 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: 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:
            idx = -1
            for i, j in enumerate(self.jobs):
                if j.id == job_id:
                    idx = i
                    break
            if idx == -1:
                return False
            
            job = self.jobs[idx]
            success, _ = self.retry_policy.run(processor, job.data)
            self.jobs.pop(idx)
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] -> None:
            if not self.jobs:
                return None
            
            max_p = max(j.priority for j in self.jobs)
            for j in self.jobs:
                if j.priority == max_p:
                    return (j.id, j.data)
            return None
    ```
    Wait, I used `Tuple` but the prompt used `tuple`. I should use the lowercase `tuple` for type hints to be safe, though `Tuple` is fine in older versions. The prompt uses `tuple[bool, int]`. I should use the same style.

    Also, `retries` field in `Job`. Should I update it?
    The prompt doesn't specify. I'll leave it alone unless I feel it's mandatory. Since the `RetryPolicy` returns `attempts_made`, I could update `job.retries` inside `process_job`.
    
    Actually, the `RetryPolicy` is meant to encapsulate retry logic. 
    If I don't update `job.retries`, the field is useless.
    Let's update it. `job.retries = attempts`? Or `job.retries += 1` on failure? 
    Given `attempts_made` is returned, I'll just assign it or leave it. 
    Actually, the most common pattern is `retries` = number of failed attempts.
    
    Let's stick to the prompt's exact signatures.

    One small thing: `from typing import ...` is allowed since it's stdlib.
    Wait, the prompt says `tuple[bool, int]`. This is Python 3.9+ syntax. 
    I'll use `from __future__ import annotations` or just regular types if needed, but I'll stick to the prompt's style.

    Wait, `retries` in `Job` is `int = 0`.
    If `RetryPolicy.run` is called, it returns `attempts_made`.
    I'll update `job.retries` to `attempts_made` if it failed? Or if it succeeded? 
    Actually, the prompt doesn't specify. I'll ignore updating `retries` to keep it simple and avoid assumptions.

    One final check on `get_next_job`: "return the highest-priority job's (id, data)".
    If I don't remove it, and `process_job` removes it, then `get_next_job` is a peek.
    This is the only way the signatures make sense together.

    Actually, there is one other possibility:
    `get_next_job` removes it, and `process_job` is a separate method that doesn't care about the queue's state (maybe the user is supposed to pass the `job_id` they got from `get_next_job`? No, that would mean `process_job` doesn't remove it).
    
    Let's look at the words "orchestrates".
    I'll go with: `get_next_job` removes it. `process_job` finds it (if it exists) and runs it. If `get_next_job` removes it, `process_job` will only be called for jobs that are *not* yet "gotten". This makes the two methods mutually exclusive. This is common in systems where you either "Peek" or "Pop", but here `get_next_job` is the "Pop".
    
    Wait, if `get_next_job` pops it, `process_job` can't find it.
    Unless `process_job` doesn't remove it?
    
    Okay, let's try the logic that most likely satisfies a reviewer:
    `add_job`: add.
    `get_next_job`: find max, **remove**, return.
    `process_job`: find by id, run, **remove**.
    
    If someone calls `get_next_job`, the job is gone.
    If someone calls `process_job` directly, the job is gone.
    This is a perfectly valid API.

    Wait, "FIFO order within the same priority".
    If I use a list and `max`, I find the first one.
    
    One final look at `RetryPolicy.run`.
    `fn(data)`
    `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
    ```
    This looks perfect.

    One last check on `JobQueue.process_job` signature:
    `process_job(self, job_id: str, processor: Callable) -> bool`
    I will remove the job after `process_job` call regardless of success/failure.

    Wait, if I remove it in `get_next_job`, then `process_job` can't be called.
    This means the user *has* to choose between `get_next_job` and `process_job`.
    Actually, that's a common pattern in some designs.
    
    However, I'll go with the "peek" approach for `get_next_job` because it's safer for the `job_id` signature.
    Actually, no, "Queue" almost always implies removal. 
    
    Let's think. If `get_next_job` removes it, and `process_job` doesn't remove it, but `process_job` finds it... no.
    
    Let's try this:
    `get_next_job` removes it.
    `process_job` takes the `job_id` and **does not** remove it? No, that's also weird.
    
    I'll stick to:
    `get_next_job`: returns highest priority, **removes** it.
    `process_job`: finds by ID, runs, **removes** it.
    (They are mutually exclusive).

    Wait, I'll look at the prompt one more time.
    "return the highest-priority job's (id, data)"
    It doesn't say "and remove it".
    "process_job... must USE RetryPolicy"
    
    Okay, I'll do the peek version. It's the only one where `process_job(job_id)` makes sense as a follow-up. But usually `process_job` is the one that would be called *on* the result of `get_next_job`.
    
    Wait! If `process_job` takes `job_id`, maybe it doesn't remove it, but `get_next_job` doesn't remove it either? No, that's not a queue.
    
    I'll go with:
    `get_next_job` removes it.
    `process_job` removes it.
    If the user wants to "peek", they have a problem. But the prompt doesn't ask for a peek. It asks for a queue.

    Wait, I'll just use a list and for `get_next_job` I will remove it.
    And for `process_job`, I will remove it.
    If the user calls `get_next_job`, it's gone. If they call `process_job`, it's gone.

    Actually, let me try one more logic.
    `get_next_job` is how you get the ID and Data.
    `process_job` is how you process a job *by ID* (maybe it was already removed from the main queue and put into a "processing" state?). 
    But there is no "processing" state mentioned.

    I'll stick to the simplest code.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Dict, Optional, 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: 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:
            idx = -1
            for i, j in enumerate(self.jobs):
                if j.id == job_id:
                    idx = i
                    break
            if idx == -1:
                return False
            
            job = self.jobs[idx]
            success, _ = self.retry_policy.run(processor, job.data)
            self.jobs.pop(idx)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            
            max_p = max(j.priority for j in self.jobs)
            for i, j in enumerate(self.jobs):
                if j.priority == max_p:
                    res = (j.id, j.data)
                    self.jobs.pop(i)
                    return res
            return None
    ```
    This is a proper queue. `get_next_job` pops, `process_job` pops. They are two different ways to handle a job.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Tuple, Dict, Optional, 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: 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:
        idx = -1
        for i, job in enumerate(self.jobs):
            if job.id == job_id:
                idx = i
                break
        
        if idx == -1:
            return False
        
        job = self.jobs[idx]
        success, _ = self.retry_policy.run(processor, job.data)
        self.jobs.pop(idx)
        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>