import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { DB } from "../generated/kysely-tailordb";
import { expectOk, expectErr } from "../testing/commandTestUtils";
import { CompanyScopeRequiredError, ItemReferenceRequiredError } from "../lib/errors.generated";
import { run } from "./detectBillOfMaterialCircularReference";

describe("detectBillOfMaterialCircularReference", () => {
  it("detects a direct BOM loop", async () => {
    const { db, spies } = createMockDb<DB>();
    // B has an active BOM with a subassembly line pointing back to A
    spies.select.mockReturnValueOnce([{ id: "bom-b" }]); // active BOMs for B
    spies.select.mockReturnValueOnce([{ itemId: "item-1" }]); // subassembly lines -> back to A

    const result = await run(db, {
      companyId: "company-1",
      parentItemId: "item-1",
      childItemId: "item-2",
    });
    const value = expectOk<{
      hasCircularReference: boolean;
      path: string[];
    }>(result as never);

    expect(value.hasCircularReference).toBe(true);
    expect(value.path).toContain("item-1");
    expect(value.path).toContain("item-2");
  });

  it("detects an indirect BOM loop across multiple subassemblies", async () => {
    const { db, spies } = createMockDb<DB>();
    // A -> B -> C -> A (circular)
    // B has active BOM with subassembly line to C
    spies.select.mockReturnValueOnce([{ id: "bom-b" }]); // active BOMs for B
    spies.select.mockReturnValueOnce([{ itemId: "item-3" }]); // subassembly lines -> C
    // C has active BOM with subassembly line back to A
    spies.select.mockReturnValueOnce([{ id: "bom-c" }]); // active BOMs for C
    spies.select.mockReturnValueOnce([{ itemId: "item-1" }]); // subassembly lines -> A (circular!)

    const result = await run(db, {
      companyId: "company-1",
      parentItemId: "item-1",
      childItemId: "item-2",
    });
    const value = expectOk<{
      hasCircularReference: boolean;
      path: string[];
    }>(result as never);

    expect(value.hasCircularReference).toBe(true);
    expect(value.path).toContain("item-1");
  });

  it("returns no loop when the graph is acyclic", async () => {
    const { db, spies } = createMockDb<DB>();
    // B has active BOM with subassembly line to C (not A)
    spies.select.mockReturnValueOnce([{ id: "bom-b" }]); // active BOMs for B
    spies.select.mockReturnValueOnce([{ itemId: "item-3" }]); // subassembly lines -> C
    // C has no active BOMs (leaf node)
    spies.select.mockReturnValueOnce([]); // active BOMs for C - none

    const result = await run(db, {
      companyId: "company-1",
      parentItemId: "item-1",
      childItemId: "item-2",
    });
    const value = expectOk<{
      hasCircularReference: boolean;
      path: string[] | null;
    }>(result as never);

    expect(value.hasCircularReference).toBe(false);
    expect(value.path).toBeNull();
  });

  it("scopes detection to the requested company", async () => {
    const { db, spies } = createMockDb<DB>();
    // No active BOMs found for this company scope
    spies.select.mockReturnValueOnce([]); // no active BOMs for B in company-2

    const result = await run(db, {
      companyId: "company-2",
      parentItemId: "item-1",
      childItemId: "item-2",
    });
    const value = expectOk<{
      hasCircularReference: boolean;
      path: string[] | null;
    }>(result as never);

    expect(value.hasCircularReference).toBe(false);
  });

  it("returns error when parent or child reference is missing", async () => {
    const { db } = createMockDb<DB>();

    const result = await run(db, {
      companyId: "company-1",
      parentItemId: "item-1",
      childItemId: "",
    });
    expectErr(result as never, ItemReferenceRequiredError);
  });

  it("returns error when company scope is missing", async () => {
    const { db } = createMockDb<DB>();

    const result = await run(db, {
      companyId: "",
      parentItemId: "item-1",
      childItemId: "item-2",
    });
    expectErr(result as never, CompanyScopeRequiredError);
  });
});
