import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  ParentItemNotFoundError,
  ParentItemNotManufacturedError,
  InvalidBomTypeError,
  ComponentLineRequiredError,
  InvalidComponentQuantityError,
  ComponentItemInactiveError,
  BomVersionConflictError,
} from "../lib/errors.generated";
import { baseDraftBom, baseBomLine } from "../testing/fixtures";
import { run } from "./createBillOfMaterial";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

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

  const validInput = {
    parentItemId: "item-1",
    companyId: "company-1",
    siteId: null,
    bomType: "MANUFACTURE",
    effectivityStartDate: null,
    effectivityEndDate: null,
    defaultSelection: true,
    revisionNumber: "V1",
    lines: [
      {
        itemId: "item-2",
        requiredQuantity: 2.0,
        unitOfMeasure: "EA",
        scrapAssumption: 5.0,
        isSubassembly: false,
      },
    ],
  };

  it("creates a draft BOM for an active manufactured item", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdBom = {
      ...baseDraftBom,
      id: "new-bom-id",
    };

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
    // component item exists
    spies.select.mockReturnValueOnce({ id: "item-2", status: "ACTIVE" });
    // no existing BOM with same version identity
    spies.select.mockReturnValueOnce(undefined);
    // insert BOM
    spies.insert.mockReturnValueOnce(createdBom);
    // insert BOM line
    spies.insert.mockReturnValueOnce({ ...baseBomLine, billOfMaterialId: "new-bom-id" });

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.billOfMaterial.status).toBe("DRAFT");
      expect(result.value.billOfMaterial.id).toBe("new-bom-id");
    }
    expect(spies.insert).toHaveBeenCalled();
  });

  it("returns error when the parent item does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item not found
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentItemNotFoundError);
    }
  });

  it("returns error when the parent item is not manufacturable", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists but not manufacturable
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: false });

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentItemNotManufacturedError);
    }
  });

  it("returns error when bomType is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });

    const result = await run(db, { ...validInput, bomType: "UNSUPPORTED" }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidBomTypeError);
    }
  });

  it("returns error when no component lines are provided", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });

    const result = await run(db, { ...validInput, lines: [] }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ComponentLineRequiredError);
    }
  });

  it("returns error when a component quantity is not positive", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });

    const result = await run(
      db,
      {
        ...validInput,
        lines: [{ itemId: "item-2", requiredQuantity: 0, unitOfMeasure: "EA" }],
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidComponentQuantityError);
    }
  });

  it("returns error when a component item is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
    // component item not found (treated as inactive)
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
    }
  });

  it("returns error when a component item exists but is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
    // component item exists but is inactive
    spies.select.mockReturnValueOnce({ id: "item-2", isActive: false });

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
    }
  });

  it("returns error when the version identity already exists in scope", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // parent item exists
    spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
    // component item exists
    spies.select.mockReturnValueOnce({ id: "item-2", status: "ACTIVE" });
    // existing BOM with same version identity
    spies.select.mockReturnValueOnce(baseDraftBom);

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(BomVersionConflictError);
    }
  });

  it("returns error when the parent item is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();

    spies.select.mockReturnValueOnce({ id: "item-1", status: "INACTIVE", isManufacturable: true });

    const result = await run(db, validInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ParentItemNotManufacturedError);
    }
  });
});
