import { describe, it, expect, vi, beforeEach } from "vitest";
import * as path from "path";

// Mock electron
vi.mock("electron", () => ({
  app: {
    getPath: vi.fn().mockReturnValue("/mock/userData"),
    getAppPath: vi.fn().mockReturnValue("/mock/appPath"),
  },
}));

// Mock electron-log/main
vi.mock("electron-log/main", () => ({
  default: {
    scope: vi.fn(() => ({
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    })),
  },
}));

// Mock child_process (needed by whisper.ts import)
vi.mock("child_process", () => ({
  execFile: vi.fn(),
}));

// Mock fs module
vi.mock("fs", async () => {
  const actual = await vi.importActual<typeof import("fs")>("fs");
  return {
    ...actual,
    existsSync: vi.fn().mockReturnValue(false),
    unlinkSync: vi.fn(),
    readFileSync: vi.fn(),
    readdirSync: vi.fn().mockReturnValue([]),
    promises: {
      mkdir: vi.fn().mockResolvedValue(undefined),
      writeFile: vi.fn().mockResolvedValue(undefined),
      access: vi.fn().mockRejectedValue(
        Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
      ),
      unlink: vi.fn().mockResolvedValue(undefined),
      readdir: vi.fn().mockResolvedValue([]),
    },
  };
});

import * as fs from "fs";
import {
  getAudioDir,
  getAudioFilePath,
  saveAudioFile,
  deleteAudioFile,
  decodeWavFile,
  cleanupOrphanedAudioFiles,
} from "../../../src/main/audio/persistence";
import { encodeWav } from "../../../src/main/audio/whisper";

describe("AudioPersistence", () => {
  beforeEach(() => {
    vi.mocked(fs.existsSync).mockReset().mockReturnValue(false);
    vi.mocked(fs.unlinkSync).mockReset();
    vi.mocked(fs.readFileSync).mockReset();
    vi.mocked(fs.readdirSync).mockReset().mockReturnValue([]);
    vi.mocked(fs.promises.mkdir).mockReset().mockResolvedValue(undefined);
    vi.mocked(fs.promises.writeFile).mockReset().mockResolvedValue(undefined);
    vi.mocked(fs.promises.access)
      .mockReset()
      .mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
    vi.mocked(fs.promises.unlink).mockReset().mockResolvedValue(undefined);
    vi.mocked(fs.promises.readdir).mockReset().mockResolvedValue([]);
  });

  describe("getAudioDir", () => {
    it("should return the audio directory path under userData", () => {
      const result = getAudioDir();
      expect(result).toBe(path.join("/mock/userData", "audio"));
    });
  });

  describe("getAudioFilePath", () => {
    it("should return the deterministic WAV path without I/O", () => {
      const result = getAudioFilePath("my-entry-id");
      expect(result).toBe(path.join("/mock/userData", "audio", "my-entry-id.wav"));
    });
  });

  describe("saveAudioFile", () => {
    it("should create directory and write WAV file", async () => {
      const audioBuffer = new Float32Array([0.1, -0.2, 0.3, -0.4]);
      const sampleRate = 16000;
      const entryId = "test-entry-123";

      const result = await saveAudioFile(audioBuffer, sampleRate, entryId);

      expect(fs.promises.mkdir).toHaveBeenCalledWith(
        path.join("/mock/userData", "audio"),
        { recursive: true },
      );
      expect(fs.promises.writeFile).toHaveBeenCalledWith(
        path.join("/mock/userData", "audio", "test-entry-123.wav"),
        expect.any(Buffer),
      );
      expect(result).toBe(path.join("/mock/userData", "audio", "test-entry-123.wav"));
    });

    it("should reject when writeFile fails", async () => {
      vi.mocked(fs.promises.writeFile).mockRejectedValue(new Error("disk full"));

      await expect(
        saveAudioFile(new Float32Array([0.1]), 16000, "fail-entry"),
      ).rejects.toThrow("disk full");
    });
  });

  describe("deleteAudioFile", () => {
    it("should delete an existing file", async () => {
      vi.mocked(fs.promises.access).mockResolvedValue(undefined);

      await deleteAudioFile("/mock/userData/audio/test.wav");

      expect(fs.promises.unlink).toHaveBeenCalledWith("/mock/userData/audio/test.wav");
    });

    it("should not attempt to delete a missing file", async () => {
      vi.mocked(fs.promises.access).mockRejectedValue(
        Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
      );

      await deleteAudioFile("/mock/userData/audio/missing.wav");

      expect(fs.promises.unlink).not.toHaveBeenCalled();
    });

    it("should not throw when unlink fails", async () => {
      vi.mocked(fs.promises.access).mockResolvedValue(undefined);
      vi.mocked(fs.promises.unlink).mockRejectedValue(new Error("permission denied"));

      await expect(deleteAudioFile("/mock/userData/audio/test.wav")).resolves.toBeUndefined();
    });
  });

  describe("decodeWavFile", () => {
    it("should round-trip correctly with encodeWav", () => {
      const original = new Float32Array([0.5, -0.5, 0.25, -0.25, 0.0]);
      const sampleRate = 16000;

      const wavBuffer = encodeWav(original, sampleRate);
      vi.mocked(fs.readFileSync).mockReturnValue(wavBuffer);

      const result = decodeWavFile("/fake/path.wav");

      expect(result.sampleRate).toBe(sampleRate);
      expect(result.audioBuffer.length).toBe(original.length);

      // 16-bit quantization introduces small rounding errors, so check with tolerance
      for (let i = 0; i < original.length; i++) {
        expect(result.audioBuffer[i]).toBeCloseTo(original[i], 3);
      }
    });

    it("should correctly decode sample rate from WAV header", () => {
      const samples = new Float32Array([0.1, 0.2]);
      const sampleRate = 44100;

      const wavBuffer = encodeWav(samples, sampleRate);
      vi.mocked(fs.readFileSync).mockReturnValue(wavBuffer);

      const result = decodeWavFile("/fake/path.wav");
      expect(result.sampleRate).toBe(44100);
    });
  });

  describe("cleanupOrphanedAudioFiles", () => {
    it("should remove files not in the valid IDs set", async () => {
      vi.mocked(fs.promises.readdir).mockResolvedValue([
        "keep-1.wav",
        "keep-2.wav",
        "orphan-1.wav",
        "orphan-2.wav",
      ] as unknown as string[]);
      vi.mocked(fs.promises.access).mockResolvedValue(undefined);

      const validIds = new Set(["keep-1", "keep-2"]);
      await cleanupOrphanedAudioFiles(validIds);

      expect(fs.promises.unlink).toHaveBeenCalledTimes(2);
      expect(fs.promises.unlink).toHaveBeenCalledWith(
        path.join("/mock/userData", "audio", "orphan-1.wav"),
      );
      expect(fs.promises.unlink).toHaveBeenCalledWith(
        path.join("/mock/userData", "audio", "orphan-2.wav"),
      );
    });

    it("should do nothing when audio directory does not exist", async () => {
      vi.mocked(fs.promises.readdir).mockRejectedValue(
        Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
      );

      await cleanupOrphanedAudioFiles(new Set(["id-1"]));

      expect(fs.promises.unlink).not.toHaveBeenCalled();
    });

    it("should keep all files when all IDs are valid", async () => {
      vi.mocked(fs.promises.readdir).mockResolvedValue([
        "id-1.wav",
        "id-2.wav",
      ] as unknown as string[]);
      vi.mocked(fs.promises.access).mockResolvedValue(undefined);

      await cleanupOrphanedAudioFiles(new Set(["id-1", "id-2"]));

      expect(fs.promises.unlink).not.toHaveBeenCalled();
    });
  });
});
