// Required by Jest: hoisting mocks to the top
jest.mock("../src/ffmpeg-manager", () => ({
  FFmpegProcessManager: jest.fn(),
}));

jest.mock("../src/utils");

import { Recorder } from "../src/recorder";
import {
  Camera,
  Display,
  DisplayRecordingItem,
  Microphone,
  RecorderOptions,
  WebcamRecordingItem,
} from "../src/types";

// Mock devices
const devicesMock = {
  displays: [
    { id: "display1", name: "Main Display" } as Display,
    { id: "display2", name: "Secondary Display" } as Display,
  ],
  cameras: [
    { id: "camera1", name: "FaceTime HD Camera" } as Camera,
    { id: "camera2", name: "External Camera" } as Camera,
  ],
  microphones: [
    { id: "mic1", name: "Built-in Microphone" } as Microphone,
    { id: "mic2", name: "External Microphone" } as Microphone,
  ],
};

// Mock process manager
const mockProcessManager = {
  startProcess: jest.fn().mockResolvedValue(undefined),
  stopProcess: jest.fn().mockResolvedValue(undefined),
  stopAllProcesses: jest.fn().mockResolvedValue(undefined),
};

describe("Recorder", () => {
  let recorder: Recorder;

  // Mock utilities that will be spied on
  const utils = require("../src/utils");
  const FFmpegProcessManager =
    require("../src/ffmpeg-manager").FFmpegProcessManager;

  // Ensure we have valid Display and Camera objects with non-null assertion
  const display = devicesMock.displays[0]!;
  const camera = devicesMock.cameras[0]!;
  const microphone = devicesMock.microphones[0]!;

  const displayItem: DisplayRecordingItem = {
    type: "display",
    display,
  };

  const webcamItem: WebcamRecordingItem = {
    type: "webcam",
    camera,
    microphone,
  };

  const options: RecorderOptions = {
    output_directory: "/test/output",
    items: [displayItem, webcamItem],
  };

  beforeEach(() => {
    // Clear all mocks before each test
    jest.clearAllMocks();

    // Configure the mocks for this test
    utils.createOutputDirectory.mockImplementation((dir: string) => dir);
    utils.getAbsolutePath.mockImplementation((path: string) => path);
    utils.getDeviceIndex.mockResolvedValue(0);
    utils.getVideoMetadata.mockResolvedValue({
      dimensions: {
        width: 1920,
        height: 1080,
      },
      durationInSeconds: 60,
    });
    utils.waitForFile.mockResolvedValue(true);

    // Make FFmpegProcessManager return our mock
    FFmpegProcessManager.mockImplementation(() => mockProcessManager);

    // Create a fresh recorder for each test
    recorder = new Recorder(options);
  });

  describe("prepare", () => {
    it("should prepare recorder for recording", async () => {
      // Act
      await recorder.prepare();

      // Assert
      expect(utils.createOutputDirectory).toHaveBeenCalledWith(
        options.output_directory
      );
    });

    it("should throw error if already prepared", async () => {
      // Arrange
      await recorder.prepare();

      // Act & Assert
      await expect(recorder.prepare()).rejects.toThrow();
    });

    it("should throw error if no recording items provided", async () => {
      // Arrange
      const emptyRecorder = new Recorder({
        output_directory: "/test/output",
        items: [],
      });

      // Act & Assert
      await expect(emptyRecorder.prepare()).rejects.toThrow();
    });
  });

  describe("start", () => {
    it("should start recording", async () => {
      // Arrange
      await recorder.prepare();

      // Act
      await recorder.start();

      // Assert - each item should spawn a process
      expect(mockProcessManager.startProcess).toHaveBeenCalledTimes(
        options.items.length
      );
    });

    it("should throw error if already recording", async () => {
      // Arrange
      await recorder.prepare();
      await recorder.start();

      // Set the recording flag manually
      Object.defineProperty(recorder, "recording", {
        value: true,
        writable: true,
      });

      // Act & Assert
      await expect(recorder.start()).rejects.toThrow();
    });

    it("should auto-prepare if not prepared", async () => {
      // Act
      await recorder.start();

      // Assert
      expect(mockProcessManager.startProcess).toHaveBeenCalledTimes(
        options.items.length
      );
    });
  });

  describe("stop", () => {
    it("should stop recording and return results", async () => {
      // Arrange
      await recorder.prepare();
      await recorder.start();

      // Set the recording flag manually
      Object.defineProperty(recorder, "recording", {
        value: true,
        writable: true,
      });

      // Act
      const result = await recorder.stop();

      // Assert
      expect(mockProcessManager.stopAllProcesses).toHaveBeenCalled();
      expect(result).toHaveProperty("files");
      expect(result.files.length).toBe(options.items.length);
    });

    it("should throw error if no recording in progress", async () => {
      // Act & Assert
      await expect(recorder.stop()).rejects.toThrow();
    });
  });
});
