import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CommitmentNotFoundError,
  InvalidConsumptionAmountError,
  ConsumptionExceedsCommitmentError,
} from "../lib/errors.generated";
import { baseCommitment, partiallyConsumedCommitment } from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./consumeCommitment";

const validInput = {
  costElementId: baseCommitment.costElementId,
  costObjectType: baseCommitment.costObjectType,
  costObjectId: baseCommitment.costObjectId,
  accountingPeriodId: baseCommitment.accountingPeriodId,
  consumptionAmount: 2000,
};

describe("consumeCommitment", () => {
  it("consumes a portion of an existing commitment", async () => {
    const updatedCommitment = { ...baseCommitment, consumedAmount: 2000 };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(updatedCommitment);

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

    const value = expectOk(result);
    expect(value.commitment.consumedAmount).toBe(2000);
  });

  it("reduces commitment amount by the consumption amount", async () => {
    const updatedCommitment = { ...baseCommitment, consumedAmount: 2000 };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(updatedCommitment);

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

    const value = expectOk(result);
    // remaining = amount - consumedAmount = 5000 - 2000 = 3000
    expect(value.commitment.amount - value.commitment.consumedAmount).toBe(3000);
  });

  it("increases actual amount by the consumption amount", async () => {
    const updatedCommitment = { ...baseCommitment, consumedAmount: 2000 };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(updatedCommitment);

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

    const value = expectOk(result);
    expect(value.commitment.consumedAmount).toBe(2000);
  });

  it("budget availability remains unchanged for the consumed portion", async () => {
    const updatedCommitment = { ...baseCommitment, consumedAmount: 2000 };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(updatedCommitment);

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

    const value = expectOk(result);
    // The total obligation (consumedAmount) stays at same level of impact on budget
    expect(value.commitment.amount).toBe(baseCommitment.amount);
  });

  it("returns error when commitment 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, CommitmentNotFoundError);
  });

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

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

    expectErr(result, InvalidConsumptionAmountError);
  });

  it("returns error when consumption amount exceeds remaining commitment", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(partiallyConsumedCommitment); // remaining = 5000 - 2000 = 3000

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

    expectErr(result, ConsumptionExceedsCommitmentError);
  });

  it("fully consumes a commitment when consumption equals remaining amount", async () => {
    const consumed = { ...baseCommitment, consumedAmount: 5000, status: "CONSUMED" as const };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment); // remaining = 5000
    spies.update.mockReturnValueOnce(consumed);

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

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

  it("emits audit event recording the consumption and acting user", async () => {
    const updatedCommitment = { ...baseCommitment, consumedAmount: 2000 };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(updatedCommitment);

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

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