import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { DB } from "../generated/kysely-tailordb";
import {
  AccountingPeriodNotFoundError,
  CostElementNotFoundError,
  CostObjectNotFoundError,
} from "../lib/errors.generated";
import {
  activeBudget,
  activeCostElement,
  baseAccountingPeriod,
  baseBudgetLineItem,
  baseCommitment,
  baseCostCenter,
} from "../testing/fixtures";
import { expectErr } from "../testing/commandTestUtils";
import { run } from "./calculateBudgetAvailability";

const baseInput = {
  costElementId: activeCostElement.id,
  costObjectType: "CostCenter",
  costObjectId: baseCostCenter.id,
  accountingPeriodId: baseAccountingPeriod.id,
};

function queueHappyPath(
  spies: ReturnType<typeof createMockDb<DB>>["spies"],
  options?: {
    budgetLineItem?: Record<string, unknown>;
    actualRows?: Record<string, unknown>[];
    commitmentAmount?: number;
  },
) {
  spies.select
    .mockReturnValueOnce(activeCostElement)
    .mockReturnValueOnce(baseCostCenter)
    .mockReturnValueOnce(baseAccountingPeriod)
    .mockReturnValueOnce({ id: activeBudget.id })
    .mockReturnValueOnce(options?.budgetLineItem ?? baseBudgetLineItem)
    .mockReturnValueOnce({ totalCommitment: options?.commitmentAmount ?? 0 })
    .mockReturnValueOnce(options?.actualRows ?? []);
}

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

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

    expectErr(result, CostElementNotFoundError);
  });

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

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

    expectErr(result, CostObjectNotFoundError);
  });

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

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

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns budget availability with all component amounts when budget and line item exist", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies);

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability).not.toBeNull();
      expect(result.value.availability?.plannedAmount).toEqual(baseBudgetLineItem.plannedAmount);
      expect(result.value.availability?.encumbranceAmount).toEqual(0);
      expect(result.value.availability?.availableAmount).toBeDefined();
    }
  });

  it("computes available as budget minus actual plus commitments plus encumbrances", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, {
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 2000 }],
      commitmentAmount: 3000,
    });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.availableAmount).toEqual(5000);
      expect(result.value.availability?.commitmentAmount).toEqual(3000);
    }
  });

  it("returns actual amount aggregated from posted journal entries only", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, {
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 2500 }],
    });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.actualAmount).toEqual(2500);
    }
  });

  it("excludes draft journal entries from actual amount calculation", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, {
      actualRows: [{ costElementId: "other-cost-element", functionalDebitAmount: 2500 }],
    });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.actualAmount).toEqual(0);
    }
  });

  it("returns commitment amount from outstanding purchase order records", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, { commitmentAmount: baseCommitment.amount });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.commitmentAmount).toEqual(baseCommitment.amount);
    }
  });

  it("returns encumbrance amount as zero", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies);

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.encumbranceAmount).toEqual(0);
    }
  });

  it("returns null availability with NO_BUDGET_DEFINED flag when no active budget exists", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability).toBeNull();
      if (result.value.availability === null) {
        expect((result.value as { availability: null; reason: string }).reason).toEqual(
          "NO_BUDGET_DEFINED",
        );
      }
    }
  });

  it("returns null availability with NO_BUDGET_LINE_ITEM flag when line item does not exist for cost element and period", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseCostCenter)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce({ id: activeBudget.id })
      .mockReturnValueOnce(undefined);

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability).toBeNull();
      if (result.value.availability === null) {
        expect((result.value as { availability: null; reason: string }).reason).toEqual(
          "NO_BUDGET_LINE_ITEM",
        );
      }
    }
  });

  it("handles zero budget amount gracefully", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, { budgetLineItem: { ...baseBudgetLineItem, plannedAmount: 0 } });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.plannedAmount).toEqual(0);
      expect(result.value.availability?.availableAmount).toEqual(0);
    }
  });

  it("handles case where actual exceeds budget resulting in negative availability", async () => {
    const { db, spies } = createMockDb<DB>();
    queueHappyPath(spies, {
      actualRows: [{ costElementId: activeCostElement.id, functionalDebitAmount: 12000 }],
      commitmentAmount: 3000,
    });

    const result = await run(db, baseInput);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.availability?.availableAmount).toEqual(-5000);
    }
  });
});
