import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  DescriptionRequiredError,
  EmptyPurchaseRequisitionLinesError,
  InvalidEstimatedUnitPriceError,
  InvalidQuantityError,
  ItemNotFoundError,
  PartnerNotSupplierError,
  PurchaseRequisitionNotDraftError,
  PurchaseRequisitionNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk, mockBPQueries } from "../testing/commandTestUtils";
import {
  baseItem,
  basePurchaseRequisition,
  basePurchaseRequisitionLines,
  baseSupplier,
  baseUnit,
  inactiveSupplier,
  submittedPurchaseRequisition,
} from "../testing/fixtures";
import { run } from "./updatePurchaseRequisition";

describe("updatePurchaseRequisition", () => {
  it("updates line content on a draft purchase requisition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = { ...basePurchaseRequisitionLines[0], description: "Updated description" };
    const updated = { ...basePurchaseRequisition, justification: "Updated justification" };
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem); // selectOne Item
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      { id: basePurchaseRequisition.id, lines: [updatedLine] },
      commandCtx,
      mockBPQueries(),
    );

    expect(expectOk(result).purchaseRequisition).toBeDefined();
  });

  it("adds new lines to a draft requisition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem) // selectOne Item (line 1)
      .mockReturnValueOnce(baseItem); // selectOne Item (line 2)
    spies.update.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          basePurchaseRequisitionLines[0],
          { ...basePurchaseRequisitionLines[0], description: "New line" },
        ],
      },
      commandCtx,
      mockBPQueries(),
    );

    expect(expectOk(result).purchaseRequisition).toBeDefined();
  });

  it("removes lines from a draft requisition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem); // Item lookup
    spies.update.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          {
            description: "Remaining line",
            quantity: "1",
            estimatedUnitPrice: "10",
            itemId: baseItem.id,
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockBPQueries(),
    );

    expect(expectOk(result).purchaseRequisition).toBeDefined();
  });

  it("returns error when all lines are removed from the requisition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseRequisition); // selectOne PurchaseRequisition

    const result = await run(
      db,
      { id: basePurchaseRequisition.id, lines: [] },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, EmptyPurchaseRequisitionLinesError);
  });

  it("returns error when purchase requisition does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { id: "missing-pr" }, commandCtx, mockBPQueries());

    expectErr(result, PurchaseRequisitionNotFoundError);
  });

  it("returns error when purchase requisition is not in draft", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(submittedPurchaseRequisition);

    const result = await run(
      db,
      { id: submittedPurchaseRequisition.id },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, PurchaseRequisitionNotDraftError);
  });

  it("returns error when a line is missing a description", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [{ ...basePurchaseRequisitionLines[0], description: "" }],
      },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, DescriptionRequiredError);
  });

  it("returns error when quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [{ ...basePurchaseRequisitionLines[0], quantity: "-1" }],
      },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, InvalidQuantityError);
  });

  it("returns error when estimated unit price is negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [{ ...basePurchaseRequisitionLines[0], estimatedUnitPrice: "-5" }],
      },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, InvalidEstimatedUnitPriceError);
  });

  it("returns error when a referenced item does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(undefined); // selectOne Item

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [{ ...basePurchaseRequisitionLines[0], itemId: "missing-item" }],
      },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, ItemNotFoundError);
  });

  it("returns error when a suggested supplier does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: undefined });
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem); // selectOne Item

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          {
            ...basePurchaseRequisitionLines[0],
            suggestedSupplierId: "missing-supplier",
          },
        ],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, SupplierNotFoundError);
  });

  it("returns error when a suggested supplier is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: inactiveSupplier });
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem); // selectOne Item

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          {
            ...basePurchaseRequisitionLines[0],
            suggestedSupplierId: inactiveSupplier.id,
          },
        ],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, SupplierNotActiveError);
  });

  it("preserves existing justification and desiredDeliveryDate when not provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const existing = {
      ...basePurchaseRequisition,
      justification: "Existing justification",
      desiredDeliveryDate: new Date("2024-06-01"),
    };
    spies.select.mockReturnValueOnce(existing);

    const result = await run(db, { id: basePurchaseRequisition.id }, commandCtx, mockBPQueries());

    expectOk(result);
    expect(spies.update).not.toHaveBeenCalled();
  });

  it("returns error when line description is null", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseRequisition);

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          {
            ...basePurchaseRequisitionLines[0],
            description: null as unknown as string,
          },
        ],
      },
      commandCtx,
      mockBPQueries(),
    );

    expectErr(result, DescriptionRequiredError);
  });

  it("returns error when a suggested supplier is not classified as a supplier", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: undefined });
    spies.select
      .mockReturnValueOnce(basePurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce(baseItem); // selectOne Item

    const result = await run(
      db,
      {
        id: basePurchaseRequisition.id,
        lines: [
          {
            ...basePurchaseRequisitionLines[0],
            suggestedSupplierId: baseSupplier.id,
          },
        ],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, PartnerNotSupplierError);
  });
});
