import { describe, it, expect, vi, beforeEach } from "vitest";
import { TRPCError } from "@trpc/server";

const mockQueryResult = vi.fn().mockResolvedValue([]);

function createChain(initialPromise?: any): any {
  const p = typeof initialPromise !== "undefined" ? initialPromise : mockQueryResult();
  return new Proxy(p, {
    get(target, prop) {
      if (prop === "then" || prop === "catch" || prop === "finally") {
        return Reflect.get(target, prop).bind(target);
      }
      return () => createChain(p);
    },
  });
}

vi.mock("~/server/db", () => ({
  db: {
    select: vi.fn(() => createChain()),
    insert: vi.fn(() => createChain()),
    update: vi.fn(() => createChain()),
  },
}));

vi.mock("./voiceprint/storage", () => ({
  saveAudio: vi.fn(),
  getAudioUrl: vi.fn(),
  deleteFile: vi.fn(),
  computeHash: vi.fn(),
  deleteAudio: vi.fn(),
}));

vi.mock("./voiceprint/ml.engine", () => ({
  preprocessAudio: vi.fn(),
  detectSynthetic: vi.fn(),
  matchVoice: vi.fn(),
  generateEmbedding: vi.fn(),
}));

const storage = await import("./voiceprint/storage");
const ml = await import("./voiceprint/ml.engine");

const mockEnrollment = {
  id: "enr-1",
  userId: "user-1",
  name: "My Voice",
  voiceHash: "hash-123",
  audioMetadata: { filePath: "/some/path.wav", duration: 2.5, sampleRate: 16000 },
  isActive: true,
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockAnalysis = {
  id: "ana-1",
  enrollmentId: null,
  userId: "user-1",
  audioHash: "audio-hash-1",
  isSynthetic: false,
  confidence: 0.95,
  analysisResult: { verdict: "NATURAL", score: 0.05, matchedSimilarity: null },
  audioUrl: "/uploads/voiceprint/user-1/audio-hash-1.wav",
  createdAt: new Date(),
};

const mockJob = {
  id: "job-1",
  userId: "user-1",
  analysisType: "BATCH",
  audioFilePath: "/path/to/audio.wav",
  status: "PENDING",
  errorMessage: null,
  completedAt: null,
  createdAt: new Date(),
};

const mockResult = {
  id: "res-1",
  analysisJobId: "job-1",
  syntheticScore: 0.1,
  verdict: "NATURAL",
  confidence: 0.95,
  processingTimeMs: 1500,
  matchedEnrollmentId: null,
  matchedSimilarity: null,
  modelVersion: "v1",
  createdAt: new Date(),
};

beforeEach(() => {
  vi.clearAllMocks();
});

describe("getEnrollments", () => {
  it("returns enrollments for the user", async () => {
    mockQueryResult.mockResolvedValueOnce([mockEnrollment]);

    const { getEnrollments } = await import("./voiceprint.service");
    const result = await getEnrollments("user-1");
    expect(result).toEqual([mockEnrollment]);
  });
});

describe("createEnrollment", () => {
  it("saves audio and creates a DB record", async () => {
    vi.mocked(storage.saveAudio).mockResolvedValue({
      hash: "audio-hash",
      filePath: "/path/file.wav",
    });
    vi.mocked(ml.preprocessAudio).mockResolvedValue({
      duration: 2.5, sampleRate: 16000, channels: 1, rawPcm: Buffer.from("test"),
    });
    vi.mocked(ml.generateEmbedding).mockResolvedValue({
      vector: new Float64Array(256), hash: "embed-hash",
    });
    mockQueryResult.mockResolvedValueOnce([mockEnrollment]);

    const { createEnrollment } = await import("./voiceprint.service");
    const result = await createEnrollment("user-1", "My Voice", "dGVzdC1hdWRpbw==");
    expect(result).toEqual(mockEnrollment);
    expect(storage.saveAudio).toHaveBeenCalledWith("user-1", Buffer.from("test-audio"));
    expect(ml.generateEmbedding).toHaveBeenCalled();
  });
});

describe("deleteEnrollment", () => {
  it("soft deletes enrollment and removes audio file", async () => {
    mockQueryResult
      .mockResolvedValueOnce([mockEnrollment])
      .mockResolvedValueOnce([{ ...mockEnrollment, isActive: false }]);

    const { deleteEnrollment } = await import("./voiceprint.service");
    const result = await deleteEnrollment("user-1", "enr-1");
    expect(result.isActive).toBe(false);
    expect(storage.deleteFile).toHaveBeenCalledWith("/some/path.wav");
  });

  it("throws NOT_FOUND if enrollment does not belong to user", async () => {
    mockQueryResult.mockResolvedValueOnce([]);

    const { deleteEnrollment } = await import("./voiceprint.service");
    await expect(deleteEnrollment("user-1", "nonexistent")).rejects.toThrow(TRPCError);
  });
});

describe("analyzeAudio", () => {
  it("returns verdict and confidence for analysis", async () => {
    vi.mocked(storage.saveAudio).mockResolvedValue({
      hash: "audio-hash", filePath: "/path/file.wav",
    });
    vi.mocked(storage.getAudioUrl).mockReturnValue("/uploads/voiceprint/user-1/audio-hash.wav");
    vi.mocked(ml.preprocessAudio).mockResolvedValue({
      duration: 2.5, sampleRate: 16000, channels: 1, rawPcm: Buffer.from("test"),
    });
    vi.mocked(ml.detectSynthetic).mockResolvedValue({
      isSynthetic: false, confidence: 0.95, score: 0.05,
    });
    mockQueryResult.mockResolvedValueOnce([mockAnalysis]);

    const { analyzeAudio } = await import("./voiceprint.service");
    const result = await analyzeAudio("user-1", "dGVzdC1hdWRpbw==");
    expect(result.verdict).toBe("NATURAL");
    expect(result.confidence).toBe(0.95);
    expect(result.isSynthetic).toBe(false);
    expect(result.score).toBe(0.05);
  });
});

describe("getAnalyses", () => {
  it("returns paginated analyses", async () => {
    mockQueryResult
      .mockResolvedValueOnce([{ count: 1 }])
      .mockResolvedValueOnce([mockAnalysis]);

    const { getAnalyses } = await import("./voiceprint.service");
    const result = await getAnalyses("user-1", { page: 1, limit: 10 });
    expect(result.items).toEqual([mockAnalysis]);
    expect(result.total).toBe(1);
    expect(result.page).toBe(1);
  });
});

describe("getAnalysisResult", () => {
  it("returns detailed analysis", async () => {
    mockQueryResult.mockResolvedValueOnce([mockAnalysis]);

    const { getAnalysisResult } = await import("./voiceprint.service");
    const result = await getAnalysisResult("user-1", "ana-1");
    expect(result).toEqual(mockAnalysis);
  });

  it("throws NOT_FOUND for non-existent analysis", async () => {
    mockQueryResult.mockResolvedValueOnce([]);

    const { getAnalysisResult } = await import("./voiceprint.service");
    await expect(getAnalysisResult("user-1", "nonexistent")).rejects.toThrow(TRPCError);
  });
});

describe("getJobStatus", () => {
  it("returns job status with result when completed", async () => {
    const completedJob = { ...mockJob, status: "COMPLETED" };
    mockQueryResult
      .mockResolvedValueOnce([completedJob])
      .mockResolvedValueOnce([mockResult]);

    const { getJobStatus } = await import("./voiceprint.service");
    const result = await getJobStatus("user-1", "job-1");
    expect(result.status).toBe("COMPLETED");
    expect(result.result).toEqual(mockResult);
  });

  it("throws NOT_FOUND for non-existent job", async () => {
    mockQueryResult.mockResolvedValueOnce([]);

    const { getJobStatus } = await import("./voiceprint.service");
    await expect(getJobStatus("user-1", "nonexistent")).rejects.toThrow(TRPCError);
  });
});

describe("createBatchJob", () => {
  it("creates a batch analysis job", async () => {
    mockQueryResult.mockResolvedValueOnce([mockJob]);

    const { createBatchJob } = await import("./voiceprint.service");
    const result = await createBatchJob("user-1", "/path/to/audio.wav");
    expect(result.id).toBe("job-1");
    expect(result.analysisType).toBe("BATCH");
    expect(result.status).toBe("PENDING");
  });
});
