/**
 * PiperTtsProvider Unit Tests
 *
 * Tests the Piper TTS provider via OpenedAI Speech (fallback tier).
 * Validates provider identity, OpenAI voice name mapping, voice listing,
 * and ultra-lightweight CPU-only design characteristics.
 *
 * Issue #395
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import {
  PiperTtsProvider,
  PIPER_VOICE_MAP,
  PIPER_SUPPORTED_FORMATS,
  OPENAI_STANDARD_VOICES,
} from "./piper-tts.provider";
import type { VoiceInfo } from "../interfaces/speech-types";

// ==========================================
// Mock OpenAI SDK
// ==========================================

vi.mock("openai", () => {
  class MockOpenAI {
    audio = {
      speech: {
        create: vi.fn(),
      },
    };
  }
  return { default: MockOpenAI };
});

// ==========================================
// Provider identity
// ==========================================

describe("PiperTtsProvider", () => {
  const testBaseURL = "http://openedai-speech:8000/v1";
  let provider: PiperTtsProvider;

  beforeEach(() => {
    provider = new PiperTtsProvider(testBaseURL);
  });

  describe("provider identity", () => {
    it("should have name 'piper'", () => {
      expect(provider.name).toBe("piper");
    });

    it("should have tier 'fallback'", () => {
      expect(provider.tier).toBe("fallback");
    });
  });

  // ==========================================
  // Constructor
  // ==========================================

  describe("constructor", () => {
    it("should use 'alloy' as default voice", () => {
      const newProvider = new PiperTtsProvider(testBaseURL);
      expect(newProvider).toBeDefined();
    });

    it("should accept a custom default voice", () => {
      const customProvider = new PiperTtsProvider(testBaseURL, "nova");
      expect(customProvider).toBeDefined();
    });

    it("should accept a custom default format", () => {
      const customProvider = new PiperTtsProvider(testBaseURL, "alloy", "wav");
      expect(customProvider).toBeDefined();
    });
  });

  // ==========================================
  // listVoices()
  // ==========================================

  describe("listVoices", () => {
    let voices: VoiceInfo[];

    beforeEach(async () => {
      voices = await provider.listVoices();
    });

    it("should return an array of VoiceInfo objects", () => {
      expect(voices).toBeInstanceOf(Array);
      expect(voices.length).toBeGreaterThan(0);
    });

    it("should return exactly 6 voices (OpenAI standard set)", () => {
      expect(voices.length).toBe(6);
    });

    it("should set tier to 'fallback' on all voices", () => {
      for (const voice of voices) {
        expect(voice.tier).toBe("fallback");
      }
    });

    it("should have exactly one default voice", () => {
      const defaults = voices.filter((v) => v.isDefault === true);
      expect(defaults.length).toBe(1);
    });

    it("should mark 'alloy' as the default voice", () => {
      const defaultVoice = voices.find((v) => v.isDefault === true);
      expect(defaultVoice).toBeDefined();
      expect(defaultVoice?.id).toBe("alloy");
    });

    it("should have an id and name for every voice", () => {
      for (const voice of voices) {
        expect(voice.id).toBeTruthy();
        expect(voice.name).toBeTruthy();
      }
    });

    it("should set language on every voice", () => {
      for (const voice of voices) {
        expect(voice.language).toBeTruthy();
      }
    });

    // ==========================================
    // All 6 OpenAI standard voices present
    // ==========================================

    describe("OpenAI standard voices", () => {
      const standardVoiceIds = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];

      it.each(standardVoiceIds)("should include voice '%s'", (voiceId) => {
        const voice = voices.find((v) => v.id === voiceId);
        expect(voice).toBeDefined();
      });
    });

    // ==========================================
    // Voice metadata
    // ==========================================

    describe("voice metadata", () => {
      it("should include gender info in voice names", () => {
        const alloy = voices.find((v) => v.id === "alloy");
        expect(alloy?.name).toMatch(/Female|Male/);
      });

      it("should map alloy to a female voice", () => {
        const alloy = voices.find((v) => v.id === "alloy");
        expect(alloy?.name).toContain("Female");
      });

      it("should map echo to a male voice", () => {
        const echo = voices.find((v) => v.id === "echo");
        expect(echo?.name).toContain("Male");
      });

      it("should map fable to a British voice", () => {
        const fable = voices.find((v) => v.id === "fable");
        expect(fable?.language).toBe("en-GB");
      });

      it("should map onyx to a male voice", () => {
        const onyx = voices.find((v) => v.id === "onyx");
        expect(onyx?.name).toContain("Male");
      });

      it("should map nova to a female voice", () => {
        const nova = voices.find((v) => v.id === "nova");
        expect(nova?.name).toContain("Female");
      });

      it("should map shimmer to a female voice", () => {
        const shimmer = voices.find((v) => v.id === "shimmer");
        expect(shimmer?.name).toContain("Female");
      });
    });
  });
});

// ==========================================
// PIPER_VOICE_MAP
// ==========================================

describe("PIPER_VOICE_MAP", () => {
  it("should contain all 6 OpenAI standard voice names", () => {
    const expectedKeys = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
    for (const key of expectedKeys) {
      expect(PIPER_VOICE_MAP).toHaveProperty(key);
    }
  });

  it("should map each voice to a Piper voice ID", () => {
    for (const entry of Object.values(PIPER_VOICE_MAP)) {
      expect(entry.piperVoice).toBeTruthy();
      expect(typeof entry.piperVoice).toBe("string");
    }
  });

  it("should have gender for each voice entry", () => {
    for (const entry of Object.values(PIPER_VOICE_MAP)) {
      expect(entry.gender).toMatch(/^(female|male)$/);
    }
  });

  it("should have a language for each voice entry", () => {
    for (const entry of Object.values(PIPER_VOICE_MAP)) {
      expect(entry.language).toBeTruthy();
    }
  });

  it("should have a description for each voice entry", () => {
    for (const entry of Object.values(PIPER_VOICE_MAP)) {
      expect(entry.description).toBeTruthy();
    }
  });
});

// ==========================================
// OPENAI_STANDARD_VOICES
// ==========================================

describe("OPENAI_STANDARD_VOICES", () => {
  it("should be an array of 6 voice IDs", () => {
    expect(Array.isArray(OPENAI_STANDARD_VOICES)).toBe(true);
    expect(OPENAI_STANDARD_VOICES.length).toBe(6);
  });

  it("should contain all standard OpenAI voice names", () => {
    expect(OPENAI_STANDARD_VOICES).toContain("alloy");
    expect(OPENAI_STANDARD_VOICES).toContain("echo");
    expect(OPENAI_STANDARD_VOICES).toContain("fable");
    expect(OPENAI_STANDARD_VOICES).toContain("onyx");
    expect(OPENAI_STANDARD_VOICES).toContain("nova");
    expect(OPENAI_STANDARD_VOICES).toContain("shimmer");
  });
});

// ==========================================
// PIPER_SUPPORTED_FORMATS
// ==========================================

describe("PIPER_SUPPORTED_FORMATS", () => {
  it("should include mp3", () => {
    expect(PIPER_SUPPORTED_FORMATS).toContain("mp3");
  });

  it("should include wav", () => {
    expect(PIPER_SUPPORTED_FORMATS).toContain("wav");
  });

  it("should include opus", () => {
    expect(PIPER_SUPPORTED_FORMATS).toContain("opus");
  });

  it("should include flac", () => {
    expect(PIPER_SUPPORTED_FORMATS).toContain("flac");
  });

  it("should be a readonly array", () => {
    expect(Array.isArray(PIPER_SUPPORTED_FORMATS)).toBe(true);
  });
});
