```python
import uuid
from collections import deque
from typing import Dict, Any, Optional

class JobQueue:
    """
    A basic FIFO job queue implementation.
    """
    def __init__(self):
        # Queue storing job IDs in FIFO order
        self._job_queue: deque[str] = deque()
        
        # Storage for job data waiting to be processed
        self._pending_jobs: Dict[str, Dict[str, Any]] = {}
        
        # Storage for completed job results
        self._completed_results: Dict[str, Dict[str, Any]] = {}

    def add_job(self, job_id: str, data: Dict[str, Any]) -> str:
        """
        Adds a job to the queue.
        
        Args:
            job_id: The unique identifier for the job.
            data: The payload/data associated with the job.
            
        Returns:
            The job_id.
        """
        if job_id in self._pending_jobs or job_id in self._completed_results:
            raise ValueError(f"Job ID '{job_id}' already exists.")
            
        self._pending_jobs[job_id] = {"data": data, "status": "PENDING"}
        self._job_queue.append(job_id)
        return job_id

    def get_result(self, job_id: str) -> Optional[Dict[str, Any]]:
        """
        Retrieves the result of a completed job.
        
        Args:
            job_id: The unique identifier of the job.
            
        Returns:
            The result data dictionary, or None if the job is not found or not complete.
        """
        return self._completed_results.get(job_id)

    def process_next_job(self) -> Optional[str]:
        """
        Simulates a worker processing the next job in the queue.
        
        Returns:
            The job_id that was processed, or None if the queue is empty.
        """
        if not self._job_queue:
            return None

        job_id = self._job_queue.popleft()
        job = self._pending_jobs.pop(job_id)
        
        # --- Simulation of job processing ---
        print(f"--- Processing Job: {job_id} ---")
        
        # In a real scenario, this would involve running the task defined in job['data']
        try:
            task_data = job['data']
            result = {"status": "SUCCESS", "processed_data": task_data.get("task", "No task specified")}
            
            # Move job to completed results
            self._completed_results[job_id] = result
            print(f"--- Job {job_id} completed successfully. ---")
            return job_id
        except Exception as e:
            # Handle processing failure
            self._completed_results[job_id] = {"status": "FAILED", "error": str(e)}
            print(f"--- Job {job_id} failed: {e} ---")
            return job_id


if __name__ == '__main__':
    queue = JobQueue()

    # 1. Add jobs
    job_id_1 = queue.add_job("job1", {"task": "process_data", "priority": 1})
    job_id_2 = queue.add_job("job2", {"task": "send_email", "recipient": "admin"})
    job_id_3 = queue.add_job("job3", {"task": "cleanup_logs"})

    print(f"\nJobs added: {job_id_1}, {job_id_2}, {job_id_3}")
    print("-" * 30)

    # 2. Process the first job (FIFO)
    print("Starting processing cycle 1...")
    processed_id = queue.process_next_job()
    print(f"Processed ID: {processed_id}")
    
    # Check result before job 2 is processed (should be None)
    result_1_pre = queue.get_result(job_id_2)
    print(f"Result for job2 before processing: {result_1_pre}")
    
    print("-" * 30)

    # 3. Process the second job
    print("Starting processing cycle 2...")
    processed_id = queue.process_next_job()
    print(f"Processed ID: {processed_id}")

    # Check result for job 1 (should be available)
    result_1_post = queue.get_result(job_id_1)
    print(f"Result for job1 after processing: {result_1_post}")

    print("-" * 30)

    # 4. Process the third job
    print("Starting processing cycle 3...")
    processed_id = queue.process_next_job()
    print(f"Processed ID: {processed_id}")

    # Check result for job 3
    result_3 = queue.get_result(job_id_3)
    print(f"Result for job3 after processing: {result_3}")
```