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

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

  it("returns error when code already exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseRootNode); // Code lookup finds existing

    const result = await run(db, { code: "ELECTRONICS", name: "Electronics" }, ctx, {
      maxDepth: 10,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateNodeCodeError);
    }
  });

  it("returns error when parent does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(undefined) // Code unique
      .mockReturnValueOnce(undefined); // Parent not found

    const result = await run(
      db,
      { code: "LAPTOPS", name: "Laptops", parentId: "nonexistent" },
      ctx,
      { maxDepth: 10 },
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentNodeNotFoundError);
    }
  });

  it("returns error when max depth would be exceeded", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(undefined) // getTaxonomyNode: code unique
      .mockReturnValueOnce(baseLeafNode) // getTaxonomyNode: parent exists (node-3)
      .mockReturnValueOnce(baseLeafNode) // calculateNodeDepth: lookup node-3 (parentId=node-2)
      .mockReturnValueOnce(baseChildNode) // calculateNodeDepth: lookup node-2 (parentId=node-1)
      .mockReturnValueOnce(baseRootNode); // calculateNodeDepth: lookup node-1 (parentId=null, stop)

    // parentDepth=3, new node depth=4 > maxDepth=3
    const result = await run(
      db,
      { code: "DEEP", name: "Too Deep", parentId: baseLeafNode.id },
      ctx,
      { maxDepth: 3 },
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MaxDepthExceededError);
    }
  });

  it("creates a root node", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdNode = { ...baseRootNode, id: "new-node-id", code: "CLOTHING" };
    spies.select.mockReturnValueOnce(undefined); // Code unique
    spies.insert.mockReturnValue(createdNode);

    const result = await run(db, { code: "CLOTHING", name: "Clothing" }, ctx, { maxDepth: 10 });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.taxonomyNode.code).toBe("CLOTHING");
      expect(result.value.taxonomyNode.parentId).toBeNull();
    }
    expect(spies.insert).toHaveBeenCalled();
  });

  it("creates a child node under existing parent", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdNode = {
      ...baseChildNode,
      id: "new-node-id",
      code: "TABLETS",
      parentId: baseRootNode.id,
    };
    spies.select
      .mockReturnValueOnce(undefined) // getTaxonomyNode: code unique
      .mockReturnValueOnce(baseRootNode) // getTaxonomyNode: parent exists (root)
      .mockReturnValueOnce(baseRootNode); // calculateNodeDepth: lookup root (parentId=null, depth=1)
    spies.insert.mockReturnValue(createdNode);

    const result = await run(
      db,
      { code: "TABLETS", name: "Tablets", parentId: baseRootNode.id },
      ctx,
      { maxDepth: 10 },
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.taxonomyNode.code).toBe("TABLETS");
      expect(result.value.taxonomyNode.parentId).toBe(baseRootNode.id);
    }
  });

  it("passes custom fields through to insert", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdNode = { ...baseRootNode, id: "new-node-id", sortOrder: 1 };
    spies.select.mockReturnValueOnce(undefined); // Code unique
    spies.insert.mockReturnValue(createdNode);

    await run(db, { code: "SORTED", name: "Sorted Node", sortOrder: 1 }, ctx, { maxDepth: 10 });

    expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ sortOrder: 1 }));
  });
});
