import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CostElementNotFoundError,
  InvalidStateError,
  TypeChangeNotAllowedError,
  CategoryChangeNotAllowedError,
  GlAccountChangeNotAllowedError,
  InvalidCategoryForTypeError,
  GlAccountNotFoundError,
  DuplicateGlAccountReferenceError,
  InvalidValidityPeriodError,
} from "../lib/errors.generated";
import {
  baseCostElement,
  activeCostElement,
  inactiveCostElement,
  baseAccount2,
  baseChartOfAccounts,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./updateCostElement";

describe("updateCostElement", () => {
  it("updates description on a DRAFT cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostElement);
    const updated = { ...baseCostElement, description: "Updated desc" };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: baseCostElement.id, description: "Updated desc" },
      commandCtx,
    );

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

  it("updates description on an ACTIVE cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activeCostElement);
    const updated = { ...activeCostElement, description: "Updated desc" };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: activeCostElement.id, description: "Updated desc" },
      commandCtx,
    );

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

  it("updates name on a DRAFT cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostElement);
    const updated = { ...baseCostElement, name: "New Name" };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: baseCostElement.id, name: "New Name" },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.name).toBe("New Name");
  });

  it("updates name on an ACTIVE cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activeCostElement);
    const updated = { ...activeCostElement, name: "New Name" };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: activeCostElement.id, name: "New Name" },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.name).toBe("New Name");
  });

  it("updates category on a DRAFT cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostElement);
    const updated = { ...baseCostElement, category: "material" };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: baseCostElement.id, category: "material" },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.category).toBe("material");
  });

  it("returns error when changing category on an ACTIVE cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activeCostElement);

    const result = await run(
      db,
      { costElementId: activeCostElement.id, category: "material" },
      commandCtx,
    );

    expectErr(result, CategoryChangeNotAllowedError);
  });

  it("returns error when changing type on any cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostElement);

    const result = await run(
      db,
      { costElementId: baseCostElement.id, type: "SECONDARY" },
      commandCtx,
    );

    expectErr(result, TypeChangeNotAllowedError);
  });

  it("returns error when changing GL account on an ACTIVE cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activeCostElement);

    const result = await run(
      db,
      { costElementId: activeCostElement.id, glAccountId: baseAccount2.id },
      commandCtx,
    );

    expectErr(result, GlAccountChangeNotAllowedError);
  });

  it("updates GL account on a DRAFT PRIMARY cost element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostElement)
      .mockReturnValueOnce(baseAccount2)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce(undefined);
    const updated = { ...baseCostElement, glAccountId: baseAccount2.id };
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { costElementId: baseCostElement.id, glAccountId: baseAccount2.id },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costElement.glAccountId).toBe(baseAccount2.id);
  });

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

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

    expectErr(result, GlAccountNotFoundError);
  });

  it("returns error when updated GL account is already referenced by another PRIMARY element", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostElement)
      .mockReturnValueOnce(baseAccount2)
      .mockReturnValueOnce(baseChartOfAccounts)
      .mockReturnValueOnce({
        ...baseCostElement,
        id: "other-element",
        glAccountId: baseAccount2.id,
      });

    const result = await run(
      db,
      { costElementId: baseCostElement.id, glAccountId: baseAccount2.id },
      commandCtx,
    );

    expectErr(result, DuplicateGlAccountReferenceError);
  });

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

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

    expectErr(result, InvalidCategoryForTypeError);
  });

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

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

    expectErr(result, CostElementNotFoundError);
  });

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

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

    expectErr(result, InvalidStateError);
  });

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

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

    expectErr(result, InvalidValidityPeriodError);
  });

  it("emits audit event recording previous and new field values", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostElement);
    spies.update.mockReturnValueOnce({ ...baseCostElement, name: "Updated" });

    const result = await run(
      db,
      { costElementId: baseCostElement.id, name: "Updated" },
      commandCtx,
    );

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