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

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

describe("releaseCommitment", () => {
  it("releases remaining commitment amount back to available budget", async () => {
    const released = { ...baseCommitment, consumedAmount: 5000, status: "RELEASED" as const };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(released);

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

    const value = expectOk(result);
    expect(value.commitment.status).toBe("RELEASED");
    expect(value.commitment.consumedAmount).toBe(baseCommitment.amount);
  });

  it("increases budget availability by the released amount", async () => {
    const released = { ...baseCommitment, consumedAmount: 5000, status: "RELEASED" as const };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(released);

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

    const value = expectOk(result);
    // After release, consumedAmount equals amount, meaning no remaining obligation
    expect(value.commitment.consumedAmount).toBe(value.commitment.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 commitment has already been fully consumed", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(fullyConsumedCommitment); // remaining = 0

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

    expectErr(result, NoCommitmentToReleaseError);
  });

  it("releases a partially consumed commitment (only remaining portion)", async () => {
    const released = {
      ...partiallyConsumedCommitment,
      consumedAmount: partiallyConsumedCommitment.amount,
      status: "RELEASED" as const,
    };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(partiallyConsumedCommitment); // remaining = 3000
    spies.update.mockReturnValueOnce(released);

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

    const value = expectOk(result);
    expect(value.commitment.status).toBe("RELEASED");
    expect(value.commitment.consumedAmount).toBe(partiallyConsumedCommitment.amount);
  });

  it("emits audit event recording the release and acting user", async () => {
    const released = { ...baseCommitment, consumedAmount: 5000, status: "RELEASED" as const };
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCommitment);
    spies.update.mockReturnValueOnce(released);

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

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