import { describe, it, expect, beforeEach } from "vitest";
import { prisma, resetDb, seedTestOrganization, DEFAULT_ORG_ID } from "./helpers.js";
import { getOrgUsage, getProjectUsage } from "../../src/org/usage.js";

/**
 * ADR-0026: org/project usage rolls up from the UsageFact ledger, not from the
 * AgentRun scalar cache. These tests pin the fact-based aggregation contract:
 *   - a run with a cost-bearing fact → runsWithCost, costUsd summed
 *   - a run whose facts all have null costUsd → runsWithoutCost (≠ zero)
 *   - non-token meter (quantity/unit) is stored but does not corrupt token sums
 *   - multiple facts per run (main loop + external capability) are summed together
 *   - the AgentRun rollup cache is NOT consulted by getOrgUsage
 */
describe("usage rollup from UsageFact (ADR-0026)", () => {
  beforeEach(async () => {
    await resetDb();
    await seedTestOrganization();
  });

  async function seedRun(opts: {
    readonly projectId: string;
    readonly projectName: string;
    readonly folderId?: string;
    readonly facts: ReadonlyArray<{
      readonly kind?: string;
      readonly provider?: string;
      readonly model?: string;
      readonly inputTokens?: number;
      readonly outputTokens?: number;
      readonly costUsd?: number | null;
      readonly costSource?: string;
      readonly capabilityId?: string;
      readonly quantity?: number | null;
      readonly unit?: string | null;
    }>;
  }): Promise<void> {
    const project = await prisma.project.create({
      data: {
        id: opts.projectId,
        organizationId: DEFAULT_ORG_ID,
        name: opts.projectName,
        workspaceDir: `/tmp/test-${opts.projectId}`,
        ...(opts.folderId !== undefined ? { folderId: opts.folderId } : {}),
      },
    });
    const startedAt = new Date("2026-07-18T10:00:00Z");
    const finishedAt = new Date("2026-07-18T10:05:00Z");
    const run = await prisma.agentRun.create({
      data: {
        projectId: project.id,
        entrypoint: "FEISHU",
        provider: "openrouter",
        model: "mock-model",
        metadata: {},
        status: "COMPLETED",
        prompt: "test",
        startedAt,
        finishedAt,
        // Deliberately set the rollup cache to values that differ from the
        // facts, to prove getOrgUsage reads facts, not the cache.
        inputTokens: 999,
        outputTokens: 999,
        costUsd: 999,
        costSource: "provider_reported",
      },
    });
    for (const [i, f] of opts.facts.entries()) {
      await prisma.usageFact.create({
        data: {
          runId: run.id,
          occurredAt: new Date(startedAt.getTime() + i * 1000),
          kind: f.kind ?? "model_completion",
          provider: f.provider ?? "openrouter",
          model: f.model !== undefined ? f.model : "mock-model",
          inputTokens: f.inputTokens ?? null,
          outputTokens: f.outputTokens ?? null,
          costUsd: f.costUsd ?? null,
          costSource: f.costSource ?? (f.costUsd !== undefined && f.costUsd !== null ? "provider_reported" : "unknown"),
          capabilityId: f.capabilityId ?? null,
          quantity: f.quantity ?? null,
          unit: f.unit ?? null,
          metadata: {},
        },
      });
    }
  }

  it("sums cost and tokens from facts, ignoring the AgentRun rollup cache", async () => {
    await seedRun({
      projectId: "proj-a",
      projectName: "A",
      facts: [{ inputTokens: 10, outputTokens: 5, costUsd: 0.0023 }],
    });
    await seedRun({
      projectId: "proj-b",
      projectName: "B",
      facts: [{ inputTokens: 20, outputTokens: 10, costUsd: 0.01 }],
    });

    const report = await getOrgUsage(prisma, { organizationId: DEFAULT_ORG_ID });

    expect(report.totals.runCount).toBe(2);
    expect(report.totals.runsWithCost).toBe(2);
    expect(report.totals.runsWithoutCost).toBe(0);
    expect(report.totals.inputTokens).toBe(30);
    expect(report.totals.outputTokens).toBe(15);
    expect(report.totals.costUsd).toBeCloseTo(0.0123, 8);
  });

  it("treats a run with only null-cost facts as runsWithoutCost, never zero", async () => {
    await seedRun({
      projectId: "proj-unknown",
      projectName: "Unknown",
      facts: [{ inputTokens: 7, outputTokens: 3, costUsd: null, costSource: "unknown" }],
    });
    await seedRun({
      projectId: "proj-known",
      projectName: "Known",
      facts: [{ inputTokens: 4, outputTokens: 2, costUsd: 0.05 }],
    });

    const report = await getOrgUsage(prisma, { organizationId: DEFAULT_ORG_ID });

    expect(report.totals.runCount).toBe(2);
    expect(report.totals.runsWithCost).toBe(1);
    expect(report.totals.runsWithoutCost).toBe(1);
    // The unknown-cost run still contributes its tokens.
    expect(report.totals.inputTokens).toBe(11);
    expect(report.totals.outputTokens).toBe(5);
    // And its cost is NOT counted as zero — only the known cost is summed.
    expect(report.totals.costUsd).toBeCloseTo(0.05, 8);
  });

  it("sums multiple facts per run (main loop + external capability)", async () => {
    await seedRun({
      projectId: "proj-multi",
      projectName: "Multi",
      facts: [
        { kind: "model_completion", inputTokens: 100, outputTokens: 50, costUsd: 0.02 },
        {
          kind: "external_capability",
          provider: "mineru",
          model: null,
          inputTokens: null,
          outputTokens: null,
          costUsd: 0.015,
          costSource: "provider_reported",
          capabilityId: "pdf_to_md_bundle",
          quantity: 12,
          unit: "pages",
        },
      ],
    });

    const report = await getOrgUsage(prisma, { organizationId: DEFAULT_ORG_ID });
    expect(report.totals.runCount).toBe(1);
    expect(report.totals.runsWithCost).toBe(1);
    expect(report.totals.inputTokens).toBe(100);
    expect(report.totals.outputTokens).toBe(50);
    expect(report.totals.costUsd).toBeCloseTo(0.035, 8);
    // ADR-0026 breakdown: model tokens vs external non-token meter must not collapse.
    expect(report.breakdown).toHaveLength(2);
    const modelBucket = report.breakdown.find((b) => b.kind === "model_completion");
    const capBucket = report.breakdown.find((b) => b.kind === "external_capability");
    expect(modelBucket).toMatchObject({
      provider: "openrouter",
      model: "mock-model",
      capabilityId: null,
      inputTokens: 100,
      outputTokens: 50,
      quantity: null,
      costUsd: 0.02,
      factCount: 1,
    });
    expect(capBucket).toMatchObject({
      provider: "mineru",
      model: null,
      capabilityId: "pdf_to_md_bundle",
      unit: "pages",
      quantity: 12,
      inputTokens: 0,
      outputTokens: 0,
      costUsd: 0.015,
      factCount: 1,
    });

  });

  it("a run with no facts at all is runsWithoutCost and contributes nothing", async () => {
    await seedRun({ projectId: "proj-empty", projectName: "Empty", facts: [] });

    const report = await getOrgUsage(prisma, { organizationId: DEFAULT_ORG_ID });
    expect(report.totals.runCount).toBe(1);
    expect(report.totals.runsWithCost).toBe(0);
    expect(report.totals.runsWithoutCost).toBe(1);
    expect(report.totals.inputTokens).toBe(0);
    expect(report.totals.outputTokens).toBe(0);
    expect(report.totals.costUsd).toBeNull();
  });

  it("getProjectUsage returns just that project's row", async () => {
    await seedRun({ projectId: "proj-x", projectName: "X", facts: [{ costUsd: 0.1, inputTokens: 1, outputTokens: 1 }] });
    await seedRun({ projectId: "proj-y", projectName: "Y", facts: [{ costUsd: 0.2, inputTokens: 2, outputTokens: 2 }] });

    const row = await getProjectUsage(prisma, { organizationId: DEFAULT_ORG_ID, projectId: "proj-x" });
    expect(row.projectId).toBe("proj-x");
    expect(row.runCount).toBe(1);
    expect(row.costUsd).toBeCloseTo(0.1, 8);
    expect(row.breakdown).toHaveLength(1);
    expect(row.breakdown[0]).toMatchObject({ kind: "model_completion", costUsd: 0.1, inputTokens: 1 });
  });

  it("returns an empty report for an org with no projects", async () => {
    const report = await getOrgUsage(prisma, { organizationId: DEFAULT_ORG_ID });
    expect(report.projects).toHaveLength(0);
    expect(report.totals.runCount).toBe(0);
    expect(report.totals.costUsd).toBeNull();
    expect(report.breakdown).toEqual([]);

  });
});
