import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CostElementNotFoundError,
  CostObjectNotFoundError,
  AccountingPeriodNotFoundError,
  InvalidCommitmentAmountError,
  NoActiveBudgetError,
} from "../lib/errors.generated";
import {
  baseCompany,
  activeCostElement,
  baseCostCenter,
  baseAccountingPeriod,
  activeBudget,
  baseCommitment,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./recordCommitment";

const validInput = {
  costElementId: activeCostElement.id,
  costObjectType: "CostCenter",
  costObjectId: baseCostCenter.id,
  accountingPeriodId: baseAccountingPeriod.id,
  amount: 5000,
  purchaseOrderId: "purchase-order-1",
  companyId: baseCompany.id,
};

describe("recordCommitment", () => {
  it("records a commitment against a cost element, cost object, and period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement) // cost element
      .mockReturnValueOnce(baseCostCenter) // cost object
      .mockReturnValueOnce(baseAccountingPeriod) // accounting period
      .mockReturnValueOnce(activeBudget); // active budget
    spies.insert.mockReturnValueOnce(baseCommitment);

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

    const value = expectOk(result);
    expect(value.commitment).toEqual(baseCommitment);
  });

  it("reduces budget availability by the commitment amount", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(activeBudget);
    spies.insert.mockReturnValueOnce(baseCommitment);

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

    const value = expectOk(result);
    expect(value.commitment.status).toBe("OPEN");
    expect(value.commitment.consumedAmount).toBe(0);
    expect(value.commitment.amount).toBe(5000);
  });

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

    const result = await run(db, { ...validInput, costElementId: "missing-element" }, commandCtx);

    expectErr(result, CostElementNotFoundError);
  });

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

    const result = await run(
      db,
      { ...validInput, costObjectId: "missing-cost-object" },
      commandCtx,
    );

    expectErr(result, CostObjectNotFoundError);
  });

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

    const result = await run(
      db,
      { ...validInput, accountingPeriodId: "missing-period" },
      commandCtx,
    );

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns error when commitment amount is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod);

    const result = await run(db, { ...validInput, amount: 0 }, commandCtx);

    expectErr(result, InvalidCommitmentAmountError);
  });

  it("returns error when no active budget exists for the cost object", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined); // no active budget

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

    expectErr(result, NoActiveBudgetError);
  });

  it("associates commitment with purchase order reference", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(activeBudget);
    spies.insert.mockReturnValueOnce(baseCommitment);

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

    const value = expectOk(result);
    expect(value.commitment.purchaseOrderId).toBe("purchase-order-1");
  });

  it("emits audit event recording the commitment and acting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(activeBudget);
    spies.insert.mockReturnValueOnce(baseCommitment);

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

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