import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import { CompanyScopeRequiredError, ItemReferenceRequiredError } from "../lib/errors.generated";
import { expectOk, expectErr } from "../testing/commandTestUtils";
import { run } from "./detectBillOfMaterialCircularReference";

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

    const result = await run(mock.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");
    expect(mock.selects).toHaveLength(2);
    expect(mock.selects[0].parameters).toContain("item-2");
    expect(mock.selects[0].parameters).toContain("company-1");
    expect(mock.selects[0].parameters).toContain("ACTIVE");
    expect(mock.selects[0].sql).toMatch(/"parentItemId"/);
    expect(mock.selects[0].sql).toMatch(/"companyId"/);
    expect(mock.selects[0].sql).toMatch(/"status"/);
    expect(mock.selects[1].parameters).toContain("bom-b");
    expect(mock.selects[1].sql).toMatch(/"billOfMaterialId"/);
    expect(mock.selects[1].sql).toMatch(/"isSubassembly"/);
  });

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

    const result = await run(mock.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(mock.selects).toHaveLength(4);
  });

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

    const result = await run(mock.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();
    expect(mock.selects).toHaveLength(3);
  });

  it("scopes detection to the requested company", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // No active BOMs found for this company scope
    mock.enqueueResults([]); // no active BOMs for B in company-2

    const result = await run(mock.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);
    expect(mock.selects).toHaveLength(1);
    expect(mock.selects[0].parameters).toContain("company-2");
    expect(mock.selects[0].sql).toMatch(/"companyId"/);
  });

  it("returns error when parent or child reference is missing", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

  it("returns error when company scope is missing", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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