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

describe("moveCostCenterHierarchyNode", () => {
  it("returns error for self-reference", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(
      db,
      { nodeId: childCostCenterNode.id, newParentNodeId: childCostCenterNode.id },
      commandCtx,
    );

    expectErr(result, SelfReferenceError);
  });

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

    const result = await run(
      db,
      { nodeId: "missing-node", newParentNodeId: rootCostCenterNode.id },
      commandCtx,
    );

    expectErr(result, NodeNotFoundError);
  });

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

    const result = await run(
      db,
      { nodeId: childCostCenterNode.id, newParentNodeId: "other-hierarchy-node" },
      commandCtx,
    );

    expectErr(result, ParentNodeNotFoundError);
  });

  it("returns error for circular reference (moving parent under its child)", async () => {
    // Scenario: rootCostCenterNode -> childCostCenterNode -> grandchild
    // Moving rootCostCenterNode under childCostCenterNode would be circular
    const grandchild = {
      ...childCostCenterNode2,
      id: "cc-node-grandchild",
      parentNodeId: childCostCenterNode.id,
      level: 2,
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(rootCostCenterNode) // node to move exists
      .mockReturnValueOnce(grandchild) // new parent found in same hierarchy
      .mockReturnValueOnce(childCostCenterNode); // walking up: grandchild's parent is childCostCenterNode, whose parentNodeId is rootCostCenterNode.id

    // childCostCenterNode.parentNodeId === rootCostCenterNode.id === input.nodeId => circular!
    const result = await run(
      db,
      { nodeId: rootCostCenterNode.id, newParentNodeId: grandchild.id },
      commandCtx,
    );

    expectErr(result, CircularReferenceError);
  });

  it("moves node to new parent successfully", async () => {
    const movedNode = {
      ...childCostCenterNode,
      parentNodeId: childCostCenterNode2.id,
      level: 2,
      updatedAt: new Date(),
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(childCostCenterNode) // node to move exists
      .mockReturnValueOnce(childCostCenterNode2); // new parent found (level 1), parentNodeId is rootCostCenterNode.id
    // Walk up: childCostCenterNode2.parentNodeId = rootCostCenterNode.id, which is not the node being moved

    // Next walk iteration: need rootCostCenterNode
    spies.select.mockReturnValueOnce(rootCostCenterNode); // rootCostCenterNode.parentNodeId is null, loop ends

    spies.update.mockReturnValueOnce(movedNode);

    const result = await run(
      db,
      { nodeId: childCostCenterNode.id, newParentNodeId: childCostCenterNode2.id },
      commandCtx,
    );

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

  it("updates level based on new parent level", async () => {
    const movedNode = {
      ...childCostCenterNode,
      parentNodeId: rootCostCenterNode.id,
      level: 1,
      updatedAt: new Date(),
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(childCostCenterNode) // node exists (current parent is root already, but we test the path)
      .mockReturnValueOnce(rootCostCenterNode); // new parent (level 0)
    // rootCostCenterNode.parentNodeId is null, so circular check loop doesn't iterate

    spies.update.mockReturnValueOnce(movedNode);

    const result = await run(
      db,
      { nodeId: childCostCenterNode.id, newParentNodeId: rootCostCenterNode.id },
      commandCtx,
    );

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