import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  BudgetNotFoundError,
  InvalidStateError,
  InvalidCostElementError,
  AccountingPeriodNotFoundError,
  InvalidPlannedAmountError,
  DuplicateLineItemError,
} from "../lib/errors.generated";
import {
  baseBudget,
  submittedBudget,
  activeCostElement,
  secondaryCostElement,
  inactiveCostElement,
  baseAccountingPeriod,
  baseBudgetLineItem,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./createBudgetLineItem";

const validInput = {
  budgetId: baseBudget.id,
  costElementId: activeCostElement.id,
  accountingPeriodId: baseAccountingPeriod.id,
  plannedAmount: 10000,
};

describe("createBudgetLineItem", () => {
  it("adds a line item to a DRAFT budget with valid cost element, period, and amount", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseBudget)
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined); // no duplicate
    spies.insert.mockReturnValueOnce(baseBudgetLineItem);

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

    const value = expectOk(result);
    expect(value.budgetLineItem).toEqual(baseBudgetLineItem);
  });

  it("derives GL account automatically from the primary cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseBudget)
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(baseBudgetLineItem);

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

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

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

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

    expectErr(result, BudgetNotFoundError);
  });

  it("returns error when budget is not in DRAFT status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(submittedBudget);

    const result = await run(db, { ...validInput, budgetId: submittedBudget.id }, commandCtx);

    expectErr(result, InvalidStateError);
  });

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

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

    expectErr(result, InvalidCostElementError);
  });

  it("returns error when cost element is not PRIMARY type", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseBudget).mockReturnValueOnce(secondaryCostElement);

    const result = await run(
      db,
      { ...validInput, costElementId: secondaryCostElement.id },
      commandCtx,
    );

    expectErr(result, InvalidCostElementError);
  });

  it("returns error when cost element is not in ACTIVE status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseBudget).mockReturnValueOnce(inactiveCostElement);

    const result = await run(
      db,
      { ...validInput, costElementId: inactiveCostElement.id },
      commandCtx,
    );

    expectErr(result, InvalidCostElementError);
  });

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

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

    expectErr(result, AccountingPeriodNotFoundError);
  });

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

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

    expectErr(result, InvalidPlannedAmountError);
  });

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

    const result = await run(db, { ...validInput, plannedAmount: -100 }, commandCtx);

    expectErr(result, InvalidPlannedAmountError);
  });

  it("returns error when duplicate line item exists (same cost element and period)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseBudget)
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(baseBudgetLineItem); // duplicate found

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

    expectErr(result, DuplicateLineItemError);
  });

  it("emits audit event recording the line item creation and acting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseBudget)
      .mockReturnValueOnce(activeCostElement)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(baseBudgetLineItem);

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

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