import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountingPeriodNotFoundError,
  CompanyNotFoundError,
  CostObjectNotFoundError,
  NoBudgetDataError,
} from "../lib/errors.generated";
import {
  activeCostElement,
  baseAccountingPeriod,
  baseBudgetLineItem,
  baseCompany,
  baseCostCenter,
  baseVarianceReport,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./generateVarianceReport";

const baseInput = {
  companyId: baseCompany.id,
  accountingPeriodId: baseAccountingPeriod.id,
  costObjectType: "CostCenter" as const,
  costObjectId: baseCostCenter.id,
};

const revenueCostElement = {
  ...activeCostElement,
  id: "cost-element-revenue",
  category: "revenue",
};

function queueHappyPath(
  spies: ReturnType<typeof createMockDb<Transaction>>["spies"],
  options?: {
    budgetLines?: Record<string, unknown>[];
    actualRows?: Record<string, unknown>[];
    costElements?: Record<string, unknown>[];
    existingDraft?: Record<string, unknown>;
  },
) {
  const budgetLines = options?.budgetLines ?? [baseBudgetLineItem];
  const actualRows = options?.actualRows ?? [];
  const costElements = options?.costElements ?? [activeCostElement];

  spies.select
    .mockReturnValueOnce(baseCompany)
    .mockReturnValueOnce(baseAccountingPeriod)
    .mockReturnValueOnce(baseCostCenter)
    .mockReturnValueOnce(budgetLines)
    .mockReturnValueOnce(actualRows);

  for (const costElement of costElements) {
    spies.select.mockReturnValueOnce(costElement);
  }

  spies.select.mockReturnValueOnce(options?.existingDraft);
}

describe("generateVarianceReport", () => {
  it("returns error when company does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { ...baseInput, companyId: "missing-company" }, commandCtx);

    expectErr(result, CompanyNotFoundError);
  });

  it("returns error when company is not ACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...baseCompany, status: "INACTIVE" });

    const result = await run(db, baseInput, commandCtx);

    expectErr(result, CompanyNotFoundError);
  });

  it("returns error when accounting period does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(db, baseInput, commandCtx);

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns error when cost object does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);

    const result = await run(db, baseInput, commandCtx);

    expectErr(result, CostObjectNotFoundError);
  });

  it("returns error when no budget line items exist for the cost object and period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce([]);

    const result = await run(db, baseInput, commandCtx);

    expectErr(result, NoBudgetDataError);
  });

  it("generates a variance report in DRAFT status for a cost center", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 10000,
      totalActual: 0,
      totalVariance: -10000,
      status: "DRAFT",
    };

    queueHappyPath(spies);
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    const value = expectOk(result);
    expect(value.varianceReport).toEqual(createdReport);
    expect(value.varianceReport.status).toBe("DRAFT");
  });

  it("calculates variance as actual minus planned per cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 5000,
      totalActual: 3000,
      totalVariance: -2000,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      budgetLines: [{ ...baseBudgetLineItem, plannedAmount: 5000 }],
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 3000 }],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    const value = expectOk(result);
    expect(value.varianceReport.totalVariance).toBe(-2000);
  });

  it("handles division by zero when planned amount is zero", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 0,
      totalActual: 1200,
      totalVariance: 1200,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      budgetLines: [{ ...baseBudgetLineItem, plannedAmount: 0 }],
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 1200 }],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
  });

  it("sets variance direction to FAVORABLE when actual equals planned", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 1000,
      totalActual: 1000,
      totalVariance: 0,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      budgetLines: [{ ...baseBudgetLineItem, plannedAmount: 1000 }],
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 1000 }],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
    expect(spies.values).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ varianceDirection: "FAVORABLE" }),
    );
  });

  it("categorizes cost/expense variance as FAVORABLE when actual is less than planned", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 10000,
      totalActual: 2000,
      totalVariance: -8000,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 2000 }],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
    expect(spies.values).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ varianceDirection: "FAVORABLE" }),
    );
  });

  it("categorizes revenue variance as FAVORABLE when actual is greater than planned", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 10000,
      totalActual: 12000,
      totalVariance: 2000,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      budgetLines: [
        { ...baseBudgetLineItem, costElementId: revenueCostElement.id, plannedAmount: 10000 },
      ],
      actualRows: [{ costElementId: revenueCostElement.id, functionalCreditAmount: 12000 }],
      costElements: [revenueCostElement],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
    expect(spies.values).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ varianceDirection: "FAVORABLE" }),
    );
  });

  it("creates a line for actual postings without a matching budget line item", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unbudgetedCostElement = { ...activeCostElement, id: "cost-element-unbudgeted" };
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 10000,
      totalActual: 14000,
      totalVariance: 4000,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      actualRows: [
        { costElementId: activeCostElement.id, functionalDebitAmount: 10000 },
        { costElementId: unbudgetedCostElement.id, functionalDebitAmount: 4000 },
      ],
      costElements: [activeCostElement, unbudgetedCostElement],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({}).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
    expect(spies.values).toHaveBeenNthCalledWith(
      3,
      expect.objectContaining({
        costElementId: unbudgetedCostElement.id,
        plannedAmount: 0,
        actualAmount: 4000,
      }),
    );
  });

  it("computes summary totals from the sum of detail lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const costElement2 = { ...activeCostElement, id: "ce-2" };
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 8000,
      totalActual: 6000,
      totalVariance: -2000,
      status: "DRAFT",
    };

    queueHappyPath(spies, {
      budgetLines: [
        {
          ...baseBudgetLineItem,
          id: "bli-1",
          costElementId: activeCostElement.id,
          plannedAmount: 5000,
        },
        { ...baseBudgetLineItem, id: "bli-2", costElementId: costElement2.id, plannedAmount: 3000 },
      ],
      actualRows: [
        { costElementId: activeCostElement.id, functionalDebitAmount: 4000 },
        { costElementId: costElement2.id, functionalDebitAmount: 2000 },
      ],
      costElements: [activeCostElement, costElement2],
    });
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({}).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    const value = expectOk(result);
    expect(value.varianceReport.totalPlanned).toBe(8000);
    expect(value.varianceReport.totalActual).toBe(6000);
    expect(value.varianceReport.totalVariance).toBe(-2000);
  });

  it("replaces existing DRAFT report when regenerating for same cost object and period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const existingDraft = { ...baseVarianceReport, id: "old-draft", status: "DRAFT" };
    const createdReport = {
      ...baseVarianceReport,
      id: "new-report",
      totalPlanned: 10000,
      totalActual: 0,
      totalVariance: -10000,
      status: "DRAFT",
    };

    queueHappyPath(spies, { existingDraft });
    spies.delete.mockReturnValueOnce(undefined).mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    const value = expectOk(result);
    expect(value.varianceReport).toEqual(createdReport);
    expect(spies.delete).toHaveBeenCalledTimes(2);
  });

  it("does not overwrite a FINAL report when regenerating", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      id: "new-draft",
      totalPlanned: 10000,
      totalActual: 0,
      totalVariance: -10000,
      status: "DRAFT",
    };

    queueHappyPath(spies);
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    const value = expectOk(result);
    expect(value.varianceReport.status).toBe("DRAFT");
    expect(spies.delete).not.toHaveBeenCalled();
  });

  it("emits audit event recording the report generation and acting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdReport = {
      ...baseVarianceReport,
      totalPlanned: 10000,
      totalActual: 0,
      totalVariance: -10000,
      status: "DRAFT",
    };

    queueHappyPath(spies);
    spies.insert.mockReturnValueOnce(createdReport).mockReturnValueOnce({});

    const result = await run(db, baseInput, commandCtx);

    expectOk(result);
    expect(spies.insert).toHaveBeenCalled();
  });
});
