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,
  InsufficientStockError,
  InsufficientBlockedStockError,
  LotNotFoundError,
  SerialNotFoundError,
  SerialNotAvailableError,
} from "../lib/errors.generated";
import {
  baseStorageLocation,
  baseInactiveLocation,
  baseStockLevel,
  baseActiveLot,
  baseAvailableSerial,
  baseLotStockLevel,
} from "../testing/fixtures";
import { run } from "./issueStock";

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

  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",
    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);

    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 insufficient available stock (fromBlocked=false)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // baseStockLevel: onHand=100, reserved=10, blocked=5 => available=85
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel); // StockLevel lookup

    const result = await run(
      db,
      { ...baseInput, quantity: "90" }, // available is 85
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientStockError);
    }
  });

  it("returns error when insufficient blocked stock (fromBlocked=true)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // baseStockLevel: blocked=5
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(baseStockLevel); // StockLevel lookup

    const result = await run(
      db,
      { ...baseInput, quantity: "10", fromBlocked: true }, // blocked is 5
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientBlockedStockError);
    }
  });

  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);
    }
  });

  it("returns error when serial number is not in AVAILABLE state", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issuedSerial = { ...baseAvailableSerial, id: "serial-2", status: "ISSUED" };
    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(issuedSerial); // Serial lookup — ISSUED, not AVAILABLE

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

  // --- Success scenarios (fromBlocked=false) ---

  it("issues stock (fromBlocked=false) and creates 1 ledger entry GOODS_ISSUE/CR", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_ISSUE",
      dcIndicator: "CR",
      amount: null,
    };

    // baseStockLevel: onHand=100, reserved=10, blocked=5 => available=85
    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: 90 });

    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_ISSUE");
      expect(result.value.inventoryLedgerEntries[0].dcIndicator).toBe("CR");
    }
  });

  it("updates onHand when fromBlocked=false", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_ISSUE",
      dcIndicator: "CR",
      amount: null,
    };

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

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

  // --- Success scenarios (fromBlocked=true) ---

  it("issues stock (fromBlocked=true) and creates 2 ledger entries (GOODS_ISSUE/CR + UNBLOCK/CR)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const blockedStockLevel = {
      ...baseStockLevel,
      blocked: 20,
    };
    const ledgerEntries = [
      { id: "ledger-1", sourceType: "GOODS_ISSUE", dcIndicator: "CR", amount: null },
      { id: "ledger-2", sourceType: "UNBLOCK", dcIndicator: "CR", amount: null },
    ];

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(blockedStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert
      .mockReturnValueOnce(ledgerEntries[0]) // GOODS_ISSUE entry
      .mockReturnValueOnce(ledgerEntries[1]); // UNBLOCK entry
    spies.update.mockReturnValue({ ...blockedStockLevel, onHand: 90, blocked: 10 });

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

  it("updates onHand and blocked when fromBlocked=true", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const blockedStockLevel = { ...baseStockLevel, blocked: 20 };
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_ISSUE",
      dcIndicator: "CR",
      amount: null,
    };

    spies.select
      .mockReturnValueOnce(baseStorageLocation) // Location lookup
      .mockReturnValueOnce(blockedStockLevel) // StockLevel lookup
      .mockReturnValueOnce(undefined); // ItemValuation lookup
    spies.insert.mockReturnValue(ledgerEntry);
    spies.update.mockReturnValue({ ...blockedStockLevel, onHand: 90, blocked: 10 });

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

  // --- Lot tracking ---

  it("updates LotStockLevel onHand (and blocked if fromBlocked) for lot-tracked items", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_ISSUE",
      dcIndicator: "CR",
      amount: null,
    };

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

    const result = await run(
      db,
      { ...baseInput, lotId: "lot-1" },
      ctx,
      mockItemQueries(activeItem),
    );
    expect(result.ok).toBe(true);
    expect(spies.update).toHaveBeenCalled();
  });

  // --- Serial number transitions ---

  it("transitions serial number from AVAILABLE to ISSUED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const ledgerEntry = {
      id: "ledger-1",
      sourceType: "GOODS_ISSUE",
      dcIndicator: "CR",
      amount: null,
    };

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

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