// src/pages/VoiceLabPage.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { VoiceLabPage } from './VoiceLabPage';
import * as aiApiClient from '../services/aiApiClient';
import { notifyError } from '../services/notificationService';
import { logger } from '../services/logger.client';

// Extensive logging for debugging
const LOG_PREFIX = '[TEST DEBUG]';

// The aiApiClient, notificationService, and logger are mocked globally.
// We can get a typed reference to the aiApiClient for individual test overrides.
const mockedAiApiClient = vi.mocked(aiApiClient);

// Define mock at module level so it can be referenced in the implementation
const mockAudioPlay = vi.fn(() => {
  console.log(`${LOG_PREFIX} mockAudioPlay executed`);
  return Promise.resolve();
});

describe('VoiceLabPage', () => {
  beforeEach(() => {
    console.log(`${LOG_PREFIX} beforeEach: Cleaning mocks and setting up Audio mock`);
    vi.clearAllMocks();
    mockAudioPlay.mockClear();

    // Mock the global Audio constructor
    // We use a robust mocking strategy to ensure it overrides JSDOM's Audio
    const AudioMock = vi.fn(function (url: string) {
      console.log(`${LOG_PREFIX} Audio constructor called with URL:`, url);
      return {
        play: mockAudioPlay,
        pause: vi.fn(),
        addEventListener: vi.fn(),
        removeEventListener: vi.fn(),
        src: url,
      };
    });

    // Stub global Audio
    vi.stubGlobal('Audio', AudioMock);

    // Forcefully overwrite window.Audio using defineProperty to bypass potential JSDOM read-only restrictions
    Object.defineProperty(window, 'Audio', {
      writable: true,
      value: AudioMock,
    });
  });

  afterEach(() => {
    console.log(`${LOG_PREFIX} afterEach: unstubbing globals`);
    vi.unstubAllGlobals();
  });

  it('should render the initial state correctly', () => {
    console.log(`${LOG_PREFIX} Test: render initial state`);
    render(<VoiceLabPage />);
    expect(screen.getByRole('heading', { name: /admin voice lab/i })).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: /text-to-speech generation/i })).toBeInTheDocument();
    expect(screen.getByRole('textbox')).toHaveValue(
      'Hello! This is a test of the text-to-speech generation.',
    );
    expect(screen.getByRole('button', { name: /generate & play/i })).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /replay/i })).not.toBeInTheDocument();
    console.log(`${LOG_PREFIX} Test: render initial state passed`);
  });

  describe('Text-to-Speech Generation', () => {
    it('should call generateSpeechFromText and play audio on success', async () => {
      console.log(`${LOG_PREFIX} Test: generateSpeechFromText success flow`);
      const mockBase64Audio = 'mock-audio-data';
      mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
        json: async () => {
          console.log(`${LOG_PREFIX} Mock response.json() called`);
          return mockBase64Audio;
        },
      } as Response);

      render(<VoiceLabPage />);

      const generateButton = screen.getByRole('button', { name: /generate & play/i });
      console.log(`${LOG_PREFIX} Clicking generate button`);
      fireEvent.click(generateButton);

      // Check for loading state
      expect(generateButton).toBeDisabled();

      await waitFor(() => {
        console.log(`${LOG_PREFIX} Waiting for generateSpeechFromText call`);
        expect(mockedAiApiClient.generateSpeechFromText).toHaveBeenCalledWith(
          'Hello! This is a test of the text-to-speech generation.',
        );
      });

      // Wait specifically for the audio constructor call
      await waitFor(() => {
        console.log(`${LOG_PREFIX} Waiting for Audio constructor call`);
        expect(global.Audio).toHaveBeenCalledWith(`data:audio/mpeg;base64,${mockBase64Audio}`);
      });

      // Then check play
      await waitFor(() => {
        console.log(`${LOG_PREFIX} Waiting for mockAudioPlay call`);
        // Debugging helper: if play wasn't called, check if error was notified
        if (mockAudioPlay.mock.calls.length === 0) {
          const errorCalls = vi.mocked(notifyError).mock.calls;
          if (errorCalls.length > 0) {
            console.error(
              `${LOG_PREFIX} Unexpected: notifyError was called during success test:`,
              errorCalls,
            );
          } else {
            // If notifyError wasn't called, verify if Audio constructor was actually called as expected
            console.log(`${LOG_PREFIX} mockAudioPlay not called yet...`);
          }
        }
        expect(mockAudioPlay).toHaveBeenCalled();
      });

      // Check that loading state is gone and replay button is visible
      await waitFor(() => {
        console.log(`${LOG_PREFIX} Waiting for UI update (button enabled, replay visible)`);
        expect(screen.getByRole('button', { name: /generate & play/i })).not.toBeDisabled();
        expect(screen.getByRole('button', { name: /replay/i })).toBeInTheDocument();
      });
      console.log(`${LOG_PREFIX} Test: generateSpeechFromText success flow passed`);
    });

    it('should show an error notification if text is empty', async () => {
      console.log(`${LOG_PREFIX} Test: empty text validation`);
      render(<VoiceLabPage />);
      const textarea = screen.getByRole('textbox');
      fireEvent.change(textarea, { target: { value: '   ' } }); // Empty text

      console.log(`${LOG_PREFIX} Clicking generate button with empty text`);
      fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Please enter some text to generate speech.');
      });
      expect(mockedAiApiClient.generateSpeechFromText).not.toHaveBeenCalled();
      console.log(`${LOG_PREFIX} Test: empty text validation passed`);
    });

    it('should show an error notification if API call fails', async () => {
      console.log(`${LOG_PREFIX} Test: API failure`);
      mockedAiApiClient.generateSpeechFromText.mockRejectedValue(new Error('AI service is down'));
      render(<VoiceLabPage />);

      console.log(`${LOG_PREFIX} Clicking generate button (expecting failure)`);
      fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Speech generation failed: AI service is down');
      });
      expect(logger.error).toHaveBeenCalledWith(
        { err: expect.any(Error) },
        '[VoiceLabPage] Failed to generate speech',
      );
    });

    it('should show an error if API returns no audio data', async () => {
      console.log(`${LOG_PREFIX} Test: No audio data returned`);
      mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
        json: async () => null, // Simulate falsy response
      } as Response);
      render(<VoiceLabPage />);

      fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('The AI did not return any audio data.');
      });
      console.log(`${LOG_PREFIX} Test: No audio data returned passed`);
    });

    it('should handle non-Error objects in catch block', async () => {
      console.log(`${LOG_PREFIX} Test: Non-error object rejection`);
      mockedAiApiClient.generateSpeechFromText.mockRejectedValue('A simple string error');
      render(<VoiceLabPage />);

      fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith(
          'Speech generation failed: An unknown error occurred.',
        );
      });
      expect(logger.error).toHaveBeenCalledWith(
        { err: 'A simple string error' },
        '[VoiceLabPage] Failed to generate speech',
      );
    });

    it('should allow replaying the generated audio', async () => {
      console.log(`${LOG_PREFIX} Test: Replay functionality`);
      mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
        json: async () => 'mock-audio-data',
      } as Response);
      render(<VoiceLabPage />);

      const generateButton = screen.getByRole('button', { name: /generate & play/i });

      console.log(`${LOG_PREFIX} Clicking generate button for replay test`);
      fireEvent.click(generateButton);

      // Wait for the replay button to appear
      console.log(`${LOG_PREFIX} Waiting for replay button to appear`);
      const replayButton = await screen.findByTestId('replay-button');
      console.log(`${LOG_PREFIX} Replay button found`);

      // Verify initial play happened
      await waitFor(() => {
        console.log(`${LOG_PREFIX} Verifying initial play count: 1`);
        expect(mockAudioPlay).toHaveBeenCalledTimes(1);
      });

      // Click the replay button
      console.log(`${LOG_PREFIX} Clicking replay button`);
      fireEvent.click(replayButton);

      // Verify that play was called a second time
      await waitFor(() => {
        console.log(
          `${LOG_PREFIX} Verifying play count reaches 2. Current calls: ${mockAudioPlay.mock.calls.length}`,
        );
        expect(mockAudioPlay).toHaveBeenCalledTimes(2);
      });
      console.log(`${LOG_PREFIX} Test: Replay functionality passed`);
    });
  });

  describe('Real-time Voice Session', () => {
    it('should call startVoiceSession and show an error notification', async () => {
      console.log(`${LOG_PREFIX} Test: Real-time voice session error`);
      // The startVoiceSession function is a stub that will throw an error.
      // We can mock it to control the error message for the test.
      mockedAiApiClient.startVoiceSession.mockImplementation(() => {
        console.log(`${LOG_PREFIX} mocked startVoiceSession called`);
        throw new Error('WebSocket proxy not implemented.');
      });

      render(<VoiceLabPage />);
      fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));

      await waitFor(() => {
        expect(mockedAiApiClient.startVoiceSession).toHaveBeenCalled();
        expect(notifyError).toHaveBeenCalledWith(
          'Could not start voice session: WebSocket proxy not implemented.',
        );
      });
      expect(logger.error).toHaveBeenCalledWith(
        { err: expect.any(Error) },
        'Failed to start voice session:',
      );
    });

    it('should handle unknown errors in startVoiceSession', async () => {
      mockedAiApiClient.startVoiceSession.mockImplementation(() => {
        throw 'Unknown session error';
      });

      render(<VoiceLabPage />);
      fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith(
          'Could not start voice session: An unknown error occurred.',
        );
      });
      expect(logger.error).toHaveBeenCalledWith(
        { err: 'Unknown session error' },
        'Failed to start voice session:',
      );
    });

    it('should handle successful startVoiceSession and log messages', async () => {
      mockedAiApiClient.startVoiceSession.mockImplementation(async (config) => {
        if (config && config.onmessage) {
          // Cast to `any` as we don't need a full LiveServerMessage object for this test
          config.onmessage('Test message from socket' as any);
        }
      });

      render(<VoiceLabPage />);
      fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));

      await waitFor(() => {
        expect(mockedAiApiClient.startVoiceSession).toHaveBeenCalled();
      });
      expect(logger.info).toHaveBeenCalledWith(
        'Received voice session message:',
        'Test message from socket',
      );
    });
  });
});
