import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import { Transaction } from "../generated/kysely-tailordb";
import {
  CircularReferenceError,
  MaxDepthExceededError,
  NodeNotFoundError,
  ParentNodeNotFoundError,
} from "../lib/errors.generated";
import { baseChildNode, baseLeafNode, baseRootNode, secondRootNode } from "../testing/fixtures";
import { run } from "./moveTaxonomyNode";
import { type CommandContext } from "@tailor-platform/erp-kit/module";

describe("moveTaxonomyNode", () => {
  const ctx: CommandContext = {
    actorId: "test-actor",
    permissions: ["item-management:moveTaxonomyNode"],
  };

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

    const result = await run(db, { id: "nonexistent", newParentId: null }, ctx, { maxDepth: 10 });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(NodeNotFoundError);
    }
  });

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

    const result = await run(db, { id: baseChildNode.id, newParentId: "nonexistent" }, ctx, {
      maxDepth: 10,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentNodeNotFoundError);
    }
  });

  it("returns error when moving node under itself", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseRootNode) // getTaxonomyNode for node
      .mockReturnValueOnce(baseRootNode); // getTaxonomyNode for new parent

    const result = await run(db, { id: baseRootNode.id, newParentId: baseRootNode.id }, ctx, {
      maxDepth: 10,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CircularReferenceError);
    }
  });

  it("returns error when moving node under its descendant", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseRootNode) // getTaxonomyNode for node (node-1)
      .mockReturnValueOnce(baseLeafNode) // getTaxonomyNode for new parent (node-3)
      .mockReturnValueOnce(baseLeafNode) // detectCircularReference: lookup node-3
      .mockReturnValueOnce(baseChildNode); // detectCircularReference: lookup node-2 → parentId=node-1=nodeId → circular!

    const result = await run(db, { id: baseRootNode.id, newParentId: baseLeafNode.id }, ctx, {
      maxDepth: 10,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CircularReferenceError);
    }
  });

  it("promotes node to root by setting parent to null", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const movedNode = { ...baseChildNode, parentId: null };
    spies.select.mockReturnValueOnce(baseChildNode); // getTaxonomyNode for node
    spies.update.mockReturnValue(movedNode);

    const result = await run(db, { id: baseChildNode.id, newParentId: null }, ctx, {
      maxDepth: 10,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.taxonomyNode.parentId).toBeNull();
    }
    expect(spies.update).toHaveBeenCalled();
  });

  it("moves node to different parent", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const movedNode = { ...baseChildNode, parentId: secondRootNode.id };
    spies.select
      .mockReturnValueOnce(baseChildNode) // getTaxonomyNode for node (node-2)
      .mockReturnValueOnce(secondRootNode) // getTaxonomyNode for new parent (node-4)
      .mockReturnValueOnce(secondRootNode) // detectCircularReference: lookup node-4 → parentId=null → not circular
      .mockReturnValueOnce(secondRootNode) // calculateNodeDepth: lookup node-4 → parentId=null → depth=1
      .mockReturnValueOnce([baseLeafNode]) // calculateSubtreeDepth: children of node-2
      .mockReturnValueOnce([]); // calculateSubtreeDepth: children of node-3 (leaf, no children)
    spies.update.mockReturnValue(movedNode);

    const result = await run(db, { id: baseChildNode.id, newParentId: secondRootNode.id }, ctx, {
      maxDepth: 10,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.taxonomyNode.parentId).toBe(secondRootNode.id);
    }
  });

  it("returns error when subtree depth + new parent depth exceeds maxDepth", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseChildNode) // getTaxonomyNode for node (node-2)
      .mockReturnValueOnce(secondRootNode) // getTaxonomyNode for new parent (node-4)
      .mockReturnValueOnce(secondRootNode) // detectCircularReference: lookup node-4 → not circular
      .mockReturnValueOnce(secondRootNode) // calculateNodeDepth: lookup node-4 → depth=1
      .mockReturnValueOnce([baseLeafNode]) // calculateSubtreeDepth: children of node-2
      .mockReturnValueOnce([]); // calculateSubtreeDepth: children of node-3 (leaf)

    // newParentDepth=1 (root), subtreeDepth=2 (child+leaf), totalDepth=3 > maxDepth=2
    const result = await run(db, { id: baseChildNode.id, newParentId: secondRootNode.id }, ctx, {
      maxDepth: 2,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MaxDepthExceededError);
    }
  });
});
