```python
import heapq
from typing import Optional, Tuple, Dict, Any

class JobQueue:
    """
    A priority-based job queue.
    Higher priority values (2 > 1 > 0) are processed first.
    FIFO order is maintained for jobs with the same priority.
    """
    def __init__(self):
        # The heap stores tuples: (-priority, insertion_index, job_name, job_data)
        # Using negative priority makes the min-heap behave like a max-priority queue
        # for priority levels (e.g., -2 is smaller than -1).
        self._queue: list[Tuple[int, int, str, Dict[str, Any]]] = []
        self._counter = 0  # Used to ensure FIFO order for jobs of the same priority

    def add_job(self, job_name: str, job_data: Dict[str, Any], priority: int = 0) -> None:
        """
        Adds a job to the queue with a specified priority.
        Priority levels: 0 (Normal), 1 (High), 2 (Critical).
        """
        # The counter ensures that if priorities are the same, the job added earlier
        # (smaller counter value) is retrieved first (FIFO).
        entry = (-priority, self._counter, job_name, job_data)
        heapq.heappush(self._queue, entry)
        self._counter += 1

    def get_next_job(self) -> Optional[Tuple[str, Dict[str, Any]]]:
        """
        Retrieves and removes the highest priority job from the queue.

        Returns:
            tuple[str, dict] | None: (job_name, job_data) or None if the queue is empty.
        """
        if not self._queue:
            return None

        # Pop the smallest element (which corresponds to the highest priority/earliest insertion)
        _, _, job_name, job_data = heapq.heappop(self._queue)
        return (job_name, job_data)

    def is_empty(self) -> bool:
        """Checks if the queue is empty."""
        return not self._queue

if __name__ == '__main__':
    # Example 1: Standard Priority Test
    queue = JobQueue()
    print("--- Running Standard Priority Test ---")
    
    # FIFO test (normal jobs added first)
    queue.add_job("normal_a", {"type": "normal"}, priority=0)
    queue.add_job("normal_b", {"type": "normal"}, priority=0)

    # Critical job (highest priority)
    queue.add_job("critical", {"type": "critical"}, priority=2)

    # High priority job
    queue.add_job("high", {"type": "high"}, priority=1)
    
    # Check order: Critical (2) -> High (1) -> Normal A (0, FIFO) -> Normal B (0, FIFO)
    
    print(f"Next job: {queue.get_next_job()}")  # Expected: critical
    print(f"Next job: {queue.get_next_job()}")  # Expected: high
    print(f"Next job: {queue.get_next_job()}")  # Expected: normal_a
    print(f"Next job: {queue.get_next_job()}")  # Expected: normal_b
    print(f"Next job: {queue.get_next_job()}")  # Expected: None

    # Example 2: FIFO Tie Breaker Test
    queue_fifo = JobQueue()
    print("\n--- Running FIFO Tie Breaker Test ---")
    
    # All priority 1
    queue_fifo.add_job("p1_first", {"type": "p1"}, priority=1)
    queue_fifo.add_job("p1_second", {"type": "p1"}, priority=1)
    queue_fifo.add_job("p1_third", {"type": "p1"}, priority=1)

    # Check order: p1_first -> p1_second -> p1_third
    
    print(f"Next job: {queue_fifo.get_next_job()}")  # Expected: p1_first
    print(f"Next job: {queue_fifo.get_next_job()}")  # Expected: p1_second
    print(f"Next job: {queue_fifo.get_next_job()}")  # Expected: p1_third
```