/**
 * BaseTTSProvider Unit Tests
 *
 * Tests the abstract base class for OpenAI-compatible TTS providers.
 * Uses a concrete test implementation to exercise the base class logic.
 *
 * Issue #391
 */

import { describe, it, expect, beforeEach, vi, type Mock } from "vitest";
import { BaseTTSProvider } from "./base-tts.provider";
import type { SpeechTier, SynthesizeOptions, AudioFormat } from "../interfaces/speech-types";

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

const mockCreate = vi.fn();

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

// ==========================================
// Concrete test implementation
// ==========================================

class TestTTSProvider extends BaseTTSProvider {
  readonly name = "test-provider";
  readonly tier: SpeechTier = "default";

  constructor(baseURL: string, defaultVoice?: string, defaultFormat?: AudioFormat) {
    super(baseURL, defaultVoice, defaultFormat);
  }
}

// ==========================================
// Test helpers
// ==========================================

/**
 * Create a mock Response-like object that mimics OpenAI SDK's audio.speech.create() return.
 * The OpenAI SDK returns a Response object with arrayBuffer() method.
 */
function createMockAudioResponse(audioData: Uint8Array): { arrayBuffer: Mock } {
  return {
    arrayBuffer: vi.fn().mockResolvedValue(audioData.buffer),
  };
}

describe("BaseTTSProvider", () => {
  let provider: TestTTSProvider;

  const testBaseURL = "http://localhost:8880/v1";
  const testVoice = "af_heart";
  const testFormat: AudioFormat = "mp3";

  beforeEach(() => {
    vi.clearAllMocks();
    provider = new TestTTSProvider(testBaseURL, testVoice, testFormat);
  });

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

  describe("constructor", () => {
    it("should create an instance with provided configuration", () => {
      expect(provider).toBeDefined();
      expect(provider.name).toBe("test-provider");
      expect(provider.tier).toBe("default");
    });

    it("should use default voice 'alloy' when none provided", () => {
      const defaultProvider = new TestTTSProvider(testBaseURL);
      expect(defaultProvider).toBeDefined();
    });

    it("should use default format 'mp3' when none provided", () => {
      const defaultProvider = new TestTTSProvider(testBaseURL, "voice-1");
      expect(defaultProvider).toBeDefined();
    });
  });

  // ==========================================
  // synthesize()
  // ==========================================

  describe("synthesize", () => {
    it("should synthesize text and return a SynthesisResult with audio buffer", async () => {
      const audioBytes = new Uint8Array([0x49, 0x44, 0x33, 0x04, 0x00]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      const result = await provider.synthesize("Hello, world!");

      expect(result).toBeDefined();
      expect(result.audio).toBeInstanceOf(Buffer);
      expect(result.audio.length).toBe(audioBytes.length);
      expect(result.format).toBe("mp3");
      expect(result.voice).toBe("af_heart");
      expect(result.tier).toBe("default");
    });

    it("should pass correct parameters to OpenAI SDK", async () => {
      const audioBytes = new Uint8Array([0x01, 0x02]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      await provider.synthesize("Test text");

      expect(mockCreate).toHaveBeenCalledWith({
        model: "tts-1",
        input: "Test text",
        voice: "af_heart",
        response_format: "mp3",
        speed: 1.0,
      });
    });

    it("should use custom voice from options", async () => {
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      const options: SynthesizeOptions = { voice: "custom_voice" };
      const result = await provider.synthesize("Hello", options);

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ voice: "custom_voice" }));
      expect(result.voice).toBe("custom_voice");
    });

    it("should use custom format from options", async () => {
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      const options: SynthesizeOptions = { format: "wav" };
      const result = await provider.synthesize("Hello", options);

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ response_format: "wav" }));
      expect(result.format).toBe("wav");
    });

    it("should use custom speed from options", async () => {
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      const options: SynthesizeOptions = { speed: 1.5 };
      await provider.synthesize("Hello", options);

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ speed: 1.5 }));
    });

    it("should throw an error when synthesis fails", async () => {
      mockCreate.mockRejectedValue(new Error("Connection refused"));

      await expect(provider.synthesize("Hello")).rejects.toThrow(
        "TTS synthesis failed for test-provider: Connection refused"
      );
    });

    it("should throw an error when response arrayBuffer fails", async () => {
      const mockResponse = {
        arrayBuffer: vi.fn().mockRejectedValue(new Error("Read error")),
      };
      mockCreate.mockResolvedValue(mockResponse);

      await expect(provider.synthesize("Hello")).rejects.toThrow(
        "TTS synthesis failed for test-provider: Read error"
      );
    });

    it("should handle empty text input gracefully", async () => {
      const audioBytes = new Uint8Array([]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      const result = await provider.synthesize("");

      expect(result.audio).toBeInstanceOf(Buffer);
      expect(result.audio.length).toBe(0);
    });

    it("should handle non-Error exceptions", async () => {
      mockCreate.mockRejectedValue("string error");

      await expect(provider.synthesize("Hello")).rejects.toThrow(
        "TTS synthesis failed for test-provider: string error"
      );
    });
  });

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

  describe("listVoices", () => {
    it("should return default voice list with the configured default voice", async () => {
      const voices = await provider.listVoices();

      expect(voices).toBeInstanceOf(Array);
      expect(voices.length).toBeGreaterThan(0);

      const defaultVoice = voices.find((v) => v.isDefault === true);
      expect(defaultVoice).toBeDefined();
      expect(defaultVoice?.id).toBe("af_heart");
      expect(defaultVoice?.tier).toBe("default");
    });

    it("should set tier correctly on all returned voices", async () => {
      const voices = await provider.listVoices();

      for (const voice of voices) {
        expect(voice.tier).toBe("default");
      }
    });
  });

  // ==========================================
  // isHealthy()
  // ==========================================

  describe("isHealthy", () => {
    it("should return true when the TTS server is reachable", async () => {
      // Mock global fetch for health check
      const mockFetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
      });
      vi.stubGlobal("fetch", mockFetch);

      const healthy = await provider.isHealthy();

      expect(healthy).toBe(true);
      expect(mockFetch).toHaveBeenCalled();

      vi.unstubAllGlobals();
    });

    it("should return false when the TTS server is unreachable", async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
      vi.stubGlobal("fetch", mockFetch);

      const healthy = await provider.isHealthy();

      expect(healthy).toBe(false);

      vi.unstubAllGlobals();
    });

    it("should return false when the TTS server returns an error status", async () => {
      const mockFetch = vi.fn().mockResolvedValue({
        ok: false,
        status: 503,
      });
      vi.stubGlobal("fetch", mockFetch);

      const healthy = await provider.isHealthy();

      expect(healthy).toBe(false);

      vi.unstubAllGlobals();
    });

    it("should use the base URL for the health check", async () => {
      const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
      vi.stubGlobal("fetch", mockFetch);

      await provider.isHealthy();

      // Should call a health-related endpoint at the base URL
      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("localhost:8880");

      vi.unstubAllGlobals();
    });

    it("should set a timeout for the health check", async () => {
      const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
      vi.stubGlobal("fetch", mockFetch);

      await provider.isHealthy();

      // Should pass an AbortSignal for timeout
      const fetchOptions = mockFetch.mock.calls[0][1] as RequestInit;
      expect(fetchOptions.signal).toBeDefined();

      vi.unstubAllGlobals();
    });
  });

  // ==========================================
  // Default values
  // ==========================================

  describe("default values", () => {
    it("should use 'alloy' as default voice when none specified", async () => {
      const defaultProvider = new TestTTSProvider(testBaseURL);
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      await defaultProvider.synthesize("Hello");

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ voice: "alloy" }));
    });

    it("should use 'mp3' as default format when none specified", async () => {
      const defaultProvider = new TestTTSProvider(testBaseURL);
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      await defaultProvider.synthesize("Hello");

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ response_format: "mp3" }));
    });

    it("should use speed 1.0 as default speed", async () => {
      const audioBytes = new Uint8Array([0x01]);
      mockCreate.mockResolvedValue(createMockAudioResponse(audioBytes));

      await provider.synthesize("Hello");

      expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ speed: 1.0 }));
    });
  });
});
