import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import { baseRootCategory, baseChildCategory, baseLeafCategory } from "../testing/fixtures";
import {
  CategoryNotFoundError,
  ParentNotFoundError,
  CircularReferenceError,
  MaxDepthExceededError,
} from "../lib/errors.generated";
import type { MoveProductCategoryInput } from "./moveProductCategory";
import { run } from "./moveProductCategory";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

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

  const config = { maxDepth: 3 };

  it("should return CategoryNotFoundError when category does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // getProductCategory(id) -> null
    spies.select.mockReturnValueOnce(undefined);
    const input: MoveProductCategoryInput = {
      id: "non-existent",
      newParentId: baseRootCategory.id,
    };
    const result = await run(db, input, ctx, config);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CategoryNotFoundError);
    }
  });

  it("should return ParentNotFoundError when new parent does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. getProductCategory(id) -> found
    spies.select.mockReturnValueOnce(baseChildCategory);
    // 2. getProductCategory(newParentId) -> null
    spies.select.mockReturnValueOnce(undefined);
    const input: MoveProductCategoryInput = {
      id: baseChildCategory.id,
      newParentId: "non-existent",
    };
    const result = await run(db, input, ctx, config);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentNotFoundError);
    }
  });

  it("should return CircularReferenceError when move would create a cycle", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. getProductCategory(id=category-1) -> found (root)
    spies.select.mockReturnValueOnce(baseRootCategory);
    // 2. getProductCategory(newParentId=category-3) -> found (leaf, child of child of root)
    spies.select.mockReturnValueOnce(baseLeafCategory);
    // 3. detectCategoryCircularReference walks parent chain from newParentId:
    //    input.newParentId !== input.categoryId so enters while loop
    //    1st iteration: select parentId where id=category-3 -> { parentId: "category-2" }
    spies.select.mockReturnValueOnce({ parentId: "category-2" });
    //    currentId=category-2, not equal to category-1, continue
    //    2nd iteration: select parentId where id=category-2 -> { parentId: "category-1" }
    spies.select.mockReturnValueOnce({ parentId: "category-1" });
    //    currentId=category-1, equals categoryId -> isCircular: true

    const input: MoveProductCategoryInput = {
      id: baseRootCategory.id,
      newParentId: baseLeafCategory.id,
    };
    const result = await run(db, input, ctx, config);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CircularReferenceError);
    }
  });

  it("should return MaxDepthExceededError when move would exceed max depth", async () => {
    // Use a scenario: move leaf (category-3) under a deep parent
    const { db: db2, spies: spies2 } = createMockDb<Transaction>();
    // 1. getProductCategory(id=category-3) -> leaf node
    spies2.select.mockReturnValueOnce(baseLeafCategory);
    // 2. getProductCategory(newParentId=category-2) -> child node
    spies2.select.mockReturnValueOnce(baseChildCategory);
    // 3. detectCategoryCircularReference: walk from category-2
    //    1st: id=category-2 -> { parentId: "category-1" }
    spies2.select.mockReturnValueOnce({ parentId: "category-1" });
    //    category-1 !== category-3, continue
    //    2nd: id=category-1 -> { parentId: null }
    spies2.select.mockReturnValueOnce({ parentId: null });
    //    currentId=null -> break -> isCircular: false
    // 4. calculateCategoryDepth for newParentId=category-2
    //    1st: id=category-2 -> { parentId: "category-1" }
    spies2.select.mockReturnValueOnce({ parentId: "category-1" });
    //    2nd: id=category-1 -> { parentId: null }
    spies2.select.mockReturnValueOnce({ parentId: null });
    //    depth=2
    // 5. calculateSubtreeDepth for category-3: no children
    spies2.select.mockReturnValueOnce([]); // execute() returns array for children query
    //    subtreeDepth=1, totalDepth=2+1=3, maxDepth=2 -> exceed

    const input: MoveProductCategoryInput = {
      id: baseLeafCategory.id,
      newParentId: baseChildCategory.id,
    };
    const result = await run(db2, input, ctx, { maxDepth: 2 });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MaxDepthExceededError);
    }
  });

  it("should move category to new parent successfully", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. getProductCategory(id=category-3) -> leaf
    spies.select.mockReturnValueOnce(baseLeafCategory);
    // 2. getProductCategory(newParentId=category-1) -> root
    spies.select.mockReturnValueOnce(baseRootCategory);
    // 3. detectCategoryCircularReference: walk from category-1
    //    1st: id=category-1 -> { parentId: null }
    spies.select.mockReturnValueOnce({ parentId: null });
    //    currentId=null -> break -> isCircular: false
    // 4. calculateCategoryDepth for category-1
    //    1st: id=category-1 -> { parentId: null }
    spies.select.mockReturnValueOnce({ parentId: null });
    //    depth=1
    // 5. calculateSubtreeDepth for category-3: no children
    spies.select.mockReturnValueOnce([]); // children query -> empty
    //    subtreeDepth=1, totalDepth=1+1=2, maxDepth=3 -> ok
    // 6. update returns moved category
    const movedCategory = {
      ...baseLeafCategory,
      parentId: baseRootCategory.id,
      updatedAt: new Date(),
    };
    spies.update.mockReturnValueOnce(movedCategory);

    const input: MoveProductCategoryInput = {
      id: baseLeafCategory.id,
      newParentId: baseRootCategory.id,
    };
    const result = await run(db, input, ctx, config);
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.category.parentId).toBe(baseRootCategory.id);
    }
  });

  it("should promote category to root when newParentId is null", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. getProductCategory(id=category-2) -> child
    spies.select.mockReturnValueOnce(baseChildCategory);
    // newParentId=null -> promote to root, update directly
    const movedCategory = { ...baseChildCategory, parentId: null, updatedAt: new Date() };
    spies.update.mockReturnValueOnce(movedCategory);

    const input: MoveProductCategoryInput = {
      id: baseChildCategory.id,
      newParentId: null,
    };
    const result = await run(db, input, ctx, config);
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.category.parentId).toBeNull();
    }
  });
});
