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 {
  CircularReferenceError,
  MaxDepthExceededError,
  NodeNotFoundError,
  ParentNodeNotFoundError,
} from "../lib/errors.generated";
import { baseChildNode, baseLeafNode, baseRootNode, secondRootNode } from "../testing/fixtures";
import { run } from "./moveTaxonomyNode";

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

  it("returns error when node does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

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

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

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseRootNode], // getTaxonomyNode for node (node-1)
      [baseLeafNode], // getTaxonomyNode for new parent (node-3)
      [baseLeafNode], // detectCircularReference: lookup node-3
      [baseChildNode], // detectCircularReference: lookup node-2 → parentId=node-1=nodeId → circular!
    );

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    const movedNode = { ...baseChildNode, parentId: null };
    mock.enqueueResults(
      [baseChildNode], // getTaxonomyNode for node
      [movedNode],
    );

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.taxonomyNode.parentId).toBeNull();
    }
    expect(mock.updates.length).toBeGreaterThan(0);
  });

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

    const result = await mock.withTx((trx) =>
      run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseChildNode], // getTaxonomyNode for node (node-2)
      [secondRootNode], // getTaxonomyNode for new parent (node-4)
      [secondRootNode], // detectCircularReference: lookup node-4 → not circular
      [secondRootNode], // calculateNodeDepth: lookup node-4 → depth=1
      [baseLeafNode], // calculateSubtreeDepth: children of node-2
      [], // calculateSubtreeDepth: children of node-3 (leaf)
    );

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