// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import MonitoringPage from '../../../src/pages/MonitoringPage/MonitoringPage';
import useProgramStore from '../../../src/utils/programStore';
import * as MonitoringAPI from '../../../src/pages/MonitoringPage/MonitoringPageAPI';
import * as VisProg from '../../../src/pages/VisProgPage/VisProgLogic';

// --- Mocks ---

// Mock the Zustand store
jest.mock('../../../src/utils/programStore', () => ({
  __esModule: true,
  default: jest.fn(),
}));

// Mock the API layer including hooks
jest.mock('../../../src/pages/MonitoringPage/MonitoringPageAPI', () => ({
  nextPhase: jest.fn(),
  resetPhase: jest.fn(),
  pauseExperiment: jest.fn(),
  playExperiment: jest.fn(),
  // We mock these to capture the callbacks and trigger them manually in tests
  useExperimentLogger: jest.fn(),
  useStatusLogger: jest.fn(),
}));

// Mock VisProg functionality
jest.mock('../../../src/pages/VisProgPage/VisProgLogic', () => ({
  graphReducer: jest.fn(),
  runProgram: jest.fn(),
}));

// Mock Child Components to reduce noise (optional, but keeps unit test focused)
// For this test, we will allow them to render to test data passing, 
// but we mock RobotConnected as it has its own side effects
jest.mock('../../../src/pages/MonitoringPage/MonitoringPageComponents', () => {
  const original = jest.requireActual('../../../src/pages/MonitoringPage/MonitoringPageComponents');
  return {
    ...original,
    RobotConnected: () => <div data-testid="robot-connected-mock">Robot Status</div>,
  };
});

describe('MonitoringPage', () => {
  // Capture stream callbacks
  let streamUpdateCallback: (data: any) => void;
  let statusUpdateCallback: (data: any) => void;

  // Setup default store state
  const mockGetPhaseIds = jest.fn();
  const mockGetPhaseNames = jest.fn();
  const mockGetNorms = jest.fn();
  const mockGetGoals = jest.fn();
  const mockGetGoalsWithDepth = jest.fn();
  const mockGetTriggers = jest.fn();
  const mockSetProgramState = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks();

    // Default Store Implementation
    (useProgramStore as unknown as jest.Mock).mockImplementation((selector) => {
      const state = {
        getPhaseIds: mockGetPhaseIds,
        getPhaseNames: mockGetPhaseNames,
        getNormsInPhase: mockGetNorms,
        getGoalsInPhase: mockGetGoals,
        getTriggersInPhase: mockGetTriggers,
        getGoalsWithDepth: mockGetGoalsWithDepth,
        setProgramState: mockSetProgramState,
      };
      return selector(state);
    });

    // Capture the hook callbacks
    (MonitoringAPI.useExperimentLogger as jest.Mock).mockImplementation((cb) => {
      streamUpdateCallback = cb;
    });
    (MonitoringAPI.useStatusLogger as jest.Mock).mockImplementation((cb) => {
      statusUpdateCallback = cb;
    });

    // Default mock return values
    mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
    mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
    mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1'}, { id: 'g2', name: 'Goal 2'}]);
    mockGetGoalsWithDepth.mockReturnValue([
        { id: 'g1', name: 'Goal 1', level: 0 }, 
        { id: 'g2', name: 'Goal 2', level: 0 }
    ]);
    mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
    mockGetNorms.mockReturnValue([
        { id: 'n1', norm: 'Norm 1', condition: null }, 
        { id: 'cn1', norm: 'Cond Norm 1', condition: 'some-cond' }
    ]);
  });

  test('renders "No program loaded" when phaseIds are empty', () => {
    mockGetPhaseIds.mockReturnValue([]);
    render(<MonitoringPage />);
    expect(screen.getByText('No program loaded.')).toBeInTheDocument();
  });

  test('renders the dashboard with initial state', () => {
    render(<MonitoringPage />);
    
    // Check Header
    expect(screen.getByText('Phase 1:')).toBeInTheDocument();
    expect(screen.getByText('Intro')).toBeInTheDocument();
    
    // Check Lists
    expect(screen.getByText(/Goal 1/)).toBeInTheDocument(); 
    
    expect(screen.getByText('Trigger 1')).toBeInTheDocument();
    expect(screen.getByText('Norm 1')).toBeInTheDocument();
    expect(screen.getByText('Cond Norm 1')).toBeInTheDocument();
  });

  describe('Control Buttons', () => {
    test('Pause calls API and updates UI', async () => {
      render(<MonitoringPage />);
      const pauseBtn = screen.getByRole('button', { name: /pause/i });

      await act(async () => {
        fireEvent.click(pauseBtn);
      });

      expect(MonitoringAPI.pauseExperiment).toHaveBeenCalled();
      // Ensure local state toggled (we check if play button is now inactive style or pause active)
    });

    test('Play calls API and updates UI', async () => {
      render(<MonitoringPage />);
      const playBtn = screen.getByRole('button', { name: /play/i });

      await act(async () => {
        fireEvent.click(playBtn);
      });

      expect(MonitoringAPI.playExperiment).toHaveBeenCalled();
    });

    test('Next Phase calls API', async () => {
      render(<MonitoringPage />);
      await act(async () => {
        fireEvent.click(screen.getByRole('button', { name: /next phase/i }));
      });
      expect(MonitoringAPI.nextPhase).toHaveBeenCalled();
    });

    test('Reset Experiment calls logic and resets state', async () => {
      render(<MonitoringPage />);

      // Mock graph reducer return
      (VisProg.graphReducer as jest.Mock).mockReturnValue([{ id: 'new-phase' }]);

      await act(async () => {
        fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
      });

      expect(VisProg.graphReducer).toHaveBeenCalled();
      expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
      expect(VisProg.runProgram).toHaveBeenCalled();
    });

    test('Reset Experiment handles errors gracefully', async () => {
        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
        (VisProg.runProgram as jest.Mock).mockRejectedValue(new Error('Fail'));

        render(<MonitoringPage />);
        await act(async () => {
            fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
        });

        expect(consoleSpy).toHaveBeenCalledWith('Failed to reset program:', expect.any(Error));
        consoleSpy.mockRestore();
    });
  });

  describe('Stream Updates (useExperimentLogger)', () => {
    test('Handles phase_update to next phase', () => {
      render(<MonitoringPage />);
      
      expect(screen.getByText('Intro')).toBeInTheDocument(); // Phase 0

      act(() => {
        streamUpdateCallback({ type: 'phase_update', id: 'phase-2' });
      });

      expect(screen.getByText('Main')).toBeInTheDocument(); // Phase 1
    });

    test('Handles phase_update to "end"', () => {
      render(<MonitoringPage />);
      
      act(() => {
        streamUpdateCallback({ type: 'phase_update', id: 'end' });
      });

      expect(screen.getByText('Experiment finished')).toBeInTheDocument();
      expect(screen.getByText('All phases have been successfully completed.')).toBeInTheDocument();
    });

    test('Handles phase_update with unknown ID gracefully', () => {
      render(<MonitoringPage />);
      act(() => {
        streamUpdateCallback({ type: 'phase_update', id: 'unknown-phase' });
      });
      // Should remain on current phase
      expect(screen.getByText('Intro')).toBeInTheDocument();
    });

    test('Handles goal_update: advances index and marks previous as achieved', () => {
      render(<MonitoringPage />);
      
      // Initial: Goal 1 (index 0) is current.
      // Send update for Goal 2 (index 1).
      act(() => {
        streamUpdateCallback({ type: 'goal_update', id: 'g2' });
      });

      // Goal 1 should now be marked achieved (passed via activeIds)
      // Goal 2 should be current.
      
      // We can inspect the "StatusList" props implicitly by checking styling or indicators if not mocked,
      // but since we render the full component, we check the class/text.
      // Goal 1 should have checkmark (override logic puts checkmark for activeIds)
      // The implementation details of StatusList show ✔️ for activeIds.
      
      const items = screen.getAllByRole('listitem');
      // Helper to find checkmarks within items
      expect(items[0]).toHaveTextContent('Goal 1');
      // After update, g1 is active (achieved), g2 is current
      // logic: loop i < gIndex (1). activeIds['g1'] = true.
    });
    
    test('Handles goal_update with unknown ID', () => {
        const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
        
        render(<MonitoringPage />);
        act(() => {
            streamUpdateCallback({ type: 'goal_update', id: 'unknown-goal' });
        });

        expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Goal unknown-goal not found'));
        warnSpy.mockRestore();
    });

    test('Handles trigger_update', () => {
      render(<MonitoringPage />);
      
      // Trigger 1 initially not achieved
      act(() => {
        streamUpdateCallback({ type: 'trigger_update', id: 't1', achieved: true });
      });

      // StatusList logic: if activeId is true, show ✔️
      // We look for visual confirmation or check logic
      const triggerList = screen.getByText('Triggers').parentElement;
      expect(triggerList).toHaveTextContent('✔️'); // Assuming 't1' is the only trigger
    });
  });

  describe('Status Updates (useStatusLogger)', () => {
    test('Handles cond_norms_state_update', () => {
      render(<MonitoringPage />);
      
      // Initial state: activeIds empty.
      act(() => {
        statusUpdateCallback({ 
            type: 'cond_norms_state_update', 
            norms: [{ id: 'cn1', active: true }] 
        });
      });

      // Conditional Norm 1 should now be active
      const cnList = screen.getByText('Conditional Norms').parentElement;
      expect(cnList).toHaveTextContent('✔️');
    });

    test('Ignores status update if no changes detected', () => {
       render(<MonitoringPage />);
       // First update
       act(() => {
           statusUpdateCallback({ type: 'cond_norms_state_update', norms: [{ id: 'cn1', active: true }] });
       });
       
       // Second identical update - strictly checking if this causes a rerender is hard in RTL,
       // but we ensure no errors and state remains consistent.
       act(() => {
           statusUpdateCallback({ type: 'cond_norms_state_update', norms: [{ id: 'cn1', active: true }] });
       });
       
       const cnList = screen.getByText('Conditional Norms').parentElement;
       expect(cnList).toHaveTextContent('✔️');
    });
  });
});

