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

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

  it("returns error when code already exists", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseRootNode]);

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], []);

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [], // getTaxonomyNode: code unique
      [baseLeafNode], // getTaxonomyNode: parent exists (node-3)
      [baseLeafNode], // calculateNodeDepth: lookup node-3 (parentId=node-2)
      [baseChildNode], // calculateNodeDepth: lookup node-2 (parentId=node-1)
      [baseRootNode], // calculateNodeDepth: lookup node-1 (parentId=null, stop)
    );

    // parentDepth=3, new node depth=4 > maxDepth=3
    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    const createdNode = { ...baseRootNode, id: "new-node-id", code: "CLOTHING" };
    mock.enqueueResults([], [createdNode]);

    const result = await mock.withTx((trx) =>
      run(trx, { 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(mock.inserts.length).toBeGreaterThan(0);
  });

  it("creates a child node under existing parent", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const createdNode = {
      ...baseChildNode,
      id: "new-node-id",
      code: "TABLETS",
      parentId: baseRootNode.id,
    };
    mock.enqueueResults(
      [], // getTaxonomyNode: code unique
      [baseRootNode], // getTaxonomyNode: parent exists (root)
      [baseRootNode], // calculateNodeDepth: lookup root (parentId=null, depth=1)
      [createdNode],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    const createdNode = { ...baseRootNode, id: "new-node-id", sortOrder: 1 };
    mock.enqueueResults([], [createdNode]);

    await mock.withTx((trx) =>
      run(trx, { code: "SORTED", name: "Sorted Node", sortOrder: 1 }, ctx, { maxDepth: 10 }),
    );

    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ sortOrder: 1 }));
  });
});
