import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it, vi } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  InvalidSupplyPlanQuantitiesError,
  InventorySupplyPlanNotFoundError,
  ItemNotFoundError,
  SiteNotFoundError,
  SupplyPlanNotOpenError,
  SupplyPlanUnitMismatchError,
} from "../lib/errors.generated";
import {
  expectErr,
  expectOk,
  mockItemManagementQueries,
  type ItemManagementQueryItem,
} from "../testing/commandTestUtils";
import { baseActiveItem, baseActiveSite, baseSupplyPlan } from "../testing/fixtures";
import { run, type UpdateInventorySupplyPlanInput } from "./updateInventorySupplyPlan";

const ok = <T>(value: T) => ({ ok: true as const, value });

type OrganizationQueries = Parameters<typeof run>[4];

function mockOrganizationQueries(site: unknown): OrganizationQueries {
  return {
    getSite: vi.fn().mockResolvedValue(ok({ site })),
  } as unknown as OrganizationQueries;
}

function mockItemQueries(item: ItemManagementQueryItem) {
  return mockItemManagementQueries(item);
}

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

  const input: UpdateInventorySupplyPlanInput = {
    id: "supply-plan-1",
    sourceId: "po-1",
    itemId: "item-1",
    siteId: "site-1",
    expectedQuantity: "120",
    unitId: "unit-ea",
    expectedDate: new Date("2026-06-01T00:00:00.000Z"),
  };

  it("updates an open supply plan and preserves received quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = { ...baseSupplyPlan, expectedQuantity: "100", receivedQuantity: "25" };
    const updated = {
      ...existing,
      expectedQuantity: "120",
      receivedQuantity: "25",
      status: "OPEN",
    };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{
      supplyPlan: { expectedQuantity: string; receivedQuantity: string; status: string };
    }>(result as never);

    expect(supplyPlan.expectedQuantity).toBe("120");
    expect(supplyPlan.receivedQuantity).toBe("25");
    expect(supplyPlan.status).toBe("OPEN");
  });

  it("partially updates expected quantity without rewriting omitted fields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = {
      ...baseSupplyPlan,
      expectedQuantity: "100",
      receivedQuantity: "25",
      lotId: "lot-1",
      serialNumberId: "serial-1",
    };
    const updated = { ...existing, expectedQuantity: "120", status: "OPEN" };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: "supply-plan-1",
          expectedQuantity: "120",
        },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{
      supplyPlan: { expectedQuantity: string; lotId: string; serialNumberId: string };
    }>(result as never);
    const updateSet = mock.updates[0].updateValues();

    expect(supplyPlan.expectedQuantity).toBe("120");
    expect(supplyPlan.lotId).toBe("lot-1");
    expect(supplyPlan.serialNumberId).toBe("serial-1");
    expect(updateSet).not.toHaveProperty("lotId");
    expect(updateSet).not.toHaveProperty("serialNumberId");
  });

  it("updates custom fields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = { ...baseSupplyPlan, expectedQuantity: "100", receivedQuantity: "25" };
    const updated = { ...existing, planningType: "firm", status: "OPEN" };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run<{ planningType?: string }>(
        trx,
        {
          id: "supply-plan-1",
          planningType: "firm",
        },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{ supplyPlan: { planningType: string } }>(result as never);
    const updateSet = mock.updates[0].updateValues();

    expect(supplyPlan.planningType).toBe("firm");
    expect(updateSet.planningType).toBe("firm");
  });

  it("updates source document id for a supply slice", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = { ...baseSupplyPlan, expectedQuantity: "100", receivedQuantity: "25" };
    const updated = { ...existing, sourceId: "po-2", status: "OPEN" };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: "supply-plan-1",
          sourceId: "po-2",
        },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const updateSet = mock.updates[0].updateValues();

    expectOk(result as never);
    expect(updateSet.sourceId).toBe("po-2");
    expect(updateSet).not.toHaveProperty("sourceType");
    expect(updateSet).not.toHaveProperty("sourceLineId");
  });

  it("allows expected quantity equal to received quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = { ...baseSupplyPlan, expectedQuantity: "100", receivedQuantity: "80" };
    const updated = { ...existing, expectedQuantity: "80", status: "OPEN" };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, expectedQuantity: "80" },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{ supplyPlan: { expectedQuantity: string; status: string } }>(
      result as never,
    );

    expect(supplyPlan.expectedQuantity).toBe("80");
    expect(supplyPlan.status).toBe("OPEN");
  });

  it("returns error when supply plan not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );

    expectErr(result, InventorySupplyPlanNotFoundError);
  });

  it("returns error when supply plan is closed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([{ ...baseSupplyPlan, status: "CLOSED" }]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );

    expectErr(result, SupplyPlanNotOpenError);
  });

  it("allows expected quantity below received quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const existing = { ...baseSupplyPlan, expectedQuantity: "100", receivedQuantity: "80" };
    const updated = { ...existing, expectedQuantity: "79", status: "OPEN" };
    mock.enqueueResults([existing], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, expectedQuantity: "79" },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{ supplyPlan: { expectedQuantity: string; status: string } }>(
      result as never,
    );

    expect(supplyPlan.expectedQuantity).toBe("79");
    expect(supplyPlan.status).toBe("OPEN");
  });

  it("returns error when item not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseSupplyPlan]);

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, mockItemQueries(null), mockOrganizationQueries(baseActiveSite)),
    );

    expectErr(result, ItemNotFoundError);
  });

  it("returns error when quantity unit does not match the item unit", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseSupplyPlan]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, unitId: "unit-box" },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );

    expectErr(result, SupplyPlanUnitMismatchError);
  });

  it("returns error when site not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseSupplyPlan]);

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, mockItemQueries(baseActiveItem), mockOrganizationQueries(null)),
    );

    expectErr(result, SiteNotFoundError);
  });

  it("allows expected quantity to be zero", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updated = { ...baseSupplyPlan, expectedQuantity: "0", status: "OPEN" };
    mock.enqueueResults([baseSupplyPlan], [updated]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, expectedQuantity: "0" },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );
    const { supplyPlan } = expectOk<{ supplyPlan: { expectedQuantity: string; status: string } }>(
      result as never,
    );

    expect(supplyPlan.expectedQuantity).toBe("0");
    expect(supplyPlan.status).toBe("OPEN");
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, expectedQuantity: "-1" },
        ctx,
        mockItemQueries(baseActiveItem),
        mockOrganizationQueries(baseActiveSite),
      ),
    );

    expectErr(result, InvalidSupplyPlanQuantitiesError);
  });
});
