import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  EmptyIssueLinesError,
  InsufficientStockError,
  InvalidStatusError,
} from "../lib/errors.generated";
import { expectErr, expectOk, mockItemManagementQueries } from "../testing/commandTestUtils";
import {
  baseActiveItem,
  baseGoodsIssueExecution,
  baseGoodsIssueExecutionLine,
  baseStockLevel,
  baseStockReservation,
  baseStorageLocation,
} from "../testing/fixtures";
import { run } from "./postGoodsIssue";

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

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

    const result = await mock.withTx((trx) =>
      run(trx, { id: "missing" }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

    expectErr(result as never, InvalidStatusError);
  });

  it("returns error when execution has no lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseGoodsIssueExecution], []);

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseGoodsIssueExecution.id }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

    expectErr(result as never, EmptyIssueLinesError);
    expect(mock.updates).toHaveLength(0);
  });

  it("posts execution lines into ledger and stock", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const ledger = {
      id: "ledger-1",
      inventoryExecutionId: baseGoodsIssueExecution.id,
      dcIndicator: "CR",
    };
    const postedExecution = {
      ...baseGoodsIssueExecution,
      status: "POSTED",
      postedAt: new Date("2024-01-10T00:00:00.000Z"),
    };

    mock.enqueueResults(
      [baseGoodsIssueExecution],
      [baseGoodsIssueExecutionLine],
      [baseStorageLocation],
      [{ id: baseStorageLocation.id }],
      [baseStockLevel],
      [],
      [ledger],
      [{ ...baseStockLevel, quantity: "80" }],
      [],
      [postedExecution],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseGoodsIssueExecution.id }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

    const value = expectOk<{
      inventoryExecution: { status: string };
      inventoryLedgerEntries: {
        inventoryExecutionId: string;
        dcIndicator: string;
      }[];
    }>(result as never);

    expect(value.inventoryExecution.status).toBe("POSTED");
    expect(value.inventoryLedgerEntries[0]).toMatchObject({
      inventoryExecutionId: baseGoodsIssueExecution.id,
      dcIndicator: "CR",
    });
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        inventoryExecutionId: baseGoodsIssueExecution.id,
        quantity: "5",
      }),
    );
  });

  it("consumes matching stock reservations when posting", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const matchingReservation = {
      ...baseStockReservation,
      sourceId: baseGoodsIssueExecution.sourceDocumentId,
      sourceLineId: baseGoodsIssueExecutionLine.sourceLineId ?? "",
      reservedQuantity: "10",
      consumedQuantity: "2",
    };
    const ledger = {
      id: "ledger-1",
      inventoryExecutionId: baseGoodsIssueExecution.id,
      dcIndicator: "CR",
    };
    const postedExecution = {
      ...baseGoodsIssueExecution,
      status: "POSTED",
      postedAt: new Date("2024-01-10T00:00:00.000Z"),
    };

    mock.enqueueResults(
      [baseGoodsIssueExecution],
      [baseGoodsIssueExecutionLine],
      [baseStorageLocation],
      [{ id: baseStorageLocation.id }],
      [baseStockLevel],
      [matchingReservation],
      [matchingReservation],
      [{ ...matchingReservation, consumedQuantity: "7" }],
      [ledger],
      [{ ...baseStockLevel, quantity: "80" }],
      [],
      [postedExecution],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseGoodsIssueExecution.id }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

    expectOk(result as never);
    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({
        consumedQuantity: "7",
        status: "OPEN",
      }),
    );
  });

  it("does not consume stock reserved for other demand", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const constrainedStockLevel = { ...baseStockLevel, quantity: "10" };
    const otherReservation = {
      ...baseStockReservation,
      sourceId: "other-sales-order",
      sourceLineId: "other-sales-order-line",
      reservedQuantity: "8",
      consumedQuantity: "0",
    };

    mock.enqueueResults(
      [baseGoodsIssueExecution],
      [{ ...baseGoodsIssueExecutionLine, sourceLineId: null }],
      [baseStorageLocation],
      [{ id: baseStorageLocation.id }],
      [constrainedStockLevel],
      [otherReservation],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseGoodsIssueExecution.id }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

    expectErr(result as never, InsufficientStockError);
  });
});
