import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  DuplicateCostElementCodeError,
  InvalidCostElementTypeError,
  InvalidCategoryForTypeError,
  GlAccountRequiredError,
  GlAccountNotFoundError,
  GlAccountNotAllowedError,
  DuplicateGlAccountReferenceError,
  InvalidValidityPeriodError,
} from "../lib/errors.generated";
import {
  baseCompany,
  baseAccount,
  inactiveAccount,
  baseChartOfAccounts,
  inactiveChartOfAccounts,
  baseCostElement,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./createCostElement";

describe("createCostElement", () => {
  const validPrimaryInput = {
    companyId: baseCompany.id,
    code: "CE-NEW",
    name: "New Cost Element",
    type: "PRIMARY",
    category: "overhead",
    glAccountId: baseAccount.id,
  };

  it("creates cost element in DRAFT status with required fields", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // company lookup, code uniqueness check, account lookup, CoA lookup, duplicate GL check
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce({
      ...baseCostElement,
      code: "CE-NEW",
      name: "New Cost Element",
    });

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

    const value = expectOk(result);
    expect(value.costElement).toBeDefined();
    expect(value.costElement.code).toBe("CE-NEW");
  });

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

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

    expectErr(result, CompanyNotFoundError);
  });

  it("returns error when company is not ACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...baseCompany, status: "INACTIVE" });

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

    expectErr(result, CompanyNotFoundError);
  });

  it("returns error when code already exists within the company", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(baseCostElement);

    const result = await run(db, { ...validPrimaryInput, code: baseCostElement.code }, commandCtx);

    expectErr(result, DuplicateCostElementCodeError);
  });

  it("returns error when type is not PRIMARY or SECONDARY", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(db, { ...validPrimaryInput, type: "TERTIARY" }, commandCtx);

    expectErr(result, InvalidCostElementTypeError);
  });

  it("returns error when category does not match the type (secondary category on primary element)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(db, { ...validPrimaryInput, category: "assessment" }, commandCtx);

    expectErr(result, InvalidCategoryForTypeError);
  });

  it("returns error when category does not match the type (primary category on secondary element)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(
      db,
      { ...validPrimaryInput, type: "SECONDARY", category: "overhead", glAccountId: undefined },
      commandCtx,
    );

    expectErr(result, InvalidCategoryForTypeError);
  });

  it("returns error when PRIMARY element is missing glAccountId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(db, { ...validPrimaryInput, glAccountId: undefined }, commandCtx);

    expectErr(result, GlAccountRequiredError);
  });

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

    const result = await run(
      db,
      { ...validPrimaryInput, glAccountId: "missing-account" },
      commandCtx,
    );

    expectErr(result, GlAccountNotFoundError);
  });

  it("returns error when GL account is not active", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(inactiveAccount);

    const result = await run(
      db,
      { ...validPrimaryInput, glAccountId: inactiveAccount.id },
      commandCtx,
    );

    expectErr(result, GlAccountNotFoundError);
  });

  it("returns error when GL account is not in an active Chart of Accounts", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce({ ...baseAccount, chartOfAccountsId: inactiveChartOfAccounts.id })
      .mockReturnValueOnce(inactiveChartOfAccounts);

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

    expectErr(result, GlAccountNotFoundError);
  });

  it("returns error when SECONDARY element provides a glAccountId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        ...validPrimaryInput,
        type: "SECONDARY",
        category: "assessment",
        glAccountId: baseAccount.id,
      },
      commandCtx,
    );

    expectErr(result, GlAccountNotAllowedError);
  });

  it("returns error when another PRIMARY element in the same company references the same GL account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(baseCostElement);

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

    expectErr(result, DuplicateGlAccountReferenceError);
  });

  it("returns error when validFrom is after validTo", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);

    const result = await run(
      db,
      { ...validPrimaryInput, validFrom: new Date("2025-12-31"), validTo: new Date("2025-01-01") },
      commandCtx,
    );

    expectErr(result, InvalidValidityPeriodError);
  });

  it("creates cost element with optional description", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const withDesc = { ...baseCostElement, description: "A description" };
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(withDesc);

    const result = await run(
      db,
      { ...validPrimaryInput, description: "A description" },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.description).toBe("A description");
  });

  it("creates cost element with validFrom and validTo dates", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const withDates = {
      ...baseCostElement,
      validFrom: new Date("2025-01-01"),
      validTo: new Date("2025-12-31"),
    };
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(withDates);

    const result = await run(
      db,
      { ...validPrimaryInput, validFrom: new Date("2025-01-01"), validTo: new Date("2025-12-31") },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.validFrom).toEqual(new Date("2025-01-01"));
    expect(value.costElement.validTo).toEqual(new Date("2025-12-31"));
  });

  it("creates cost element without validity dates (indefinitely valid)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(baseCostElement);

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

    const value = expectOk(result);
    expect(value.costElement.validFrom).toBeNull();
    expect(value.costElement.validTo).toBeNull();
  });

  it("cost elements from different companies are isolated", async () => {
    const companyB = { id: "company-2", status: "ACTIVE" };
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(companyB)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    const costElementB = { ...baseCostElement, companyId: companyB.id };
    spies.insert.mockReturnValueOnce(costElementB);

    const result = await run(db, { ...validPrimaryInput, companyId: companyB.id }, commandCtx);

    const value = expectOk(result);
    expect(value.costElement.companyId).toBe(companyB.id);
  });

  it("emits audit event recording acting user, timestamp, and initial field values", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(baseAccount)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(baseCostElement);

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

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