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,
  InvalidUnitConversionRateError,
  InvalidQuantityError,
  ItemNotFoundError,
  PrimaryUnitMismatchError,
} from "../lib/errors.generated";
import { expectErr, expectOk, mockItemManagementQueries } from "../testing/commandTestUtils";
import {
  baseActiveItem,
  baseGoodsIssueExecution,
  baseGoodsIssueExecutionLine,
  baseInactiveItem,
  baseInactiveLocation,
  baseStorageLocation,
} from "../testing/fixtures";
import { run } from "./createGoodsIssue";

describe("createGoodsIssue", () => {
  const ctx: CommandContext = {
    actorId: "test-actor",
    permissions: ["inventory:inventoryOperation:createGoodsIssue"],
  };
  const header = {
    effectiveDate: new Date("2024-01-10T00:00:00.000Z"),
    sourceDocumentType: "SALES_ORDER" as const,
    sourceDocumentId: "so-1",
  };
  const issueLine = {
    itemId: "item-1",
    storageLocationId: "location-1",
    quantity: "5",
    unitId: "unit-box",
    primaryQuantity: "10",
    primaryUnitId: baseActiveItem.unitId,
    unitConversionRate: "2",
  };

  it("returns error when no lines are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(trx, { header, lines: [] }, ctx, mockItemManagementQueries(baseActiveItem)),
    );

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

  it("returns error when line quantity is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, quantity: "0" }],
        },
        ctx,
        mockItemManagementQueries(baseActiveItem),
      ),
    );

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

  it("returns error when line primary quantity is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, primaryQuantity: "0" }],
        },
        ctx,
        mockItemManagementQueries(baseActiveItem),
      ),
    );

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

  it("returns error when line unit conversion rate is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, unitConversionRate: "0" }],
        },
        ctx,
        mockItemManagementQueries(baseActiveItem),
      ),
    );

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

  it("returns error when item does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, itemId: "missing" }],
        },
        ctx,
        mockItemManagementQueries(null),
      ),
    );

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

  it("returns error when primary unit differs from item primary unit", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, primaryUnitId: "unit-kg" }],
        },
        ctx,
        mockItemManagementQueries(baseActiveItem),
      ),
    );

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

  it("creates a DRAFT goods issue execution with outbound lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseStorageLocation],
      [baseGoodsIssueExecution],
      [baseGoodsIssueExecutionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [{ ...issueLine, sourceLineId: "so-line-1" }],
        },
        ctx,
        mockItemManagementQueries(baseActiveItem),
      ),
    );

    const value = expectOk<{ inventoryExecution: { status: string; executionType: string } }>(
      result as never,
    );

    expect(value.inventoryExecution).toMatchObject({
      executionType: "GOODS_ISSUE",
      status: "DRAFT",
    });
    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          direction: "OUT",
          quantity: "5",
          unitId: "unit-box",
          primaryQuantity: "10",
          primaryUnitId: issueLine.primaryUnitId,
          unitConversionRate: "2",
          unitCost: null,
          stockType: "AVAILABLE",
        }),
      ]),
    );
  });

  it("accepts inactive item and location references", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseInactiveLocation],
      [baseGoodsIssueExecution],
      [baseGoodsIssueExecutionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header,
          lines: [
            {
              ...issueLine,
              itemId: "item-3",
              storageLocationId: "location-3",
            },
          ],
        },
        ctx,
        mockItemManagementQueries(baseInactiveItem),
      ),
    );

    const value = expectOk<{ inventoryExecution: { status: string } }>(result as never);

    expect(value.inventoryExecution.status).toBe("DRAFT");
  });
});
