// 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, act, cleanup, fireEvent } from '@testing-library/react';
import Robot from '../../../src/pages/Robot/Robot';
import { API_BASE_URL } from '../../../src/config/api.ts';

// Mock EventSource
const mockInstances: MockEventSource[] = [];
class MockEventSource {
  url: string;
  onmessage: ((event: MessageEvent) => void) | null = null;
  closed = false;

  constructor(url: string) {
    this.url = url;
    mockInstances.push(this);
  }

  sendMessage(data: string) {
    this.onmessage?.({ data } as MessageEvent);
  }

  close() {
    this.closed = true;
  }
}

// Mock global EventSource
beforeAll(() => {
  (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url));
});

// Mock fetch
beforeEach(() => {
  globalThis.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ reply: 'ok' }),
    })
  ) as jest.Mock;
});

// Cleanup
afterEach(() => {
  cleanup();
  jest.restoreAllMocks();
  mockInstances.length = 0;
});

describe('Robot', () => {
  test('renders initial state', () => {
    render(<Robot />);
    expect(screen.getByText('Robot interaction')).toBeInTheDocument();
    expect(screen.getByText('Force robot speech')).toBeInTheDocument();
    expect(screen.getByText('Listening 🔴')).toBeInTheDocument();
    expect(screen.getByPlaceholderText('Enter a message')).toBeInTheDocument();
  });

  test('sends message via button', async () => {
    render(<Robot />);
    const input = screen.getByPlaceholderText('Enter a message');
    const button = screen.getByText('Speak');

    fireEvent.change(input, { target: { value: 'Hello' } });
    await act(async () => fireEvent.click(button));

    expect(globalThis.fetch).toHaveBeenCalledWith(
      `${API_BASE_URL}/message`,
      expect.objectContaining({
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: 'Hello' }),
      })
    );
  });

  test('sends message via Enter key', async () => {
    render(<Robot />);
    const input = screen.getByPlaceholderText('Enter a message');
    fireEvent.change(input, { target: { value: 'Hi Enter' } });

    await act(async () =>
      fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 })
    );

    expect(globalThis.fetch).toHaveBeenCalledWith(
      `${API_BASE_URL}/message`,
      expect.objectContaining({
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: 'Hi Enter' }),
      })
    );
    expect((input as HTMLInputElement).value).toBe('');
  });

  test('handles fetch errors', async () => {
    globalThis.fetch = jest.fn(() => Promise.reject('Network error')) as jest.Mock;
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

    render(<Robot />);
    const input = screen.getByPlaceholderText('Enter a message');
    const button = screen.getByText('Speak');
    fireEvent.change(input, { target: { value: 'Error test' } });

    await act(async () => fireEvent.click(button));

    expect(consoleSpy).toHaveBeenCalledWith(
      'Error sending message: ',
      'Network error'
    );
  });

  test('updates conversation on SSE', async () => {
    render(<Robot />);
    const eventSource = mockInstances[0];

    await act(async () => {
      eventSource.sendMessage(JSON.stringify({ voice_active: true }));
      eventSource.sendMessage(JSON.stringify({ speech: 'User says hi' }));
      eventSource.sendMessage(JSON.stringify({ llm_response: 'Assistant replies' }));
    });

    expect(screen.getByText('Listening 🟢')).toBeInTheDocument();
    expect(screen.getByText('User says hi')).toBeInTheDocument();
    expect(screen.getByText('Assistant replies')).toBeInTheDocument();
  });

  test('handles invalid SSE JSON', async () => {
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    render(<Robot />);
    const eventSource = mockInstances[0];

    await act(async () => eventSource.sendMessage('bad-json'));

    expect(logSpy).toHaveBeenCalledWith('Unparsable SSE message:', 'bad-json');
  });

  test('resets conversation with Reset button', async () => {
    render(<Robot />);
    const eventSource = mockInstances[0];

    await act(async () =>
      eventSource.sendMessage(JSON.stringify({ speech: 'Hello' }))
    );
    expect(screen.getByText('Hello')).toBeInTheDocument();

    fireEvent.click(screen.getByText('Reset'));
    expect(screen.queryByText('Hello')).not.toBeInTheDocument();
  });

  test('toggles conversationIndex with Stop/Start button', () => {
    render(<Robot />);
    const stopButton = screen.getByText('Stop');
    fireEvent.click(stopButton);
    expect(screen.getByText('Start')).toBeInTheDocument();

    fireEvent.click(screen.getByText('Start'));
    expect(screen.getByText('Stop')).toBeInTheDocument();
  });

  test('closes EventSource on unmount', () => {
    const { unmount } = render(<Robot />);
    const eventSource = mockInstances[0];
    const closeSpy = jest.spyOn(eventSource, 'close');

    unmount();
    expect(closeSpy).toHaveBeenCalled();
    expect(eventSource.closed).toBe(true);
  });
});
