import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useVoiceInput } from "./useVoiceInput";
import type { Socket } from "socket.io-client";
import { io } from "socket.io-client";

// Mock socket.io-client
vi.mock("socket.io-client");

// Mock MediaRecorder
const mockMediaRecorder = {
  start: vi.fn(),
  stop: vi.fn(),
  pause: vi.fn(),
  resume: vi.fn(),
  state: "inactive" as RecordingState,
  ondataavailable: null as ((event: BlobEvent) => void) | null,
  onstop: null as (() => void) | null,
  onerror: null as ((event: Event) => void) | null,
  addEventListener: vi.fn((event: string, handler: EventListenerOrEventListenerObject) => {
    if (event === "dataavailable") {
      mockMediaRecorder.ondataavailable = handler as (event: BlobEvent) => void;
    } else if (event === "stop") {
      mockMediaRecorder.onstop = handler as () => void;
    } else if (event === "error") {
      mockMediaRecorder.onerror = handler as (event: Event) => void;
    }
  }),
  removeEventListener: vi.fn(),
  stream: {
    getTracks: vi.fn(() => [{ stop: vi.fn() }]),
  },
};

// Mock MediaStream with getByteFrequencyData for audio level
const mockAnalyserNode = {
  fftSize: 256,
  frequencyBinCount: 128,
  getByteFrequencyData: vi.fn((array: Uint8Array) => {
    // Simulate some audio data
    for (let i = 0; i < array.length; i++) {
      array[i] = 128;
    }
  }),
  connect: vi.fn(),
  disconnect: vi.fn(),
};

const mockMediaStreamSource = {
  connect: vi.fn(),
  disconnect: vi.fn(),
};

const mockAudioContext = {
  createAnalyser: vi.fn(() => mockAnalyserNode),
  createMediaStreamSource: vi.fn(() => mockMediaStreamSource),
  close: vi.fn(),
  state: "running",
};

// Mock getUserMedia
const mockGetUserMedia = vi.fn();

// Set up global mocks
Object.defineProperty(global.navigator, "mediaDevices", {
  value: {
    getUserMedia: mockGetUserMedia,
  },
  writable: true,
  configurable: true,
});

// Mock AudioContext
vi.stubGlobal(
  "AudioContext",
  vi.fn(() => mockAudioContext)
);

// Mock MediaRecorder constructor
vi.stubGlobal(
  "MediaRecorder",
  vi.fn(() => mockMediaRecorder)
);

// Add isTypeSupported static method
(
  global.MediaRecorder as unknown as { isTypeSupported: (type: string) => boolean }
).isTypeSupported = vi.fn(() => true);

describe("useVoiceInput", (): void => {
  let mockSocket: Partial<Socket>;
  let socketEventHandlers: Record<string, (data: unknown) => void>;

  beforeEach((): void => {
    socketEventHandlers = {};

    mockSocket = {
      on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
        socketEventHandlers[event] = handler;
        return mockSocket;
      }) as unknown as Socket["on"],
      off: vi.fn(() => mockSocket) as unknown as Socket["off"],
      emit: vi.fn() as unknown as Socket["emit"],
      connect: vi.fn(),
      disconnect: vi.fn(),
      connected: true,
    };

    (io as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockSocket);

    // Reset MediaRecorder mock state
    mockMediaRecorder.state = "inactive";
    mockMediaRecorder.ondataavailable = null;
    mockMediaRecorder.onstop = null;
    mockMediaRecorder.onerror = null;

    // Default: getUserMedia succeeds
    const mockStream = {
      getTracks: vi.fn(() => [{ stop: vi.fn() }]),
    } as unknown as MediaStream;
    mockGetUserMedia.mockResolvedValue(mockStream);
  });

  afterEach((): void => {
    vi.clearAllMocks();
  });

  it("should return the correct interface", (): void => {
    const { result } = renderHook(() => useVoiceInput());

    expect(result.current).toHaveProperty("isRecording");
    expect(result.current).toHaveProperty("startRecording");
    expect(result.current).toHaveProperty("stopRecording");
    expect(result.current).toHaveProperty("transcript");
    expect(result.current).toHaveProperty("partialTranscript");
    expect(result.current).toHaveProperty("error");
    expect(result.current).toHaveProperty("audioLevel");
  });

  it("should start with default state", (): void => {
    const { result } = renderHook(() => useVoiceInput());

    expect(result.current.isRecording).toBe(false);
    expect(result.current.transcript).toBe("");
    expect(result.current.partialTranscript).toBe("");
    expect(result.current.error).toBeNull();
    expect(result.current.audioLevel).toBe(0);
  });

  it("should start recording when startRecording is called", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    expect(result.current.isRecording).toBe(true);
    expect(mockGetUserMedia).toHaveBeenCalledWith({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        sampleRate: 16000,
      },
    });
  });

  it("should stop recording when stopRecording is called", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    expect(result.current.isRecording).toBe(true);

    act(() => {
      result.current.stopRecording();
    });

    expect(result.current.isRecording).toBe(false);
  });

  it("should set error when microphone access is denied", async (): Promise<void> => {
    mockGetUserMedia.mockRejectedValueOnce(
      new DOMException("Permission denied", "NotAllowedError")
    );

    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    expect(result.current.isRecording).toBe(false);
    expect(result.current.error).toBeTruthy();
    expect(result.current.error).toContain("microphone");
  });

  it("should connect to speech WebSocket namespace", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    expect(io).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        path: "/socket.io",
      })
    );
  });

  it("should emit start-transcription when recording begins", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    expect(mockSocket.emit).toHaveBeenCalledWith(
      "start-transcription",
      expect.objectContaining({
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        format: expect.any(String),
      })
    );
  });

  it("should emit stop-transcription when recording stops", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    act(() => {
      result.current.stopRecording();
    });

    expect(mockSocket.emit).toHaveBeenCalledWith("stop-transcription");
  });

  it("should handle partial transcription events", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    act(() => {
      socketEventHandlers["transcription-partial"]?.({
        text: "hello world",
      });
    });

    await waitFor(() => {
      expect(result.current.partialTranscript).toBe("hello world");
    });
  });

  it("should handle final transcription events", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    act(() => {
      socketEventHandlers["transcription-final"]?.({
        text: "hello world final",
      });
    });

    await waitFor(() => {
      expect(result.current.transcript).toBe("hello world final");
    });
  });

  it("should handle transcription error events", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    act(() => {
      socketEventHandlers["transcription-error"]?.({
        message: "Transcription failed",
      });
    });

    await waitFor(() => {
      expect(result.current.error).toBe("Transcription failed");
    });
  });

  it("should call onTranscript callback when final transcription received", async (): Promise<void> => {
    const onTranscript = vi.fn();
    const { result } = renderHook(() => useVoiceInput({ onTranscript }));

    await act(async () => {
      await result.current.startRecording();
    });

    act(() => {
      socketEventHandlers["transcription-final"]?.({
        text: "final text",
      });
    });

    await waitFor(() => {
      expect(onTranscript).toHaveBeenCalledWith("final text");
    });
  });

  it("should clean up on unmount", async (): Promise<void> => {
    const { result, unmount } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    unmount();

    expect(mockSocket.disconnect).toHaveBeenCalled();
  });

  it("should not start recording if already recording", async (): Promise<void> => {
    const { result } = renderHook(() => useVoiceInput());

    await act(async () => {
      await result.current.startRecording();
    });

    // Reset the call count
    mockGetUserMedia.mockClear();

    await act(async () => {
      await result.current.startRecording();
    });

    // Should not have called getUserMedia again
    expect(mockGetUserMedia).not.toHaveBeenCalled();
  });

  describe("REST fallback", (): void => {
    it("should fall back to REST when WebSocket is unavailable", async (): Promise<void> => {
      // Simulate socket not connecting
      (mockSocket as { connected: boolean }).connected = false;

      const { result } = renderHook(() => useVoiceInput({ useWebSocket: false }));

      // Should still be able to start recording (REST mode)
      await act(async () => {
        await result.current.startRecording();
      });

      expect(result.current.isRecording).toBe(true);
    });
  });
});
