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 {
  ItemNotFoundError,
  ItemNotActiveError,
  StorageLocationNotFoundError,
  StorageLocationNotActiveError,
  InvalidQuantityError,
  InvalidUnitCostError,
  LotNotFoundError,
  SerialNotFoundError,
} from "../lib/errors.generated";
import {
  baseStorageLocation,
  baseInactiveLocation,
  baseStockLevel,
  baseActiveLot,
  baseIssuedSerial,
} from "../testing/fixtures";
import { run } from "./receiveStockWithInspection";

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

  const mockItemQueries = (item: { id: string; status: string } | null) => ({
    getItem: async () => await Promise.resolve({ ok: true as const, value: { item } }),
  });

  const activeItem = { id: "item-1", status: "ACTIVE" };
  const inactiveItem = { id: "item-1", status: "INACTIVE" };

  const baseInput = {
    itemId: "item-1",
    storageLocationId: "location-1",
    quantity: "10",
    unitCost: "5",
    sourceId: "source-1",
  };

  // --- Error scenarios ---

  it("returns error when item does not exist", async () => {
    const { db } = createMockDb<Transaction>();
    const result = await run(db, baseInput, ctx, mockItemQueries(null));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ItemNotFoundError);
    }
  });

  it("returns error when item is not active", async () => {
    const { db } = createMockDb<Transaction>();
    const result = await run(db, baseInput, ctx, mockItemQueries(inactiveItem));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ItemNotActiveError);
    }
  });

  it("returns error when storage location does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StorageLocationNotFoundError);
    }
  });

  it("returns error when storage location is not active", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseInactiveLocation);

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StorageLocationNotActiveError);
    }
  });

  it("returns error when quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseStorageLocation);

    const result = await run(db, { ...baseInput, quantity: "0" }, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidQuantityError);
    }
  });

  it("returns error when unit cost is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseStorageLocation);

    const result = await run(db, { ...baseInput, unitCost: "0" }, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidUnitCostError);
    }
  });

  it("returns error when lot does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(undefined); // Lot lookup

    const result = await run(
      db,
      { ...baseInput, lotId: "nonexistent-lot" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(LotNotFoundError);
    }
  });

  it("returns error when serial number does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(undefined); // Serial lookup

    const result = await run(
      db,
      { ...baseInput, serialNumberId: "nonexistent-serial", quantity: "1" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SerialNotFoundError);
    }
  });

  // --- Success scenarios ---

  it("receives stock and creates 3 ledger entries (GOODS_RECEIPT/DR + BLOCK/CR + BLOCK/DR)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntries = [
      { id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR", amount: "50" },
      { id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR", amount: null },
      { id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR", amount: null },
    ];

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert
      .mockReturnValueOnce(ledgerEntries[0])
      .mockReturnValueOnce(ledgerEntries[1])
      .mockReturnValueOnce(ledgerEntries[2]);
    spies.update.mockReturnValue({ ...baseStockLevel, onHand: "110", blocked: "15" });

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.inventoryLedgerEntries).toHaveLength(3);
      expect(result.value.inventoryLedgerEntries[0].sourceType).toBe("GOODS_RECEIPT");
      expect(result.value.inventoryLedgerEntries[0].dcIndicator).toBe("DR");
      expect(result.value.inventoryLedgerEntries[1].sourceType).toBe("BLOCK");
      expect(result.value.inventoryLedgerEntries[1].dcIndicator).toBe("CR");
      expect(result.value.inventoryLedgerEntries[2].sourceType).toBe("BLOCK");
      expect(result.value.inventoryLedgerEntries[2].dcIndicator).toBe("DR");
    }
  });

  it("updates onHand, blocked, and decreases incoming", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedStockLevel = {
      ...baseStockLevel,
      onHand: "110",
      blocked: "15",
      incoming: "10",
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce(baseStockLevel)
      .mockReturnValueOnce(undefined);
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" });
    spies.update.mockReturnValue(updatedStockLevel);

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    expect(spies.set).toHaveBeenCalled();
  });

  it("available stock does not increase from this operation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // baseStockLevel: onHand=100, reserved=10, blocked=5 → available=85
    // After receiveStockWithInspection(10): onHand=110, blocked=15 → available=85 (unchanged)
    spies.select
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce(baseStockLevel)
      .mockReturnValueOnce(undefined);
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" });

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // Both onHand and blocked increase by the same quantity (10), so available is unchanged
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({
        onHand: "110",
        blocked: "15",
      }),
    );
  });

  it("creates new cost layer for FIFO valuation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce(baseStockLevel)
      .mockReturnValueOnce({
        id: "iv-1",
        itemId: "item-1",
        valuationPolicyId: "policy-1",
        totalQuantity: "0",
        totalValue: "0",
        costPerUnit: "0",
      }) // ItemValuation
      .mockReturnValueOnce({ id: "policy-1", costingMethod: "FIFO", standardCostRate: null }); // ValuationPolicy
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" });
    spies.update.mockReturnValue(baseStockLevel);

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // 3 ledger + 1 cost layer = 4 inserts
    expect(spies.insert.mock.calls.length).toBeGreaterThanOrEqual(4);
  });

  it("updates LotStockLevel onHand and blocked for lot-tracked items", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const lotStockLevel = {
      id: "lot-stock-1",
      lotId: "lot-1",
      storageLocationId: "location-1",
      onHand: "50",
      blocked: "0",
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location
      .mockReturnValueOnce(baseActiveLot) // Lot
      .mockReturnValueOnce(baseStockLevel) // StockLevel
      .mockReturnValueOnce(undefined) // ItemValuation (no valuation)
      .mockReturnValueOnce(lotStockLevel); // LotStockLevel
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" });
    spies.update.mockReturnValue({ ...lotStockLevel, onHand: "60", blocked: "10" });

    const result = await run(
      db,
      { ...baseInput, lotId: "lot-1" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(true);
    // Both StockLevel and LotStockLevel should have been updated
    expect(spies.update).toHaveBeenCalled();
  });

  it("transitions serial number from ISSUED to RETURNED (customer return)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseStorageLocation)
      .mockReturnValueOnce(baseIssuedSerial) // Serial lookup (ISSUED state)
      .mockReturnValueOnce(baseStockLevel)
      .mockReturnValueOnce(undefined);
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" });
    spies.update.mockReturnValue({ ...baseIssuedSerial, status: "RETURNED" });

    const result = await run(
      db,
      { ...baseInput, serialNumberId: "serial-2", quantity: "1" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(true);
    // SerialNumber update was called (AVAILABLE/ISSUED/RETURNED state transition)
    expect(spies.update).toHaveBeenCalled();
  });

  it("creates new StockLevel when none exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location
      .mockReturnValueOnce(undefined) // StockLevel (not found)
      .mockReturnValueOnce(undefined); // ItemValuation
    spies.insert
      .mockReturnValueOnce({ id: "ledger-1", sourceType: "GOODS_RECEIPT", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "ledger-2", sourceType: "BLOCK", dcIndicator: "CR" })
      .mockReturnValueOnce({ id: "ledger-3", sourceType: "BLOCK", dcIndicator: "DR" })
      .mockReturnValueOnce({ id: "sl-new", onHand: "10", blocked: "10" }); // New StockLevel insert

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // StockLevel insert happened (because existing was undefined)
    expect(spies.insert.mock.calls.length).toBeGreaterThanOrEqual(4);
  });
});
