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

class JobQueue:
    """
    A job queue system that supports retry logic and exponential backoff.
    """
    def __init__(self):
        # Stores job data: {job_id: job_data}
        self.jobs: Dict[str, Any] = {}
        # Tracks processing state/metadata: {job_id: {"attempts": int}}
        self.job_metadata: Dict[str, Dict[str, Any]] = {}
        self.MAX_RETRIES = 3

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

    def process_job(self, job_id: str, processor: Callable[[Dict[str, Any]], Any]) -> 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 if all retries were exhausted.
        """
        if job_id not in self.jobs:
            raise KeyError(f"Job ID {job_id} not found in the queue.")

        metadata = self.job_metadata[job_id]
        
        # Start attempts from 1 (0 attempts means 1st try)
        for attempt in range(1, self.MAX_RETRIES + 1):
            metadata["attempts"] = attempt
            
            try:
                print(f"--- Processing Job {job_id}: Attempt {attempt}")
                processor(self.jobs[job_id])
                print(f"--- Job {job_id} succeeded on attempt {attempt}.")
                return True
            
            except Exception as e:
                print(f"--- Job {job_id} failed on attempt {attempt}: {e}")
                
                if attempt >= self.MAX_RETRIES:
                    print(f"--- Job {job_id} failed permanently after {self.MAX_RETRIES} attempts.")
                    return False

                # Calculate exponential backoff delay (1s, 2s, 4s, ...)
                # Delay = 2^(attempt - 1) seconds
                delay = 2**(attempt - 1)
                print(f"--- Retrying Job {job_id} in {delay}s (simulated backoff).")
                
                # Simulation of sleep/waiting
                # time.sleep(delay) 
                pass 

        return False # Should theoretically be unreachable if MAX_RETRIES is handled correctly inside the loop

if __name__ == '__main__':
    # --- Test Case 1: Job succeeds on the first attempt ---
    print("\n=============================================")
    print("TEST CASE 1: SUCCESS ON FIRST TRY")
    print("=============================================")
    queue1 = JobQueue()
    queue1.add_job("job_success_1", {"url": "http://example.com/success"})

    def fetch_url_success(data):
        print(f"  [Processor] Fetching {data['url']}...")
        # Simulate success
        pass

    result1 = queue1.process_job("job_success_1", fetch_url_success)
    print(f"\nFinal Result 1: {'SUCCESS' if result1 else 'FAILURE'}")


    # --- Test Case 2: Job fails and succeeds on the third attempt ---
    print("\n=============================================")
    print("TEST CASE 2: SUCCESS ON THIRD TRY (2 retries)")
    print("=============================================")
    queue2 = JobQueue()
    queue2.add_job("job_success_3", {"url": "http://example.com/retry"})

    attempt_count = 0
    def fetch_url_retry(data):
        nonlocal attempt_count
        attempt_count += 1
        if attempt_count < 3:
            raise ConnectionError("Network connection failed.")
        print(f"  [Processor] Successful connection established after {attempt_count} attempts.")
        # Simulate success
        pass

    result2 = queue2.process_job("job_success_3", fetch_url_retry)
    print(f"\nFinal Result 2: {'SUCCESS' if result2 else 'FAILURE'}")


    # --- Test Case 3: Job fails and exhausts all retries ---
    print("\n=============================================")
    print("TEST CASE 3: FAILURE AFTER MAX RETRIES")
    print("=============================================")
    queue3 = JobQueue()
    queue3.add_job("job_fail_all", {"url": "http://example.com/fail"})

    attempt_count = 0
    def fetch_url_fail(data):
        nonlocal attempt_count
        attempt_count += 1
        raise TimeoutError("Service timeout.")

    result3 = queue3.process_job("job_fail_all", fetch_url_fail)
    print(f"\nFinal Result 3: {'SUCCESS' if result3 else 'FAILURE'}")
```