import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  BomReferenceRequiredError,
  EffectivityDateRequiredError,
  BomNotFoundError,
  BomNotEffectiveOnDateError,
  ChildBomNotResolvedError,
  BomCircularReferenceDetectedError,
} from "../lib/errors.generated";
import { expectOk, expectErr } from "../testing/commandTestUtils";
import { run } from "./explodeBillOfMaterial";

const effectivityDate = new Date("2024-06-01T00:00:00.000Z");

const rootBom = {
  id: "bom-1",
  parentItemId: "item-1",
  companyId: "company-1",
  siteId: null,
  bomType: "MANUFACTURE",
  effectivityStartDate: null,
  effectivityEndDate: null,
  defaultSelection: true,
  revisionNumber: "V1",
  status: "ACTIVE",
  createdAt: new Date("2024-01-01T00:00:00.000Z"),
  updatedAt: null,
};

describe("explodeBillOfMaterial", () => {
  it("explodes a multi-level manufacture BOM into recursive component requirements", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM
      [rootBom],
      // root BOM lines
      [
        {
          itemId: "item-2",
          requiredQuantity: 2,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-1",
        },
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // active child BOMs for item-3
      [
        {
          id: "bom-child",
          parentItemId: "item-3",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
      ],
      // child BOM lines
      [
        {
          itemId: "item-4",
          requiredQuantity: 3,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-child",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{ components: unknown[] }>(result as never);

    expect(value.components).toHaveLength(3);
    expect(mock.selects).toHaveLength(4);
    expect(mock.selects[0].parameters).toContain("bom-1");
    expect(mock.selects[0].sql).toMatch(/"id"/);
    expect(mock.selects[1].parameters).toContain("bom-1");
    expect(mock.selects[1].sql).toMatch(/"billOfMaterialId"/);
    expect(mock.selects[2].parameters).toContain("item-3");
    expect(mock.selects[2].sql).toMatch(/"parentItemId"/);
    expect(mock.selects[3].parameters).toContain("bom-child");
    expect(mock.selects[3].sql).toMatch(/"billOfMaterialId"/);
  });

  it("flattens phantom subassemblies into the nearest non-phantom parent path", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM (MANUFACTURE type with a PHANTOM subassembly)
      [rootBom],
      // root BOM lines - one subassembly that is phantom
      [
        {
          itemId: "item-phantom",
          requiredQuantity: 2,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // Resolve child BOMs for item-phantom — it's a PHANTOM BOM
      [
        {
          id: "bom-phantom",
          parentItemId: "item-phantom",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "PHANTOM",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
      ],
      // PHANTOM BOM lines — these should be flattened
      [
        {
          itemId: "item-raw-1",
          requiredQuantity: 3,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-phantom",
        },
        {
          itemId: "item-raw-2",
          requiredQuantity: 5,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-phantom",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{
      components: { itemId: string; requiredQuantity: number; depth: number }[];
    }>(result as never);

    // Phantom subassembly is NOT in the output — its children are flattened
    expect(value.components).toHaveLength(2);
    const raw1 = value.components.find((c) => c.itemId === "item-raw-1");
    expect(raw1).toBeDefined();
    expect(raw1!.requiredQuantity).toBe(6); // 2 * 3
    expect(raw1!.depth).toBe(1); // flattened to parent depth
    const raw2 = value.components.find((c) => c.itemId === "item-raw-2");
    expect(raw2).toBeDefined();
    expect(raw2!.requiredQuantity).toBe(10); // 2 * 5
    expect(raw2!.depth).toBe(1);
  });

  it("expands a kit BOM into component issue requirements without work-order context for the kit parent", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const kitBom = { ...rootBom, bomType: "KIT" };
    mock.enqueueResults(
      // root BOM (kit type)
      [kitBom],
      // kit BOM lines — includes one marked as subassembly
      [
        {
          itemId: "item-2",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-1",
        },
        {
          itemId: "item-3",
          requiredQuantity: 2,
          unitOfMeasure: "EA",
          isSubassembly: true, // even subassemblies treated as issue-only
          billOfMaterialId: "bom-1",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{
      components: { itemId: string; isSubassembly: boolean }[];
    }>(result as never);

    expect(value.components).toHaveLength(2);
    // All components are issue-only (isSubassembly = false)
    expect(value.components.every((c) => !c.isSubassembly)).toBe(true);
    expect(mock.selects).toHaveLength(2);
  });

  it("resolves lower-level child BOM versions using the requested effectivity date", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM
      [rootBom],
      // root BOM lines - one subassembly
      [
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // active child BOMs for item-3 - two versions, only one effective on date
      [
        {
          id: "bom-child-old",
          parentItemId: "item-3",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: "2024-01-01T00:00:00.000Z",
        },
        {
          id: "bom-child-new",
          parentItemId: "item-3",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: "2024-02-01T00:00:00.000Z",
          effectivityEndDate: null,
        },
      ],
      // child BOM lines for the effective version
      [
        {
          itemId: "item-5",
          requiredQuantity: 4,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-child-new",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{ components: unknown[] }>(result as never);

    // subassembly line + its child component
    expect(value.components).toHaveLength(2);
  });

  it("resolves lower-level child BOMs using the applicable site scope", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...rootBom, siteId: "site-1" }],
      [
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      [
        {
          id: "bom-child-other-site",
          parentItemId: "item-3",
          companyId: "company-1",
          siteId: "site-2",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
        {
          id: "bom-child-site-1",
          parentItemId: "item-3",
          companyId: "company-1",
          siteId: "site-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
      ],
      [
        {
          itemId: "item-5",
          requiredQuantity: 4,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-child-site-1",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{ components: { bomPath: string[] }[] }>(result as never);

    expect(
      value.components.some((component) => component.bomPath.includes("bom-child-site-1")),
    ).toBe(true);
    expect(
      value.components.some((component) => component.bomPath.includes("bom-child-other-site")),
    ).toBe(false);
  });

  it("returns cumulative quantities and source BOM path metadata for exploded components", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM
      [rootBom],
      // root BOM lines - subassembly with quantity multiplier
      [
        {
          itemId: "item-3",
          requiredQuantity: 2,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // active child BOMs for item-3
      [
        {
          id: "bom-child",
          parentItemId: "item-3",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
      ],
      // child BOM lines
      [
        {
          itemId: "item-4",
          requiredQuantity: 3,
          unitOfMeasure: "EA",
          isSubassembly: false,
          billOfMaterialId: "bom-child",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    const value = expectOk<{
      components: {
        itemId: string;
        requiredQuantity: number;
        depth: number;
        bomPath: string[];
      }[];
    }>(result as never);

    // item-3 subassembly: qty 2, depth 1
    const subassembly = value.components.find((c) => c.itemId === "item-3");
    expect(subassembly).toBeDefined();
    expect(subassembly!.requiredQuantity).toBe(2);
    expect(subassembly!.depth).toBe(1);

    // item-4 leaf: qty 2 * 3 = 6, depth 2
    const leaf = value.components.find((c) => c.itemId === "item-4");
    expect(leaf).toBeDefined();
    expect(leaf!.requiredQuantity).toBe(6);
    expect(leaf!.depth).toBe(2);
    expect(leaf!.bomPath).toContain("bom-1");
    expect(leaf!.bomPath).toContain("bom-child");
  });

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

    const result = await run(mock.db, { bomId: "", effectivityDate });
    expectErr(result as never, BomReferenceRequiredError);
  });

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

    const result = await run(mock.db, {
      bomId: "bom-1",
      effectivityDate: undefined as unknown as Date,
    });
    expectErr(result as never, EffectivityDateRequiredError);
  });

  it("returns error when the root BOM is not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await run(mock.db, { bomId: "nonexistent", effectivityDate });
    expectErr(result as never, BomNotFoundError);
  });

  it("returns error when the root BOM is not effective on the requested date", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const expiredBom = {
      ...rootBom,
      effectivityEndDate: "2023-01-01T00:00:00.000Z",
    };
    mock.enqueueResults([expiredBom]);

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    expectErr(result as never, BomNotEffectiveOnDateError);
  });

  it("returns error when a required lower-level child BOM cannot be resolved", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM
      [rootBom],
      // root BOM lines - one subassembly
      [
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // no active child BOMs for item-3
      [],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    expectErr(result as never, ChildBomNotResolvedError);
  });

  it("returns error when the explosion detects a circular BOM reference", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      // root BOM
      [rootBom],
      // root BOM lines - subassembly item-3
      [
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-1",
        },
      ],
      // active child BOMs for item-3
      [
        {
          id: "bom-child",
          parentItemId: "item-3",
          companyId: "company-1",
          status: "ACTIVE",
          bomType: "MANUFACTURE",
          effectivityStartDate: null,
          effectivityEndDate: null,
        },
      ],
      // child BOM lines - subassembly pointing back to item-3 (circular!)
      [
        {
          itemId: "item-3",
          requiredQuantity: 1,
          unitOfMeasure: "EA",
          isSubassembly: true,
          billOfMaterialId: "bom-child",
        },
      ],
    );

    const result = await run(mock.db, { bomId: "bom-1", effectivityDate });
    expectErr(result as never, BomCircularReferenceDetectedError);
  });
});
