import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import {
  CreateEnrollmentSchema,
  DeleteEnrollmentSchema,
  AnalyzeAudioSchema,
  AnalysisFilterSchema,
  AnalysisResultSchema,
  JobStatusSchema,
} from "../schemas/voiceprint";

vi.mock("~/server/services/voiceprint.service", () => ({
  getEnrollments: vi.fn(),
  createEnrollment: vi.fn(),
  deleteEnrollment: vi.fn(),
  analyzeAudio: vi.fn(),
  getAnalyses: vi.fn(),
  getAnalysisResult: vi.fn(),
  getJobStatus: vi.fn(),
}));

import * as voiceprintService from "~/server/services/voiceprint.service";

const mockGetEnrollments = vi.mocked(voiceprintService.getEnrollments);
const mockCreateEnrollment = vi.mocked(voiceprintService.createEnrollment);
const mockDeleteEnrollment = vi.mocked(voiceprintService.deleteEnrollment);
const mockAnalyzeAudio = vi.mocked(voiceprintService.analyzeAudio);
const mockGetAnalyses = vi.mocked(voiceprintService.getAnalyses);
const mockGetAnalysisResult = vi.mocked(voiceprintService.getAnalysisResult);
const mockGetJobStatus = vi.mocked(voiceprintService.getJobStatus);

type User = {
  id: string; email: string; name: string | null; image: string | null;
  role: string; emailVerified: Date | null; deletedAt: Date | null;
  stripeCustomerId: string | null;
  createdAt: Date; updatedAt: Date;
};
type Ctx = { db: object; user: User | null; apiKey: string | null };

function createCaller(user: User | null) {
  const t = initTRPC.context<Ctx>().create();
  const isAuthed = t.middleware(({ ctx, next }) => {
    if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
    return next({ ctx: { ...ctx, user: ctx.user } });
  });

  const router = t.router({
    getEnrollments: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetEnrollments(ctx.user.id);
    }),
    createEnrollment: t.procedure.use(isAuthed)
      .input(wrap(CreateEnrollmentSchema))
      .mutation(async ({ ctx, input }) => {
        return mockCreateEnrollment(ctx.user.id, input.name, input.audioBase64);
      }),
    deleteEnrollment: t.procedure.use(isAuthed)
      .input(wrap(DeleteEnrollmentSchema))
      .mutation(async ({ ctx, input }) => {
        return mockDeleteEnrollment(ctx.user.id, input.enrollmentId);
      }),
    analyzeAudio: t.procedure.use(isAuthed)
      .input(wrap(AnalyzeAudioSchema))
      .mutation(async ({ ctx, input }) => {
        return mockAnalyzeAudio(ctx.user.id, input.audioBase64, input.enrollmentId);
      }),
    getAnalyses: t.procedure.use(isAuthed)
      .input(wrap(AnalysisFilterSchema))
      .query(async ({ ctx, input }) => {
        return mockGetAnalyses(ctx.user.id, input);
      }),
    getAnalysisResult: t.procedure.use(isAuthed)
      .input(wrap(AnalysisResultSchema))
      .query(async ({ ctx, input }) => {
        return mockGetAnalysisResult(ctx.user.id, input.analysisId);
      }),
    getJobStatus: t.procedure.use(isAuthed)
      .input(wrap(JobStatusSchema))
      .query(async ({ ctx, input }) => {
        return mockGetJobStatus(ctx.user.id, input.jobId);
      }),
  });

  const caller = t.createCallerFactory(router);
  return caller({ db: {} as never, user, apiKey: null });
}

const baseUser: User = {
  id: "user-1", email: "a@b.com", name: "Test", image: null,
  role: "user", emailVerified: null, deletedAt: null,
  stripeCustomerId: null,
  createdAt: new Date(), updatedAt: new Date(),
};

function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

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

describe("voiceprint.getEnrollments", () => {
  it("returns enrollments for authenticated user", async () => {
    const items = [{ id: "enr-1", name: "My Voice" }];
    mockGetEnrollments.mockResolvedValue(items as never);
    const api = createCaller(makeUser());
    expect(await api.getEnrollments()).toEqual(items);
  });

  it("rejects unauthenticated", async () => {
    const api = createCaller(null);
    await expect(api.getEnrollments()).rejects.toThrow(TRPCError);
  });
});

describe("voiceprint.createEnrollment", () => {
  it("creates an enrollment", async () => {
    const enrollment = { id: "enr-1", name: "My Voice" };
    mockCreateEnrollment.mockResolvedValue(enrollment as never);
    const api = createCaller(makeUser());
    const result = await api.createEnrollment({ name: "My Voice", audioBase64: "dGVzdA==" });
    expect(result).toEqual(enrollment);
  });

  it("rejects empty name", async () => {
    const api = createCaller(makeUser());
    await expect(
      api.createEnrollment({ name: "", audioBase64: "dGVzdA==" }),
    ).rejects.toThrow();
  });
});

describe("voiceprint.deleteEnrollment", () => {
  it("deletes enrollment", async () => {
    mockDeleteEnrollment.mockResolvedValue({ id: "enr-1", isActive: false } as never);
    const api = createCaller(makeUser());
    const result = await api.deleteEnrollment({ enrollmentId: "enr-1" });
    expect(result.isActive).toBe(false);
  });
});

describe("voiceprint.analyzeAudio", () => {
  it("analyzes audio and returns result", async () => {
    const result = { id: "ana-1", verdict: "NATURAL", confidence: 0.95 };
    mockAnalyzeAudio.mockResolvedValue(result as never);
    const api = createCaller(makeUser());
    const res = await api.analyzeAudio({ audioBase64: "dGVzdA==" });
    expect(res.verdict).toBe("NATURAL");
  });

  it("accepts optional enrollmentId", async () => {
    mockAnalyzeAudio.mockResolvedValue({ id: "ana-1" } as never);
    const api = createCaller(makeUser());
    await api.analyzeAudio({ audioBase64: "dGVzdA==", enrollmentId: "enr-1" });
    expect(mockAnalyzeAudio).toHaveBeenCalledWith("user-1", "dGVzdA==", "enr-1");
  });
});

describe("voiceprint.getAnalyses", () => {
  it("returns paginated analyses", async () => {
    const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
    mockGetAnalyses.mockResolvedValue(data);
    const api = createCaller(makeUser());
    const result = await api.getAnalyses({ page: 1, limit: 20 });
    expect(result.total).toBe(0);
  });

  it("passes verdict filter", async () => {
    mockGetAnalyses.mockResolvedValue({ items: [], total: 0, page: 1, limit: 20, totalPages: 0 });
    const api = createCaller(makeUser());
    await api.getAnalyses({ verdict: "SYNTHETIC" });
    expect(mockGetAnalyses).toHaveBeenCalledWith("user-1", { verdict: "SYNTHETIC", page: 1, limit: 20 });
  });
});

describe("voiceprint.getAnalysisResult", () => {
  it("returns analysis details", async () => {
    const analysis = { id: "ana-1", verdict: "NATURAL", confidence: 0.95 };
    mockGetAnalysisResult.mockResolvedValue(analysis as never);
    const api = createCaller(makeUser());
    const result = await api.getAnalysisResult({ analysisId: "ana-1" });
    expect(result.id).toBe("ana-1");
  });
});

describe("voiceprint.getJobStatus", () => {
  it("returns job status", async () => {
    const job = { id: "job-1", status: "RUNNING", result: null };
    mockGetJobStatus.mockResolvedValue(job as never);
    const api = createCaller(makeUser());
    const result = await api.getJobStatus({ jobId: "job-1" });
    expect(result.status).toBe("RUNNING");
  });
});
