```python
import time
from typing import Callable, Dict, Any

class JobQueue:
    """
    A job queue implementation with retry logic and exponential backoff.
    """
    def __init__(self):
        # Stores job metadata: {job_id: {"data": Any, "retries": int}}
        self.jobs: Dict[str, Dict[str, Any]] = {}

    def add_job(self, job_id: str, data: Dict[str, Any]):
        """Adds a new job to the queue."""
        if job_id in self.jobs:
            raise ValueError(f"Job ID {job_id} already exists.")
        self.jobs[job_id] = {"data": data, "retries": 0}
        print(f"[Queue] Job {job_id} added.")

    def process_job(self, job_id: str, processor: Callable) -> bool:
        """
        Processes a job with retry logic and exponential backoff.

        Args:
            job_id: The ID of the job to process.
            processor: The function that executes the job logic.

        Returns:
            True if the job succeeded, False otherwise.
        """
        if job_id not in self.jobs:
            print(f"[Error] Job {job_id} not found.")
            return False

        job_data = self.jobs[job_id]
        max_retries = 3
        
        print(f"\n[Processor] Starting job {job_id}...")

        for attempt in range(max_retries + 1):
            try:
                print(f"  -> Attempt {attempt + 1}...")
                
                # Execute the job processor
                processor(job_data["data"])
                
                print(f"[Success] Job {job_id} completed successfully on attempt {attempt + 1}.")
                return True

            except Exception as e:
                print(f"[Failure] Job {job_id} failed on attempt {attempt + 1}. Error: {type(e).__name__}: {e}")
                
                # Check if we have exhausted all retries
                if attempt >= max_retries:
                    print(f"[Critical] Job {job_id} failed after {max_retries + 1} attempts. Exhausted retries.")
                    return False
                
                # Increment retry count
                job_data["retries"] += 1
                
                # Calculate exponential backoff delay (2^n seconds)
                # Attempt 1 (retry 1): 2^1 = 2s (Wait before next attempt)
                # Attempt 2 (retry 2): 2^2 = 4s
                # Note: Since we are using 'attempt' (0-indexed), the retry counter is attempt + 1
                delay = 2 ** job_data["retries"]
                
                # Simulate delay without actually sleeping
                print(f"  -> Retrying job {job_id} in {delay} seconds (Backoff)...")
                # time.sleep(delay) # Uncomment this line to actually pause execution
                
        return False # Should theoretically be unreachable if the loop logic is correct

# --- Example Usage ---

# 1. Setup a test job that fails a few times before succeeding
class MockProcessor:
    def __init__(self, fail_count):
        self.fail_count = fail_count
        self.attempts = 0

    def __call__(self, data):
        self.attempts += 1
        print(f"    [Processor] Running with data: {data}")
        if self.attempts <= self.fail_count:
            raise ConnectionError("Simulated network failure.")
        print("    [Processor] Success!")

# 2. Setup a job that always fails
class AlwaysFailingProcessor:
    def __call__(self, data):
        raise TimeoutError("Simulated persistent timeout.")

# Initialize the queue
queue = JobQueue()

# --- Test Case 1: Success after retries (Fails 2 times, succeeds on 3rd) ---
print("=========================================")
print("TEST CASE 1: Success after 2 failures")
print("=========================================")
job1_processor = MockProcessor(fail_count=2)
queue.add_job("job1", {"url": "https://api.example.com"})
success_1 = queue.process_job("job1", job1_processor)
print(f"\nFINAL RESULT JOB 1: {'SUCCESS' if success_1 else 'FAILURE'}")


# --- Test Case 2: Exhausted retries (Fails 4 times, max retries is 3) ---
print("\n\n=========================================")
print("TEST CASE 2: Exhausted Retries")
print("=========================================")
job2_processor = AlwaysFailingProcessor()
queue.add_job("job2", {"url": "https://broken.example.com"})
success_2 = queue.process_job("job2", job2_processor)
print(f"\nFINAL RESULT JOB 2: {'SUCCESS' if success_2 else 'FAILURE'}")

# --- Test Case 3: Immediate success ---
print("\n\n=========================================")
print("TEST CASE 3: Immediate Success")
print("=========================================")
job3_processor = MockProcessor(fail_count=0)
queue.add_job("job3", {"url": "https://ok.example.com"})
success_3 = queue.process_job("job3", job3_processor)
print(f"\nFINAL RESULT JOB 3: {'SUCCESS' if success_3 else 'FAILURE'}")
```