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

// Mock db before importing the module under test
vi.mock("@/lib/db", () => ({
  db: {
    marketplaceBillingEvent: {
      create: vi.fn(),
      findMany: vi.fn(),
      findFirst: vi.fn(),
      count: vi.fn(),
      updateMany: vi.fn(),
    },
  },
}));

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

import { db } from "@/lib/db";
import {
  calculateFees,
  recordBillingEvent,
  getOperatorEarnings,
  getBuyerSpending,
  getPendingPayout,
  markEventsPaidOut,
  PLATFORM_FEE_RATE,
  MIN_PAYOUT_CENTS,
} from "@/lib/marketplace-billing";

// ---------------------------------------------------------------------------
// calculateFees — pure function tests
// ---------------------------------------------------------------------------

describe("calculateFees", () => {
  it("applies 20% platform fee (ceil) and 80% operator share", () => {
    const result = calculateFees(100);
    expect(result.priceCents).toBe(100);
    expect(result.platformFeeCents).toBe(20);
    expect(result.operatorShareCents).toBe(80);
  });

  it("rounds platform fee up (ceil) on non-integer splits", () => {
    // 10 * 0.20 = 2 (exact)
    const exact = calculateFees(10);
    expect(exact.platformFeeCents).toBe(2);
    expect(exact.operatorShareCents).toBe(8);

    // 3 * 0.20 = 0.6 -> ceil = 1
    const fractional = calculateFees(3);
    expect(fractional.platformFeeCents).toBe(1);
    expect(fractional.operatorShareCents).toBe(2);
  });

  it("handles zero price", () => {
    const result = calculateFees(0);
    expect(result.priceCents).toBe(0);
    expect(result.platformFeeCents).toBe(0);
    expect(result.operatorShareCents).toBe(0);
  });

  it("handles 1 cent (minimum possible)", () => {
    const result = calculateFees(1);
    // 1 * 0.20 = 0.2 -> ceil = 1. Operator gets 0.
    expect(result.platformFeeCents).toBe(1);
    expect(result.operatorShareCents).toBe(0);
  });

  it("throws on negative price", () => {
    expect(() => calculateFees(-5)).toThrow("priceCents must be non-negative");
  });

  it("sum of fee and share always equals price", () => {
    // Test a range of values
    for (const price of [1, 2, 3, 7, 10, 15, 50, 99, 100, 150, 999, 10000]) {
      const { priceCents, platformFeeCents, operatorShareCents } = calculateFees(price);
      expect(platformFeeCents + operatorShareCents).toBe(priceCents);
    }
  });

  it("exports correct constants", () => {
    expect(PLATFORM_FEE_RATE).toBe(0.2);
    expect(MIN_PAYOUT_CENTS).toBe(100);
  });
});

// ---------------------------------------------------------------------------
// recordBillingEvent
// ---------------------------------------------------------------------------

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

  it("creates a billing event with correct fee split", async () => {
    const mockEvent = {
      id: "evt-1",
      deviceId: "dev-1",
      operatorId: "op-1",
      buyerId: "buy-1",
      model: "qwen3:14b",
      promptTokens: 1000,
      completionTokens: 500,
      totalTokens: 1500,
      priceCents: 10,
      platformFeeCents: 2,
      operatorShareCents: 8,
      paidOut: false,
      payoutTransferId: null,
      createdAt: new Date(),
    };

    vi.mocked(db.marketplaceBillingEvent.create).mockResolvedValue(mockEvent as never);

    const result = await recordBillingEvent({
      deviceId: "dev-1",
      operatorId: "op-1",
      buyerId: "buy-1",
      model: "qwen3:14b",
      promptTokens: 1000,
      completionTokens: 500,
      priceCents: 10,
    });

    expect(result.id).toBe("evt-1");
    expect(db.marketplaceBillingEvent.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        deviceId: "dev-1",
        operatorId: "op-1",
        buyerId: "buy-1",
        model: "qwen3:14b",
        promptTokens: 1000,
        completionTokens: 500,
        totalTokens: 1500,
        priceCents: 10,
        platformFeeCents: 2,
        operatorShareCents: 8,
      }),
    });
  });

  it("computes totalTokens as prompt + completion", async () => {
    vi.mocked(db.marketplaceBillingEvent.create).mockResolvedValue({ id: "evt-2" } as never);

    await recordBillingEvent({
      deviceId: "dev-1",
      operatorId: "op-1",
      buyerId: "buy-1",
      model: "llama3.3:70b",
      promptTokens: 2048,
      completionTokens: 4096,
      priceCents: 50,
    });

    expect(db.marketplaceBillingEvent.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        totalTokens: 6144,
      }),
    });
  });
});

// ---------------------------------------------------------------------------
// getOperatorEarnings
// ---------------------------------------------------------------------------

describe("getOperatorEarnings", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Default: no first booking, 0 lifetime requests
    vi.mocked(db.marketplaceBillingEvent.findFirst).mockResolvedValue(null as never);
    vi.mocked(db.marketplaceBillingEvent.count).mockResolvedValue(0 as never);
  });

  it("aggregates earnings from billing events", async () => {
    const firstBookingDate = new Date("2026-03-01T10:00:00Z");
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([
      { priceCents: 100, operatorShareCents: 80, platformFeeCents: 20 },
      { priceCents: 50, operatorShareCents: 40, platformFeeCents: 10 },
    ] as never);
    vi.mocked(db.marketplaceBillingEvent.findFirst).mockResolvedValue(
      { createdAt: firstBookingDate } as never
    );
    vi.mocked(db.marketplaceBillingEvent.count).mockResolvedValue(5 as never);

    const result = await getOperatorEarnings("op-1", 30);

    expect(result.totalRevenue).toBeCloseTo(1.50);  // (100 + 50) / 100
    expect(result.operatorShare).toBeCloseTo(1.20);  // (80 + 40) / 100
    expect(result.platformFee).toBeCloseTo(0.30);    // (20 + 10) / 100
    expect(result.totalRequests).toBe(2);
    expect(result.periodDays).toBe(30);
    expect(result.dailyAverage).toBeCloseTo(0.04);   // 1.20 / 30
    expect(result.firstBookingAt).toBe(firstBookingDate.toISOString());
    expect(result.lifetimeRequests).toBe(5);
  });

  it("returns zeros and null firstBookingAt when no events exist", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    const result = await getOperatorEarnings("op-1", 30);

    expect(result.totalRevenue).toBe(0);
    expect(result.operatorShare).toBe(0);
    expect(result.platformFee).toBe(0);
    expect(result.totalRequests).toBe(0);
    expect(result.dailyAverage).toBe(0);
    expect(result.firstBookingAt).toBeNull();
    expect(result.lifetimeRequests).toBe(0);
  });

  it("uses correct date filter for period", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    await getOperatorEarnings("op-1", 7);

    expect(db.marketplaceBillingEvent.findMany).toHaveBeenCalledWith({
      where: {
        operatorId: "op-1",
        createdAt: { gte: expect.any(Date) },
      },
      select: {
        priceCents: true,
        operatorShareCents: true,
        platformFeeCents: true,
      },
    });
  });

  it("defaults to 30 days when no period specified", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    const result = await getOperatorEarnings("op-1");

    expect(result.periodDays).toBe(30);
  });
});

// ---------------------------------------------------------------------------
// getBuyerSpending
// ---------------------------------------------------------------------------

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

  it("aggregates buyer spending from billing events", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([
      { priceCents: 100 },
      { priceCents: 50 },
      { priceCents: 25 },
    ] as never);

    const result = await getBuyerSpending("buy-1", 30);

    expect(result.totalSpent).toBeCloseTo(1.75);   // (100 + 50 + 25) / 100
    expect(result.totalRequests).toBe(3);
    expect(result.periodDays).toBe(30);
    expect(result.dailyAverage).toBeCloseTo(1.75 / 30);
  });

  it("returns zeros when no events exist", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    const result = await getBuyerSpending("buy-1");

    expect(result.totalSpent).toBe(0);
    expect(result.totalRequests).toBe(0);
  });

  it("queries with buyer filter and date range", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    await getBuyerSpending("buy-1", 14);

    expect(db.marketplaceBillingEvent.findMany).toHaveBeenCalledWith({
      where: {
        buyerId: "buy-1",
        createdAt: { gte: expect.any(Date) },
      },
      select: {
        priceCents: true,
      },
    });
  });
});

// ---------------------------------------------------------------------------
// getPendingPayout
// ---------------------------------------------------------------------------

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

  it("sums unpaid operator share from billing events", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([
      { id: "evt-1", operatorShareCents: 80 },
      { id: "evt-2", operatorShareCents: 40 },
      { id: "evt-3", operatorShareCents: 160 },
    ] as never);

    const result = await getPendingPayout("op-1");

    expect(result.pendingCents).toBe(280);
    expect(result.eventCount).toBe(3);
    expect(result.eventIds).toEqual(["evt-1", "evt-2", "evt-3"]);
  });

  it("returns zero when no unpaid events", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    const result = await getPendingPayout("op-1");

    expect(result.pendingCents).toBe(0);
    expect(result.eventCount).toBe(0);
    expect(result.eventIds).toEqual([]);
  });

  it("filters by paidOut=false", async () => {
    vi.mocked(db.marketplaceBillingEvent.findMany).mockResolvedValue([]);

    await getPendingPayout("op-1");

    expect(db.marketplaceBillingEvent.findMany).toHaveBeenCalledWith({
      where: {
        operatorId: "op-1",
        paidOut: false,
      },
      select: {
        id: true,
        operatorShareCents: true,
      },
    });
  });
});

// ---------------------------------------------------------------------------
// markEventsPaidOut
// ---------------------------------------------------------------------------

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

  it("updates events with transfer ID and paidOut=true", async () => {
    vi.mocked(db.marketplaceBillingEvent.updateMany).mockResolvedValue({ count: 3 } as never);

    const count = await markEventsPaidOut(
      ["evt-1", "evt-2", "evt-3"],
      "tr_abc123",
    );

    expect(count).toBe(3);
    expect(db.marketplaceBillingEvent.updateMany).toHaveBeenCalledWith({
      where: { id: { in: ["evt-1", "evt-2", "evt-3"] } },
      data: {
        paidOut: true,
        payoutTransferId: "tr_abc123",
      },
    });
  });

  it("returns 0 and skips DB call when no event IDs provided", async () => {
    const count = await markEventsPaidOut([], "tr_abc123");

    expect(count).toBe(0);
    expect(db.marketplaceBillingEvent.updateMany).not.toHaveBeenCalled();
  });
});
