import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } 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 { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { id: "missing" }, ctx, mockItemManagementQueries(baseActiveItem));

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

  it("returns error when execution has no lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseGoodsIssueExecution).mockReturnValueOnce([]);

    const result = await run(
      db,
      { id: baseGoodsIssueExecution.id },
      ctx,
      mockItemManagementQueries(baseActiveItem),
    );

    expectErr(result as never, EmptyIssueLinesError);
    expect(spies.update).not.toHaveBeenCalled();
  });

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

    spies.select
      .mockReturnValueOnce(baseGoodsIssueExecution)
      .mockReturnValueOnce([baseGoodsIssueExecutionLine])
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce([{ id: baseStorageLocation.id }])
      .mockReturnValueOnce([baseStockLevel])
      .mockReturnValueOnce([])
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(ledger);
    spies.update
      .mockReturnValueOnce({ ...baseStockLevel, quantity: "80" })
      .mockReturnValueOnce(postedExecution);

    const result = await run(
      db,
      { 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(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        inventoryExecutionId: baseGoodsIssueExecution.id,
        quantity: "5",
      }),
    );
  });

  it("consumes matching stock reservations when posting", async () => {
    const { db, spies } = createMockDb<Transaction>();
    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"),
    };

    spies.select
      .mockReturnValueOnce(baseGoodsIssueExecution)
      .mockReturnValueOnce([baseGoodsIssueExecutionLine])
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce([{ id: baseStorageLocation.id }])
      .mockReturnValueOnce([baseStockLevel])
      .mockReturnValueOnce([matchingReservation])
      .mockReturnValueOnce(matchingReservation)
      .mockReturnValueOnce(undefined)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(ledger);
    spies.update
      .mockReturnValueOnce({ ...matchingReservation, consumedQuantity: "7" })
      .mockReturnValueOnce({ ...baseStockLevel, quantity: "80" })
      .mockReturnValueOnce(postedExecution);

    const result = await run(
      db,
      { id: baseGoodsIssueExecution.id },
      ctx,
      mockItemManagementQueries(baseActiveItem),
    );

    expectOk(result as never);
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({
        consumedQuantity: "7",
        status: "OPEN",
      }),
    );
  });

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

    spies.select
      .mockReturnValueOnce(baseGoodsIssueExecution)
      .mockReturnValueOnce([{ ...baseGoodsIssueExecutionLine, sourceLineId: null }])
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce([{ id: baseStorageLocation.id }])
      .mockReturnValueOnce([constrainedStockLevel])
      .mockReturnValueOnce([otherReservation]);

    const result = await run(
      db,
      { id: baseGoodsIssueExecution.id },
      ctx,
      mockItemManagementQueries(baseActiveItem),
    );

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