/**
 * SpeechService Tests
 *
 * Issue #389: Tests for provider abstraction layer with fallback logic.
 * Written FIRST following TDD (Red-Green-Refactor).
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ServiceUnavailableException } from "@nestjs/common";
import { SpeechService } from "./speech.service";
import { STT_PROVIDER, TTS_PROVIDERS } from "./speech.constants";
import { speechConfig } from "./speech.config";
import type { ISTTProvider } from "./interfaces/stt-provider.interface";
import type { ITTSProvider } from "./interfaces/tts-provider.interface";
import type {
  SpeechTier,
  TranscriptionResult,
  SynthesisResult,
  VoiceInfo,
} from "./interfaces/speech-types";

// ==========================================
// Mock provider factories
// ==========================================

function createMockSttProvider(overrides?: Partial<ISTTProvider>): ISTTProvider {
  return {
    name: "mock-stt",
    transcribe: vi.fn().mockResolvedValue({
      text: "Hello world",
      language: "en",
      durationSeconds: 2.5,
    } satisfies TranscriptionResult),
    isHealthy: vi.fn().mockResolvedValue(true),
    ...overrides,
  };
}

function createMockTtsProvider(tier: SpeechTier, overrides?: Partial<ITTSProvider>): ITTSProvider {
  return {
    name: `mock-tts-${tier}`,
    tier,
    synthesize: vi.fn().mockResolvedValue({
      audio: Buffer.from("fake-audio"),
      format: "mp3",
      voice: "test-voice",
      tier,
    } satisfies SynthesisResult),
    listVoices: vi
      .fn()
      .mockResolvedValue([
        { id: `${tier}-voice-1`, name: `${tier} Voice 1`, tier, isDefault: true },
      ] satisfies VoiceInfo[]),
    isHealthy: vi.fn().mockResolvedValue(true),
    ...overrides,
  };
}

// ==========================================
// Default config for tests
// ==========================================

function createTestConfig(): ReturnType<typeof speechConfig> {
  return {
    stt: {
      enabled: true,
      baseUrl: "http://localhost:8000/v1",
      model: "test-model",
      language: "en",
    },
    tts: {
      default: {
        enabled: true,
        url: "http://localhost:8880/v1",
        voice: "test-voice",
        format: "mp3",
      },
      premium: {
        enabled: true,
        url: "http://localhost:8881/v1",
      },
      fallback: {
        enabled: true,
        url: "http://localhost:8882/v1",
      },
    },
    limits: {
      maxUploadSize: 25_000_000,
      maxDurationSeconds: 600,
      maxTextLength: 4096,
    },
  } as ReturnType<typeof speechConfig>;
}

// ==========================================
// Test helper: create testing module
// ==========================================

async function createTestModule(options: {
  sttProvider?: ISTTProvider | null;
  ttsProviders?: Map<SpeechTier, ITTSProvider>;
  config?: ReturnType<typeof speechConfig>;
}): Promise<TestingModule> {
  const config = options.config ?? createTestConfig();
  const ttsProviders = options.ttsProviders ?? new Map<SpeechTier, ITTSProvider>();

  const providers: Array<{ provide: symbol | string; useValue: unknown }> = [
    { provide: speechConfig.KEY, useValue: config },
    { provide: TTS_PROVIDERS, useValue: ttsProviders },
  ];

  if (options.sttProvider !== undefined) {
    providers.push({ provide: STT_PROVIDER, useValue: options.sttProvider });
  }

  return Test.createTestingModule({
    providers: [SpeechService, ...providers],
  }).compile();
}

// ==========================================
// Tests
// ==========================================

describe("SpeechService", () => {
  // ==========================================
  // Construction and initialization
  // ==========================================
  describe("construction", () => {
    it("should be defined when all providers are injected", async () => {
      const module = await createTestModule({
        sttProvider: createMockSttProvider(),
        ttsProviders: new Map([["default", createMockTtsProvider("default")]]),
      });

      const service = module.get<SpeechService>(SpeechService);
      expect(service).toBeDefined();
    });

    it("should be defined with no STT provider", async () => {
      const module = await createTestModule({
        sttProvider: null,
        ttsProviders: new Map([["default", createMockTtsProvider("default")]]),
      });

      const service = module.get<SpeechService>(SpeechService);
      expect(service).toBeDefined();
    });

    it("should be defined with empty TTS providers map", async () => {
      const module = await createTestModule({
        sttProvider: createMockSttProvider(),
        ttsProviders: new Map(),
      });

      const service = module.get<SpeechService>(SpeechService);
      expect(service).toBeDefined();
    });
  });

  // ==========================================
  // transcribe()
  // ==========================================
  describe("transcribe", () => {
    let service: SpeechService;
    let mockStt: ISTTProvider;

    beforeEach(async () => {
      mockStt = createMockSttProvider();
      const module = await createTestModule({ sttProvider: mockStt });
      service = module.get<SpeechService>(SpeechService);
    });

    it("should delegate to the STT provider", async () => {
      const audio = Buffer.from("test-audio");
      const result = await service.transcribe(audio);

      expect(mockStt.transcribe).toHaveBeenCalledWith(audio, undefined);
      expect(result.text).toBe("Hello world");
      expect(result.language).toBe("en");
    });

    it("should pass options to the STT provider", async () => {
      const audio = Buffer.from("test-audio");
      const options = { language: "fr", model: "custom-model" };
      await service.transcribe(audio, options);

      expect(mockStt.transcribe).toHaveBeenCalledWith(audio, options);
    });

    it("should throw ServiceUnavailableException when STT is disabled in config", async () => {
      const config = createTestConfig();
      config.stt.enabled = false;
      const module = await createTestModule({ sttProvider: mockStt, config });
      service = module.get<SpeechService>(SpeechService);

      await expect(service.transcribe(Buffer.from("audio"))).rejects.toThrow(
        ServiceUnavailableException
      );
    });

    it("should throw ServiceUnavailableException when no STT provider is registered", async () => {
      const module = await createTestModule({ sttProvider: null });
      service = module.get<SpeechService>(SpeechService);

      await expect(service.transcribe(Buffer.from("audio"))).rejects.toThrow(
        ServiceUnavailableException
      );
    });

    it("should propagate provider errors as ServiceUnavailableException", async () => {
      const failingStt = createMockSttProvider({
        transcribe: vi.fn().mockRejectedValue(new Error("Connection refused")),
      });
      const module = await createTestModule({ sttProvider: failingStt });
      service = module.get<SpeechService>(SpeechService);

      await expect(service.transcribe(Buffer.from("audio"))).rejects.toThrow(
        ServiceUnavailableException
      );
    });
  });

  // ==========================================
  // synthesize()
  // ==========================================
  describe("synthesize", () => {
    let service: SpeechService;
    let defaultProvider: ITTSProvider;
    let premiumProvider: ITTSProvider;
    let fallbackProvider: ITTSProvider;

    beforeEach(async () => {
      defaultProvider = createMockTtsProvider("default");
      premiumProvider = createMockTtsProvider("premium");
      fallbackProvider = createMockTtsProvider("fallback");

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", defaultProvider],
        ["premium", premiumProvider],
        ["fallback", fallbackProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      service = module.get<SpeechService>(SpeechService);
    });

    it("should use the default tier when no tier is specified", async () => {
      const result = await service.synthesize("Hello world");

      expect(defaultProvider.synthesize).toHaveBeenCalledWith("Hello world", undefined);
      expect(result.tier).toBe("default");
    });

    it("should use the requested tier when specified", async () => {
      const result = await service.synthesize("Hello world", { tier: "premium" });

      expect(premiumProvider.synthesize).toHaveBeenCalled();
      expect(result.tier).toBe("premium");
    });

    it("should pass options to the TTS provider", async () => {
      const options = { voice: "custom-voice", format: "wav" as const };
      await service.synthesize("Hello", options);

      expect(defaultProvider.synthesize).toHaveBeenCalledWith("Hello", options);
    });

    it("should throw ServiceUnavailableException when TTS default is disabled and no tier specified", async () => {
      const config = createTestConfig();
      config.tts.default.enabled = false;
      config.tts.premium.enabled = false;
      config.tts.fallback.enabled = false;
      const module = await createTestModule({
        ttsProviders: new Map([["default", defaultProvider]]),
        config,
      });
      service = module.get<SpeechService>(SpeechService);

      await expect(service.synthesize("Hello")).rejects.toThrow(ServiceUnavailableException);
    });

    it("should throw ServiceUnavailableException when no TTS providers are registered", async () => {
      const module = await createTestModule({ ttsProviders: new Map() });
      service = module.get<SpeechService>(SpeechService);

      await expect(service.synthesize("Hello")).rejects.toThrow(ServiceUnavailableException);
    });
  });

  // ==========================================
  // synthesize() fallback logic
  // ==========================================
  describe("synthesize fallback", () => {
    it("should fall back from premium to default when premium provider fails", async () => {
      const failingPremium = createMockTtsProvider("premium", {
        synthesize: vi.fn().mockRejectedValue(new Error("Premium unavailable")),
      });
      const defaultProvider = createMockTtsProvider("default");

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["premium", failingPremium],
        ["default", defaultProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const result = await service.synthesize("Hello", { tier: "premium" });

      expect(failingPremium.synthesize).toHaveBeenCalled();
      expect(defaultProvider.synthesize).toHaveBeenCalled();
      expect(result.tier).toBe("default");
    });

    it("should fall back from default to fallback when default provider fails", async () => {
      const failingDefault = createMockTtsProvider("default", {
        synthesize: vi.fn().mockRejectedValue(new Error("Default unavailable")),
      });
      const fallbackProvider = createMockTtsProvider("fallback");

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", failingDefault],
        ["fallback", fallbackProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const result = await service.synthesize("Hello");

      expect(failingDefault.synthesize).toHaveBeenCalled();
      expect(fallbackProvider.synthesize).toHaveBeenCalled();
      expect(result.tier).toBe("fallback");
    });

    it("should fall back premium -> default -> fallback", async () => {
      const failingPremium = createMockTtsProvider("premium", {
        synthesize: vi.fn().mockRejectedValue(new Error("Premium fail")),
      });
      const failingDefault = createMockTtsProvider("default", {
        synthesize: vi.fn().mockRejectedValue(new Error("Default fail")),
      });
      const fallbackProvider = createMockTtsProvider("fallback");

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["premium", failingPremium],
        ["default", failingDefault],
        ["fallback", fallbackProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const result = await service.synthesize("Hello", { tier: "premium" });

      expect(failingPremium.synthesize).toHaveBeenCalled();
      expect(failingDefault.synthesize).toHaveBeenCalled();
      expect(fallbackProvider.synthesize).toHaveBeenCalled();
      expect(result.tier).toBe("fallback");
    });

    it("should throw ServiceUnavailableException when all tiers fail", async () => {
      const failingDefault = createMockTtsProvider("default", {
        synthesize: vi.fn().mockRejectedValue(new Error("Default fail")),
      });
      const failingFallback = createMockTtsProvider("fallback", {
        synthesize: vi.fn().mockRejectedValue(new Error("Fallback fail")),
      });

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", failingDefault],
        ["fallback", failingFallback],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      await expect(service.synthesize("Hello")).rejects.toThrow(ServiceUnavailableException);
    });

    it("should skip unavailable tiers in fallback chain", async () => {
      // premium requested, but only fallback registered (no default)
      const failingPremium = createMockTtsProvider("premium", {
        synthesize: vi.fn().mockRejectedValue(new Error("Premium fail")),
      });
      const fallbackProvider = createMockTtsProvider("fallback");

      const config = createTestConfig();
      config.tts.default.enabled = false;

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["premium", failingPremium],
        ["fallback", fallbackProvider],
      ]);

      const module = await createTestModule({ ttsProviders, config });
      const service = module.get<SpeechService>(SpeechService);

      const result = await service.synthesize("Hello", { tier: "premium" });
      expect(result.tier).toBe("fallback");
    });
  });

  // ==========================================
  // listVoices()
  // ==========================================
  describe("listVoices", () => {
    it("should aggregate voices from all registered TTS providers", async () => {
      const defaultProvider = createMockTtsProvider("default", {
        listVoices: vi.fn().mockResolvedValue([
          { id: "voice-1", name: "Voice 1", tier: "default" as SpeechTier, isDefault: true },
          { id: "voice-2", name: "Voice 2", tier: "default" as SpeechTier },
        ]),
      });
      const premiumProvider = createMockTtsProvider("premium", {
        listVoices: vi
          .fn()
          .mockResolvedValue([
            { id: "voice-3", name: "Voice 3", tier: "premium" as SpeechTier, isDefault: true },
          ]),
      });

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", defaultProvider],
        ["premium", premiumProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const voices = await service.listVoices();

      expect(voices).toHaveLength(3);
      expect(voices.map((v) => v.id)).toEqual(["voice-1", "voice-2", "voice-3"]);
    });

    it("should filter voices by tier when specified", async () => {
      const defaultProvider = createMockTtsProvider("default", {
        listVoices: vi
          .fn()
          .mockResolvedValue([{ id: "voice-1", name: "Voice 1", tier: "default" as SpeechTier }]),
      });
      const premiumProvider = createMockTtsProvider("premium", {
        listVoices: vi
          .fn()
          .mockResolvedValue([{ id: "voice-2", name: "Voice 2", tier: "premium" as SpeechTier }]),
      });

      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", defaultProvider],
        ["premium", premiumProvider],
      ]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const voices = await service.listVoices("premium");

      expect(voices).toHaveLength(1);
      expect(voices[0].id).toBe("voice-2");
      // Only the premium provider should have been called
      expect(premiumProvider.listVoices).toHaveBeenCalled();
      expect(defaultProvider.listVoices).not.toHaveBeenCalled();
    });

    it("should return empty array when no TTS providers are registered", async () => {
      const module = await createTestModule({ ttsProviders: new Map() });
      const service = module.get<SpeechService>(SpeechService);

      const voices = await service.listVoices();
      expect(voices).toEqual([]);
    });

    it("should return empty array when requested tier has no provider", async () => {
      const defaultProvider = createMockTtsProvider("default");
      const ttsProviders = new Map<SpeechTier, ITTSProvider>([["default", defaultProvider]]);

      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      const voices = await service.listVoices("premium");
      expect(voices).toEqual([]);
    });
  });

  // ==========================================
  // isSTTAvailable / isTTSAvailable
  // ==========================================
  describe("availability checks", () => {
    it("should report STT as available when enabled and provider registered", async () => {
      const module = await createTestModule({
        sttProvider: createMockSttProvider(),
      });
      const service = module.get<SpeechService>(SpeechService);

      expect(service.isSTTAvailable()).toBe(true);
    });

    it("should report STT as unavailable when disabled in config", async () => {
      const config = createTestConfig();
      config.stt.enabled = false;
      const module = await createTestModule({
        sttProvider: createMockSttProvider(),
        config,
      });
      const service = module.get<SpeechService>(SpeechService);

      expect(service.isSTTAvailable()).toBe(false);
    });

    it("should report STT as unavailable when no provider registered", async () => {
      const module = await createTestModule({ sttProvider: null });
      const service = module.get<SpeechService>(SpeechService);

      expect(service.isSTTAvailable()).toBe(false);
    });

    it("should report TTS as available when at least one tier is enabled with a provider", async () => {
      const ttsProviders = new Map<SpeechTier, ITTSProvider>([
        ["default", createMockTtsProvider("default")],
      ]);
      const module = await createTestModule({ ttsProviders });
      const service = module.get<SpeechService>(SpeechService);

      expect(service.isTTSAvailable()).toBe(true);
    });

    it("should report TTS as unavailable when no providers registered", async () => {
      const config = createTestConfig();
      config.tts.default.enabled = false;
      config.tts.premium.enabled = false;
      config.tts.fallback.enabled = false;
      const module = await createTestModule({ ttsProviders: new Map(), config });
      const service = module.get<SpeechService>(SpeechService);

      expect(service.isTTSAvailable()).toBe(false);
    });
  });
});
