import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  DescriptionRequiredError,
  EmptyPurchaseRequisitionLinesError,
  InvalidEstimatedUnitPriceError,
  InvalidQuantityError,
  ItemNotFoundError,
  LineNotFoundError,
  PartnerNotSupplierError,
  PurchaseRequisitionNotDraftError,
  PurchaseRequisitionNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseItem,
  basePurchaseRequisition,
  basePurchaseRequisitionLine,
  basePurchaseRequisitionLines,
  baseSupplier,
  inactiveSupplier,
  submittedPurchaseRequisition,
} from "../testing/fixtures";
import { mockBPQueries, mockItemQueries } from "../testing/moduleMocks";
import { run } from "./updatePurchaseRequisition";

describe("updatePurchaseRequisition", () => {
  it("updates requisition metadata on a draft purchase requisition", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updated = { ...basePurchaseRequisition, justification: "Updated justification" };
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
      [updated], // updateTable PurchaseRequisition
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          headerPatch: { justification: "Updated justification" },
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

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

  it("updates a line in place so its id and createdAt survive the edit", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          updateLines: [{ lineId: basePurchaseRequisitionLine.id, linePatch: { quantity: "9" } }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectOk(result);
    // Patched in place — neither deleted nor reinserted.
    expect(mock.deletes).toHaveLength(0);
    expect(mock.inserts).toHaveLength(0);
    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ quantity: "9" }));
  });

  it("adds a requisition line via addLines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], description: "New line" }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()).toHaveLength(1);
  });

  it("removes a requisition line via removeLineIds", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const secondLine = { ...basePurchaseRequisitionLine, id: "pr-line-2" };
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine, secondLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: basePurchaseRequisition.id, removeLineIds: [basePurchaseRequisitionLine.id] },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectOk(result);
    expect(mock.deletes).toHaveLength(1);
  });

  it("returns error when purchase requisition does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: "missing-pr" },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, PurchaseRequisitionNotFoundError);
  });

  it("returns error when purchase requisition is not in draft", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([submittedPurchaseRequisition]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: submittedPurchaseRequisition.id },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, PurchaseRequisitionNotDraftError);
  });

  it("returns error when an update targets a missing line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          updateLines: [{ lineId: "missing-line", linePatch: { quantity: "9" } }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, LineNotFoundError);
  });

  it("returns error when a removal would leave the requisition without lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: basePurchaseRequisition.id, removeLineIds: [basePurchaseRequisitionLine.id] },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, EmptyPurchaseRequisitionLinesError);
  });

  it("returns error when a line is missing a description", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], description: "" }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, DescriptionRequiredError);
  });

  it("returns error when quantity is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], quantity: "-1" }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, InvalidQuantityError);
  });

  it("returns error when estimated unit price is negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], estimatedUnitPrice: "-5" }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, InvalidEstimatedUnitPriceError);
  });

  it("returns error when a referenced item does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], itemId: "missing-item" }],
        },
        commandCtx,
        mockBPQueries(),
        mockItemQueries(),
      ),
    );

    expectErr(result, ItemNotFoundError);
  });

  it("returns error when a suggested supplier does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [
            { ...basePurchaseRequisitionLines[0], suggestedSupplierId: "missing-supplier" },
          ],
        },
        commandCtx,
        bpQ,
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, SupplierNotFoundError);
  });

  it("returns error when a suggested supplier is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: inactiveSupplier });
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [
            { ...basePurchaseRequisitionLines[0], suggestedSupplierId: inactiveSupplier.id },
          ],
        },
        commandCtx,
        bpQ,
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, SupplierNotActiveError);
  });

  it("returns error when a suggested supplier is not classified as a supplier", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier });
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseRequisition.id,
          addLines: [{ ...basePurchaseRequisitionLines[0], suggestedSupplierId: baseSupplier.id }],
        },
        commandCtx,
        bpQ,
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectErr(result, PartnerNotSupplierError);
  });

  it("preserves existing fields when no patch or line changes are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseRequisition], // PurchaseRequisition
      [basePurchaseRequisitionLine], // PurchaseRequisitionLine (current lines)
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: basePurchaseRequisition.id },
        commandCtx,
        mockBPQueries(),
        mockItemQueries({ item: baseItem }),
      ),
    );

    expectOk(result);
    expect(mock.updates).toHaveLength(0);
  });
});
