import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock database
vi.mock("@/lib/db", () => ({
  db: {
    tokenConversation: { findMany: vi.fn() },
    tokenMessage: { findMany: vi.fn() },
  },
}));

// Mock logger
vi.mock("@/lib/logger", () => ({
  logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}));

import { db } from "@/lib/db";
import {
  getModelPerformance,
  getModelTrends,
  formatPerformanceContext,
} from "@/lib/token/performance";
import type { ModelPerformance } from "@/lib/token/performance";

/* ── getModelPerformance ───────────────────────────────────────────── */

describe("getModelPerformance", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should return empty array when no conversations exist", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([]);

    const result = await getModelPerformance("user_1");

    expect(result).toEqual([]);
  });

  it("should return empty array when no assistant messages exist", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([]);

    const result = await getModelPerformance("user_1");

    expect(result).toEqual([]);
  });

  it("should aggregate per-model metrics correctly", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "gemini-2.5-flash", costUsd: 0.001, latencyMs: 100, inputTokens: 500, outputTokens: 200 },
      { model: "gemini-2.5-flash", costUsd: 0.002, latencyMs: 200, inputTokens: 600, outputTokens: 300 },
      { model: "gemini-2.5-flash", costUsd: 0.003, latencyMs: 150, inputTokens: 400, outputTokens: 100 },
      { model: "claude-sonnet-4.6", costUsd: 0.010, latencyMs: 500, inputTokens: 1000, outputTokens: 800 },
    ] as any);

    const result = await getModelPerformance("user_1");

    expect(result).toHaveLength(2);

    // Sorted by messageCount desc: gemini (3) before claude (1)
    const gemini = result[0];
    expect(gemini.model).toBe("gemini-2.5-flash");
    expect(gemini.messageCount).toBe(3);
    expect(gemini.avgLatencyMs).toBe(150); // (100+200+150)/3 = 150
    expect(gemini.totalCostUsd).toBe(0.006);
    expect(gemini.avgCostPerMessage).toBe(0.002);
    expect(gemini.totalInputTokens).toBe(1500);
    expect(gemini.totalOutputTokens).toBe(600);

    const claude = result[1];
    expect(claude.model).toBe("claude-sonnet-4.6");
    expect(claude.messageCount).toBe(1);
    expect(claude.avgLatencyMs).toBe(500);
    expect(claude.totalCostUsd).toBe(0.01);
    expect(claude.avgCostPerMessage).toBe(0.01);
    expect(claude.totalInputTokens).toBe(1000);
    expect(claude.totalOutputTokens).toBe(800);
  });

  it("should calculate P95 latency correctly", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    // 20 messages: latencies 10, 20, 30, ... 200
    const messages = Array.from({ length: 20 }, (_, i) => ({
      model: "test-model",
      costUsd: 0.001,
      latencyMs: (i + 1) * 10,
      inputTokens: 100,
      outputTokens: 50,
    }));

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue(messages as any);

    const result = await getModelPerformance("user_1");

    expect(result).toHaveLength(1);
    // P95 index: ceil(20 * 0.95) - 1 = 19 - 1 = 18 (0-indexed). Sorted: [10,20,...,200], index 18 = 190
    expect(result[0].p95LatencyMs).toBe(190);
  });

  it("should handle P95 for single message", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "solo-model", costUsd: 0.005, latencyMs: 300, inputTokens: 200, outputTokens: 100 },
    ] as any);

    const result = await getModelPerformance("user_1");

    expect(result).toHaveLength(1);
    expect(result[0].p95LatencyMs).toBe(300);
    expect(result[0].avgLatencyMs).toBe(300);
  });

  it("should sort by messageCount descending", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "rare-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
      { model: "popular-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
      { model: "popular-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
      { model: "popular-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
      { model: "mid-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
      { model: "mid-model", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
    ] as any);

    const result = await getModelPerformance("user_1");

    expect(result[0].model).toBe("popular-model");
    expect(result[0].messageCount).toBe(3);
    expect(result[1].model).toBe("mid-model");
    expect(result[1].messageCount).toBe(2);
    expect(result[2].model).toBe("rare-model");
    expect(result[2].messageCount).toBe(1);
  });

  it("should filter by userId through conversations", async () => {
    // Only conversations for user_1 are returned
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_user1" }] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "model-a", costUsd: 0.001, latencyMs: 100, inputTokens: 100, outputTokens: 50 },
    ] as any);

    await getModelPerformance("user_1");

    // Verify the conversation query filters by userId
    expect(db.tokenConversation.findMany).toHaveBeenCalledWith({
      where: { userId: "user_1" },
      select: { id: true },
    });

    // Verify the message query uses the conversation IDs
    expect(db.tokenMessage.findMany).toHaveBeenCalledWith({
      where: {
        conversationId: { in: ["conv_user1"] },
        role: "assistant",
        model: { not: null },
      },
      select: {
        model: true,
        costUsd: true,
        latencyMs: true,
        inputTokens: true,
        outputTokens: true,
      },
    });
  });
});

/* ── getModelTrends ────────────────────────────────────────────────── */

describe("getModelTrends", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should return empty array when no conversations exist", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([]);

    const result = await getModelTrends("user_1");

    expect(result).toEqual([]);
  });

  it("should return empty array when no messages in range", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([]);

    const result = await getModelTrends("user_1", 7);

    expect(result).toEqual([]);
  });

  it("should group messages by date and model", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    const day1 = new Date("2026-02-20T10:00:00Z");
    const day1b = new Date("2026-02-20T14:00:00Z");
    const day2 = new Date("2026-02-21T09:00:00Z");

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "model-a", costUsd: 0.001, latencyMs: 100, createdAt: day1 },
      { model: "model-a", costUsd: 0.002, latencyMs: 200, createdAt: day1b },
      { model: "model-b", costUsd: 0.005, latencyMs: 300, createdAt: day1 },
      { model: "model-a", costUsd: 0.003, latencyMs: 150, createdAt: day2 },
    ] as any);

    const result = await getModelTrends("user_1");

    expect(result).toHaveLength(3);

    // Sorted: 2026-02-20|model-a, 2026-02-20|model-b, 2026-02-21|model-a
    const day1ModelA = result.find((r) => r.date === "2026-02-20" && r.model === "model-a");
    expect(day1ModelA).toBeDefined();
    expect(day1ModelA!.messageCount).toBe(2);
    expect(day1ModelA!.totalCostUsd).toBe(0.003);
    expect(day1ModelA!.avgLatencyMs).toBe(150); // (100+200)/2

    const day1ModelB = result.find((r) => r.date === "2026-02-20" && r.model === "model-b");
    expect(day1ModelB).toBeDefined();
    expect(day1ModelB!.messageCount).toBe(1);
    expect(day1ModelB!.totalCostUsd).toBe(0.005);
    expect(day1ModelB!.avgLatencyMs).toBe(300);

    const day2ModelA = result.find((r) => r.date === "2026-02-21" && r.model === "model-a");
    expect(day2ModelA).toBeDefined();
    expect(day2ModelA!.messageCount).toBe(1);
    expect(day2ModelA!.totalCostUsd).toBe(0.003);
    expect(day2ModelA!.avgLatencyMs).toBe(150);
  });

  it("should sort by date ascending then model", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { model: "z-model", costUsd: 0.001, latencyMs: 100, createdAt: new Date("2026-02-22T10:00:00Z") },
      { model: "a-model", costUsd: 0.001, latencyMs: 100, createdAt: new Date("2026-02-22T12:00:00Z") },
      { model: "a-model", costUsd: 0.001, latencyMs: 100, createdAt: new Date("2026-02-21T10:00:00Z") },
    ] as any);

    const result = await getModelTrends("user_1");

    expect(result[0].date).toBe("2026-02-21");
    expect(result[0].model).toBe("a-model");
    expect(result[1].date).toBe("2026-02-22");
    expect(result[1].model).toBe("a-model");
    expect(result[2].date).toBe("2026-02-22");
    expect(result[2].model).toBe("z-model");
  });

  it("should default to 30 days and accept custom range", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([]);

    // Default 30 days
    await getModelTrends("user_1");
    const defaultCall = vi.mocked(db.tokenMessage.findMany).mock.calls[0][0] as any;
    const defaultSince = defaultCall.where.createdAt.gte as Date;
    const now = new Date();
    const diffMs = now.getTime() - defaultSince.getTime();
    const diffDays = diffMs / (1000 * 60 * 60 * 24);
    // Since is set to midnight 30 days ago, diff from "now" is 30-31 days
    expect(diffDays).toBeGreaterThanOrEqual(30);
    expect(diffDays).toBeLessThan(31.1);

    vi.clearAllMocks();
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_1" }] as any);
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([]);

    // Custom 7 days
    await getModelTrends("user_1", 7);
    const customCall = vi.mocked(db.tokenMessage.findMany).mock.calls[0][0] as any;
    const customSince = customCall.where.createdAt.gte as Date;
    const customDiffMs = now.getTime() - customSince.getTime();
    const customDiffDays = customDiffMs / (1000 * 60 * 60 * 24);
    expect(customDiffDays).toBeGreaterThanOrEqual(7);
    expect(customDiffDays).toBeLessThan(8.1);
  });

  it("should filter by userId through conversations", async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([{ id: "conv_42" }] as any);
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([]);

    await getModelTrends("user_42");

    expect(db.tokenConversation.findMany).toHaveBeenCalledWith({
      where: { userId: "user_42" },
      select: { id: true },
    });
  });
});

/* ── formatPerformanceContext ───────────────────────────────────────── */

describe("formatPerformanceContext", () => {
  it("should return fallback message when no data", () => {
    const result = formatPerformanceContext([]);

    expect(result).toBe("No model performance data available yet.");
  });

  it("should format top 5 models with metrics", () => {
    const perf: ModelPerformance[] = [
      {
        model: "gemini-2.5-flash",
        messageCount: 100,
        avgLatencyMs: 120,
        p95LatencyMs: 250,
        totalCostUsd: 0.5,
        avgCostPerMessage: 0.005,
        totalInputTokens: 50000,
        totalOutputTokens: 25000,
      },
      {
        model: "claude-sonnet-4.6",
        messageCount: 50,
        avgLatencyMs: 400,
        p95LatencyMs: 800,
        totalCostUsd: 1.0,
        avgCostPerMessage: 0.02,
        totalInputTokens: 30000,
        totalOutputTokens: 20000,
      },
      {
        model: "deepseek-r1",
        messageCount: 30,
        avgLatencyMs: 600,
        p95LatencyMs: 1200,
        totalCostUsd: 0.3,
        avgCostPerMessage: 0.01,
        totalInputTokens: 20000,
        totalOutputTokens: 15000,
      },
    ];

    const result = formatPerformanceContext(perf);

    expect(result).toContain("### Model Performance (Top 5)");
    expect(result).toContain("gemini-2.5-flash: 100 msgs, avg 120ms, $0.0050/msg");
    expect(result).toContain("claude-sonnet-4.6: 50 msgs, avg 400ms, $0.0200/msg");
    expect(result).toContain("deepseek-r1: 30 msgs, avg 600ms, $0.0100/msg");
  });

  it("should limit to top 5 models", () => {
    const perf: ModelPerformance[] = Array.from({ length: 8 }, (_, i) => ({
      model: `model-${i}`,
      messageCount: 100 - i * 10,
      avgLatencyMs: 100,
      p95LatencyMs: 200,
      totalCostUsd: 0.01,
      avgCostPerMessage: 0.001,
      totalInputTokens: 1000,
      totalOutputTokens: 500,
    }));

    const result = formatPerformanceContext(perf);

    // Should include models 0-4 (top 5)
    expect(result).toContain("model-0");
    expect(result).toContain("model-4");
    // Should NOT include models 5-7
    expect(result).not.toContain("model-5");
    expect(result).not.toContain("model-7");
  });

  it("should format cost with 4 decimal places", () => {
    const perf: ModelPerformance[] = [
      {
        model: "test-model",
        messageCount: 10,
        avgLatencyMs: 200,
        p95LatencyMs: 400,
        totalCostUsd: 0.1234,
        avgCostPerMessage: 0.0123,
        totalInputTokens: 5000,
        totalOutputTokens: 2500,
      },
    ];

    const result = formatPerformanceContext(perf);

    expect(result).toContain("$0.0123/msg");
  });
});
