import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  HierarchyNotFoundError,
  ParentNodeNotFoundError,
  DuplicateRootNodeError,
  InvalidNameError,
  SelfReferenceError,
  CircularReferenceError,
} from "../lib/errors.generated";
import {
  baseCostCenterHierarchy,
  rootCostCenterNode,
  childCostCenterNode,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./createCostCenterHierarchyNode";

describe("createCostCenterHierarchyNode", () => {
  it("returns error when name is empty", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, { hierarchyId: baseCostCenterHierarchy.id, name: "" }, commandCtx);

    expectErr(result, InvalidNameError);
  });

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

    const result = await run(db, { hierarchyId: "missing-hierarchy", name: "Root" }, commandCtx);

    expectErr(result, HierarchyNotFoundError);
  });

  it("returns error when parent node does not exist in hierarchy", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(undefined); // parent node not found

    const result = await run(
      db,
      {
        hierarchyId: baseCostCenterHierarchy.id,
        name: "Child",
        parentNodeId: "missing-parent",
      },
      commandCtx,
    );

    expectErr(result, ParentNodeNotFoundError);
  });

  it("returns error when creating duplicate root node (no parentNodeId)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(rootCostCenterNode); // existing root found

    const result = await run(
      db,
      { hierarchyId: baseCostCenterHierarchy.id, name: "Another Root" },
      commandCtx,
    );

    expectErr(result, DuplicateRootNodeError);
  });

  it("creates root node when no root exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(undefined); // no existing root
    spies.insert.mockReturnValueOnce(rootCostCenterNode);

    const result = await run(
      db,
      { hierarchyId: baseCostCenterHierarchy.id, name: "Root" },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costCenterHierarchyNode).toEqual(rootCostCenterNode);
  });

  it("creates child node with correct computed level", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(rootCostCenterNode) // parent node found (level 0)
      .mockReturnValueOnce(rootCostCenterNode); // ancestor walk: root has no parent, loop ends
    spies.insert.mockReturnValueOnce(childCostCenterNode);

    const result = await run(
      db,
      {
        hierarchyId: baseCostCenterHierarchy.id,
        name: "Administration",
        parentNodeId: rootCostCenterNode.id,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.costCenterHierarchyNode).toEqual(childCostCenterNode);
    expect(value.costCenterHierarchyNode.level).toBe(1);
  });

  it("returns error when node references itself as parent", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(rootCostCenterNode) // parent node found
      .mockReturnValueOnce({ ...rootCostCenterNode, parentNodeId: rootCostCenterNode.id }); // ancestor walk: self-loop detected

    const result = await run(
      db,
      {
        hierarchyId: baseCostCenterHierarchy.id,
        name: "Child",
        parentNodeId: rootCostCenterNode.id,
      },
      commandCtx,
    );

    expectErr(result, SelfReferenceError);
  });

  it("returns error when node would create a circular reference", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // Setup: node-a -> node-b -> node-a (circular chain)
    const nodeA = { ...rootCostCenterNode, id: "node-a", parentNodeId: "node-b" };
    const nodeB = { ...childCostCenterNode, id: "node-b", parentNodeId: "node-a" };

    spies.select
      .mockReturnValueOnce(baseCostCenterHierarchy) // hierarchy exists
      .mockReturnValueOnce(nodeA) // parent node (A) found
      .mockReturnValueOnce(nodeA) // ancestor walk iteration 1: fetch A, parentNodeId = "node-b"
      .mockReturnValueOnce(nodeB); // ancestor walk iteration 2: fetch B, parentNodeId = "node-a" (already visited) → circular

    const result = await run(
      db,
      {
        hierarchyId: baseCostCenterHierarchy.id,
        name: "New Child",
        parentNodeId: "node-a",
      },
      commandCtx,
    );

    expectErr(result, CircularReferenceError);
  });

  it("emits audit event by calling insert", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCostCenterHierarchy).mockReturnValueOnce(undefined); // no root
    spies.insert.mockReturnValueOnce(rootCostCenterNode);

    const result = await run(
      db,
      { hierarchyId: baseCostCenterHierarchy.id, name: "Root" },
      commandCtx,
    );

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