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,
  baseItemValuation,
  baseFifoPolicy,
} from "../testing/fixtures";
import { run } from "./receiveStock";

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

  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); // Location lookup

    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); // Location exists but inactive

    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); // Location lookup

    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); // Location lookup

    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" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SerialNotFoundError);
    }
  });

  // --- Success scenarios ---

  it("receives stock and creates 1 ledger entry with GOODS_RECEIPT/DR", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_RECEIPT",
      dcIndicator: "DR",
      amount: 50,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup (no valuation)
    spies.insert.mockReturnValue(ledgerEntry);
    spies.update.mockReturnValue({ ...baseStockLevel, onHand: 110 });

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

  it("updates onHand and decreases incoming", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedStockLevel = {
      ...baseStockLevel,
      onHand: 110,
      incoming: 10,
    };
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_RECEIPT",
      dcIndicator: "DR",
      amount: 50,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert.mockReturnValue(ledgerEntry);
    spies.update.mockReturnValue(updatedStockLevel);

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // Verify the set call was made (update StockLevel)
    expect(spies.set).toHaveBeenCalled();
  });

  // --- Valuation ---

  it("creates new cost layer for FIFO valuation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_RECEIPT",
      dcIndicator: "DR",
      amount: 50,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(baseItemValuation) // ItemValuation lookup
      .mockReturnValueOnce(baseFifoPolicy); // ValuationPolicy lookup
    spies.insert
      .mockReturnValueOnce(ledgerEntry) // Ledger entry
      .mockReturnValueOnce({ id: "cost-layer-new" }); // CostLayer insert
    spies.update
      .mockReturnValueOnce({ ...baseStockLevel, onHand: 110 }) // StockLevel update
      .mockReturnValueOnce({ ...baseItemValuation, totalQuantity: 110 }); // ItemValuation update

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // Verify insert was called for both ledger entry and cost layer
    expect(spies.insert).toHaveBeenCalledTimes(2);
  });

  // --- Lot tracking ---

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

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseActiveLot) // Lot lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined) // ItemValuation lookup (no valuation)
      .mockReturnValueOnce(lotStockLevel); // LotStockLevel lookup
    spies.insert.mockReturnValue(ledgerEntry);
    spies.update.mockReturnValue({ ...lotStockLevel, onHand: 60 });

    const result = await run(
      db,
      { ...baseInput, lotId: "lot-1" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(true);
    // Verify update was called for both StockLevel and LotStockLevel
    expect(spies.update).toHaveBeenCalled();
  });

  // --- Serial number transitions ---

  it("transitions serial number from ISSUED to RETURNED (customer return)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_RECEIPT",
      dcIndicator: "DR",
      amount: 5,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseIssuedSerial) // Serial lookup
      .mockReturnValueOnce(baseStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert.mockReturnValue(ledgerEntry);
    spies.update.mockReturnValue({ ...baseIssuedSerial, status: "RETURNED" });

    const result = await run(
      db,
      { ...baseInput, quantity: "1", unitCost: "5", serialNumberId: "serial-2" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(true);
    // Verify serial number update happened
    expect(spies.update).toHaveBeenCalled();
  });

  // --- Stock level creation for new item-location pair ---

  it("creates new StockLevel when none exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_RECEIPT",
      dcIndicator: "DR",
      amount: 50,
    };
    const newStockLevel = {
      id: "stock-level-new",
      itemId: "item-1",
      storageLocationId: "location-1",
      onHand: 10,
      reserved: 0,
      incoming: 0,
      blocked: 0,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(undefined) // StockLevel lookup — not found
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert
      .mockReturnValueOnce(ledgerEntry) // Ledger entry
      .mockReturnValueOnce(newStockLevel); // StockLevel insert

    const result = await run(db, baseInput, ctx, mockItemQueries(activeItem));
    expect(result.ok).toBe(true);
    // Insert should be called: 1 ledger entry + 1 StockLevel
    expect(spies.insert).toHaveBeenCalledTimes(2);
  });
});
