import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { NotificationType, NOTIFICATION_SOUNDS, type NotificationSound } from "./types";

// Mock HTMLAudioElement for soundPlayer tests
class MockAudioElement {
  src: string = "";
  preload: string = "";
  volume: number = 1;

  constructor(src?: string) {
    if (src) this.src = src;
  }

  cloneNode(): MockAudioElement {
    const clone = new MockAudioElement(this.src);
    clone.volume = this.volume;
    return clone;
  }

  async play(): Promise<void> {
    return Promise.resolve();
  }
}

// Store original Audio before mocking
const OriginalAudio = globalThis.Audio;

describe("notifications", () => {
  describe("NotificationType enum", () => {
    it("has SUCCESS type", () => {
      expect(NotificationType.SUCCESS).toBe("success");
    });

    it("has ERROR type", () => {
      expect(NotificationType.ERROR).toBe("error");
    });

    it("has PERMISSION type", () => {
      expect(NotificationType.PERMISSION).toBe("permission");
    });

    it("has CONNECTION type", () => {
      expect(NotificationType.CONNECTION).toBe("connection");
    });

    it("has TASK_START type", () => {
      expect(NotificationType.TASK_START).toBe("task_start");
    });

    it("has ACHIEVEMENT type", () => {
      expect(NotificationType.ACHIEVEMENT).toBe("achievement");
    });

    it("has exactly 6 notification types", () => {
      const types = Object.values(NotificationType);
      expect(types.length).toBe(6);
    });
  });

  describe("NOTIFICATION_SOUNDS constant", () => {
    it("has sounds for all notification types", () => {
      Object.values(NotificationType).forEach((type) => {
        expect(NOTIFICATION_SOUNDS[type]).toBeDefined();
      });
    });

    it("each sound has required properties", () => {
      Object.values(NOTIFICATION_SOUNDS).forEach((sound) => {
        expect(sound.type).toBeDefined();
        expect(sound.filename).toBeDefined();
        expect(sound.phrase).toBeDefined();
        expect(typeof sound.filename).toBe("string");
        expect(typeof sound.phrase).toBe("string");
        expect(sound.filename.endsWith(".mp3")).toBe(true);
      });
    });

    it("SUCCESS sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.SUCCESS];
      expect(sound.type).toBe(NotificationType.SUCCESS);
      expect(sound.filename).toBe("im-done.mp3");
      expect(sound.phrase).toBe("I'm done!");
      expect(sound.volume).toBe(0.7);
    });

    it("ERROR sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.ERROR];
      expect(sound.type).toBe(NotificationType.ERROR);
      expect(sound.filename).toBe("oh-no.mp3");
      expect(sound.phrase).toBe("Oh no...");
      expect(sound.volume).toBe(0.8);
    });

    it("PERMISSION sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.PERMISSION];
      expect(sound.type).toBe(NotificationType.PERMISSION);
      expect(sound.filename).toBe("access-please.mp3");
      expect(sound.phrase).toBe("Access please!");
      expect(sound.volume).toBe(0.9);
    });

    it("CONNECTION sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.CONNECTION];
      expect(sound.type).toBe(NotificationType.CONNECTION);
      expect(sound.filename).toBe("connected.mp3");
      expect(sound.phrase).toBe("Connected!");
      expect(sound.volume).toBe(0.7);
    });

    it("TASK_START sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.TASK_START];
      expect(sound.type).toBe(NotificationType.TASK_START);
      expect(sound.filename).toBe("working-on-it.mp3");
      expect(sound.phrase).toBe("Working on it!");
      expect(sound.volume).toBe(0.6);
    });

    it("ACHIEVEMENT sound has correct properties", () => {
      const sound = NOTIFICATION_SOUNDS[NotificationType.ACHIEVEMENT];
      expect(sound.type).toBe(NotificationType.ACHIEVEMENT);
      expect(sound.filename).toBe("achievement.mp3");
      expect(sound.phrase).toBe("Achievement Get~!");
      expect(sound.volume).toBe(0.8);
    });

    it("all volumes are within valid range (0-1)", () => {
      Object.values(NOTIFICATION_SOUNDS).forEach((sound) => {
        if (sound.volume !== undefined) {
          expect(sound.volume).toBeGreaterThanOrEqual(0);
          expect(sound.volume).toBeLessThanOrEqual(1);
        }
      });
    });
  });

  describe("NotificationSound interface", () => {
    it("can create a valid notification sound object", () => {
      const sound: NotificationSound = {
        type: NotificationType.SUCCESS,
        filename: "test-sound.mp3",
        phrase: "Test phrase",
        volume: 0.5,
      };

      expect(sound.type).toBe(NotificationType.SUCCESS);
      expect(sound.filename).toBe("test-sound.mp3");
      expect(sound.phrase).toBe("Test phrase");
      expect(sound.volume).toBe(0.5);
    });

    it("volume is optional", () => {
      const sound: NotificationSound = {
        type: NotificationType.ERROR,
        filename: "error.mp3",
        phrase: "Error occurred",
      };

      expect(sound.volume).toBeUndefined();
    });
  });

  describe("SoundPlayer class", () => {
    beforeEach(() => {
      // Mock Audio constructor
      globalThis.Audio = MockAudioElement as unknown as typeof Audio;
    });

    afterEach(() => {
      // Restore original Audio
      globalThis.Audio = OriginalAudio;
      vi.resetModules();
    });

    it("can import soundPlayer singleton", async () => {
      const { soundPlayer } = await import("./soundPlayer");
      expect(soundPlayer).toBeDefined();
    });

    it("setEnabled changes enabled state", async () => {
      const { soundPlayer } = await import("./soundPlayer");

      soundPlayer.setEnabled(true);
      expect(soundPlayer.isEnabled()).toBe(true);

      soundPlayer.setEnabled(false);
      expect(soundPlayer.isEnabled()).toBe(false);
    });

    it("starts disabled by default", async () => {
      // Need to reimport to get fresh instance behavior
      // But since it's a singleton, we just test the method
      const { soundPlayer } = await import("./soundPlayer");

      // Reset to default state
      soundPlayer.setEnabled(false);
      expect(soundPlayer.isEnabled()).toBe(false);
    });

    it("setGlobalVolume clamps values to 0-1 range", async () => {
      const { soundPlayer } = await import("./soundPlayer");

      // Test that it doesn't throw on edge cases
      soundPlayer.setGlobalVolume(0);
      soundPlayer.setGlobalVolume(1);
      soundPlayer.setGlobalVolume(0.5);

      // Test clamping below 0
      soundPlayer.setGlobalVolume(-0.5);

      // Test clamping above 1
      soundPlayer.setGlobalVolume(1.5);
    });

    it("play returns early when disabled", async () => {
      const { soundPlayer } = await import("./soundPlayer");

      soundPlayer.setEnabled(false);

      // Should not throw when disabled
      await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined();
    });

    it("play attempts to play when enabled", async () => {
      const { soundPlayer } = await import("./soundPlayer");

      soundPlayer.setEnabled(true);

      // Should not throw
      await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined();
    });
  });

  describe("NotificationManager class", () => {
    beforeEach(() => {
      globalThis.Audio = MockAudioElement as unknown as typeof Audio;
      vi.resetModules();
    });

    afterEach(() => {
      globalThis.Audio = OriginalAudio;
    });

    it("can import notificationManager singleton", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(notificationManager).toBeDefined();
    });

    it("has notifySuccess method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notifySuccess).toBe("function");
    });

    it("has notifyError method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notifyError).toBe("function");
    });

    it("has notifyPermission method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notifyPermission).toBe("function");
    });

    it("has notifyConnection method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notifyConnection).toBe("function");
    });

    it("has notifyTaskStart method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notifyTaskStart).toBe("function");
    });

    it("has notify method", async () => {
      vi.mock("@tauri-apps/api/core", () => ({
        invoke: vi.fn().mockRejectedValue(new Error("Not available")),
      }));

      const { notificationManager } = await import("./notificationManager");
      expect(typeof notificationManager.notify).toBe("function");
    });
  });

  describe("notification sounds file paths", () => {
    it("all sound files have valid paths", () => {
      Object.values(NOTIFICATION_SOUNDS).forEach((sound) => {
        // Check that filename doesn't contain path traversal
        expect(sound.filename).not.toContain("..");
        expect(sound.filename).not.toContain("/");
        expect(sound.filename).not.toContain("\\");
      });
    });

    it("sound filenames are unique", () => {
      const filenames = Object.values(NOTIFICATION_SOUNDS).map((s) => s.filename);
      const uniqueFilenames = new Set(filenames);
      expect(uniqueFilenames.size).toBe(filenames.length);
    });

    it("phrases are unique", () => {
      const phrases = Object.values(NOTIFICATION_SOUNDS).map((s) => s.phrase);
      const uniquePhrases = new Set(phrases);
      expect(uniquePhrases.size).toBe(phrases.length);
    });
  });
});
