import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  StockMovementNotFoundError,
  StockMovementNotConfirmedError,
  InsufficientStockError,
  InsufficientBlockedStockError,
  LotStockInsufficientError,
  SerialInvalidStateError,
} from "../lib/errors.generated";
import {
  baseConfirmedMovement,
  baseDraftMovement,
  baseMoveLine,
  baseStockLevel,
  baseLotStockLevel,
  baseAvailableSerial,
  baseIssuedSerial,
  baseItemValuation,
} from "../testing/fixtures";
import { run } from "./executeStockMovement";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

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

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

    const result = await run(db, { id: "nonexistent" }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StockMovementNotFoundError);
    }
  });

  it("returns error when not CONFIRMED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseDraftMovement);

    const result = await run(db, { id: baseDraftMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StockMovementNotConfirmedError);
    }
  });

  it("returns error when insufficient available stock for goods issue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issueMovement = { ...baseConfirmedMovement, movementType: "GOODS_ISSUE" as const };
    const issueLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 200,
    };
    const lowStock = { ...baseStockLevel, onHand: 10, reserved: 0, blocked: 0 };

    spies.select
      .mockReturnValueOnce(issueMovement) // movement lookup
      .mockReturnValueOnce([issueLine]) // move lines query (.execute)
      .mockReturnValueOnce(lowStock); // stock level check

    const result = await run(db, { id: issueMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientStockError);
    }
  });

  it("returns error when insufficient stock for block", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const blockMovement = {
      ...baseConfirmedMovement,
      movementType: "BLOCK" as const,
      reasonCode: "quarantine",
    };
    const blockLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 200,
    };
    const lowStock = { ...baseStockLevel, onHand: 10, reserved: 0, blocked: 0 };

    spies.select
      .mockReturnValueOnce(blockMovement)
      .mockReturnValueOnce([blockLine])
      .mockReturnValueOnce(lowStock);

    const result = await run(db, { id: blockMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientStockError);
    }
  });

  it("returns error when insufficient blocked stock for unblock", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unblockMovement = {
      ...baseConfirmedMovement,
      movementType: "UNBLOCK" as const,
      reasonCode: "release",
    };
    const unblockLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 50,
    };
    const lowBlockedStock = { ...baseStockLevel, blocked: 2 };

    spies.select
      .mockReturnValueOnce(unblockMovement)
      .mockReturnValueOnce([unblockLine])
      .mockReturnValueOnce(lowBlockedStock);

    const result = await run(db, { id: unblockMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientBlockedStockError);
    }
  });

  it("returns error when lot stock insufficient for goods issue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issueMovement = { ...baseConfirmedMovement, movementType: "GOODS_ISSUE" as const };
    const lotLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 10,
      lotId: "lot-1",
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, reserved: 0, blocked: 0 };
    const lowLotStock = { ...baseLotStockLevel, onHand: 5 }; // only 5, need 10

    spies.select
      .mockReturnValueOnce(issueMovement) // movement lookup
      .mockReturnValueOnce([lotLine]) // move lines
      .mockReturnValueOnce(existingStock) // available stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(lowLotStock); // lot stock level (insufficient)

    const result = await run(db, { id: issueMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(LotStockInsufficientError);
    }
  });

  it("executes goods receipt (increases on-hand, decreases incoming)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receiptMovement = { ...baseConfirmedMovement, movementType: "GOODS_RECEIPT" as const };
    const receiptLine = {
      ...baseMoveLine,
      quantity: 50,
      destinationLocationId: "location-1",
      sourceLocationId: null,
      lotId: null,
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, incoming: 50 };
    const doneMovement = { ...receiptMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(receiptMovement) // movement lookup
      .mockReturnValueOnce([receiptLine]) // move lines
      .mockReturnValueOnce(existingStock) // stock level lookup for upsert
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: receiptMovement.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.stockMovement.status).toBe("DONE");
    }
    expect(spies.update).toHaveBeenCalled();
  });

  it("executes goods issue (decreases on-hand)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issueMovement = { ...baseConfirmedMovement, movementType: "GOODS_ISSUE" as const };
    const issueLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 10,
      lotId: null,
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, reserved: 0, blocked: 0 };
    const doneMovement = { ...issueMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(issueMovement)
      .mockReturnValueOnce([issueLine])
      .mockReturnValueOnce(existingStock) // available stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: issueMovement.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.stockMovement.status).toBe("DONE");
    }
  });

  it("executes internal transfer (decreases source, increases destination)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const transferMovement = {
      ...baseConfirmedMovement,
      movementType: "INTERNAL_TRANSFER" as const,
    };
    const transferLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: "location-2",
      quantity: 10,
      lotId: null,
      serialNumberId: null,
    };
    const sourceStock = { ...baseStockLevel, onHand: 100 };
    const destStock = {
      ...baseStockLevel,
      id: "stock-level-dest",
      storageLocationId: "location-2",
      onHand: 20,
    };
    const doneMovement = { ...transferMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(transferMovement)
      .mockReturnValueOnce([transferLine])
      .mockReturnValueOnce(sourceStock) // source stock level for upsert
      .mockReturnValueOnce(destStock) // dest stock level for upsert
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: transferMovement.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.stockMovement.status).toBe("DONE");
    }
  });

  it("executes block (increases blocked)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const blockMovement = {
      ...baseConfirmedMovement,
      movementType: "BLOCK" as const,
      reasonCode: "quarantine",
    };
    const blockLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 10,
      lotId: null,
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, reserved: 0, blocked: 5 };
    const doneMovement = { ...blockMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(blockMovement)
      .mockReturnValueOnce([blockLine])
      .mockReturnValueOnce(existingStock) // available stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: blockMovement.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.stockMovement.status).toBe("DONE");
    }
  });

  it("executes unblock (decreases blocked)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unblockMovement = {
      ...baseConfirmedMovement,
      movementType: "UNBLOCK" as const,
      reasonCode: "release",
    };
    const unblockLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 5,
      lotId: null,
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, blocked: 10 };
    const doneMovement = { ...unblockMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(unblockMovement)
      .mockReturnValueOnce([unblockLine])
      .mockReturnValueOnce(existingStock) // blocked stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: unblockMovement.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.stockMovement.status).toBe("DONE");
    }
  });

  it("updates lot stock levels when lot specified", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receiptMovement = { ...baseConfirmedMovement, movementType: "GOODS_RECEIPT" as const };
    const lotLine = {
      ...baseMoveLine,
      destinationLocationId: "location-1",
      sourceLocationId: null,
      quantity: 20,
      lotId: "lot-1",
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, incoming: 30 };
    const existingLotStock = { ...baseLotStockLevel, onHand: 50 };
    const doneMovement = { ...receiptMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(receiptMovement)
      .mockReturnValueOnce([lotLine])
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(existingLotStock) // lot stock level lookup
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: receiptMovement.id }, ctx);

    expect(result.ok).toBe(true);
    // update called for: stock level, lot stock level, movement status
    expect(spies.update).toHaveBeenCalled();
  });

  it("transitions serial from AVAILABLE to ISSUED on goods issue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issueMovement = { ...baseConfirmedMovement, movementType: "GOODS_ISSUE" as const };
    const serialLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 1,
      lotId: null,
      serialNumberId: "serial-1",
    };
    const existingStock = { ...baseStockLevel, onHand: 100, reserved: 0, blocked: 0 };
    const doneMovement = { ...issueMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(issueMovement)
      .mockReturnValueOnce([serialLine])
      .mockReturnValueOnce(existingStock) // available stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(baseAvailableSerial) // serial lookup
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: issueMovement.id }, ctx);

    expect(result.ok).toBe(true);
    expect(spies.update).toHaveBeenCalled();
  });

  it("transitions serial from ISSUED to RETURNED on goods receipt (customer return)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receiptMovement = { ...baseConfirmedMovement, movementType: "GOODS_RECEIPT" as const };
    const serialLine = {
      ...baseMoveLine,
      destinationLocationId: "location-1",
      sourceLocationId: null,
      quantity: 1,
      lotId: null,
      serialNumberId: "serial-2",
    };
    const existingStock = { ...baseStockLevel, onHand: 100, incoming: 10 };
    const doneMovement = { ...receiptMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(receiptMovement)
      .mockReturnValueOnce([serialLine])
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(baseIssuedSerial) // serial lookup (ISSUED -> RETURNED)
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: receiptMovement.id }, ctx);

    expect(result.ok).toBe(true);
    expect(spies.update).toHaveBeenCalled();
  });

  it("transitions serial from RETURNED to AVAILABLE on goods receipt (restock after return)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receiptMovement = { ...baseConfirmedMovement, movementType: "GOODS_RECEIPT" as const };
    const returnedSerial = { ...baseIssuedSerial, id: "serial-3", status: "RETURNED" as const };
    const serialLine = {
      ...baseMoveLine,
      destinationLocationId: "location-1",
      sourceLocationId: null,
      quantity: 1,
      lotId: null,
      serialNumberId: "serial-3",
    };
    const existingStock = { ...baseStockLevel, onHand: 100, incoming: 10 };
    const doneMovement = { ...receiptMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(receiptMovement)
      .mockReturnValueOnce([serialLine])
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(returnedSerial) // serial lookup (RETURNED -> AVAILABLE)
      .mockReturnValueOnce(undefined); // no valuation
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: receiptMovement.id }, ctx);

    expect(result.ok).toBe(true);
    expect(spies.update).toHaveBeenCalled();
  });

  it("returns error when serial state is invalid for the movement type", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const issueMovement = { ...baseConfirmedMovement, movementType: "GOODS_ISSUE" as const };
    const serialLine = {
      ...baseMoveLine,
      sourceLocationId: "location-1",
      destinationLocationId: null,
      quantity: 1,
      lotId: null,
      serialNumberId: "serial-2",
    };
    const existingStock = { ...baseStockLevel, onHand: 100, reserved: 0, blocked: 0 };

    spies.select
      .mockReturnValueOnce(issueMovement)
      .mockReturnValueOnce([serialLine])
      .mockReturnValueOnce(existingStock) // available stock check
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(baseIssuedSerial); // serial is ISSUED, can't issue again

    const result = await run(db, { id: issueMovement.id }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SerialInvalidStateError);
    }
  });

  it("triggers valuation recalculation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receiptMovement = { ...baseConfirmedMovement, movementType: "GOODS_RECEIPT" as const };
    const receiptLine = {
      ...baseMoveLine,
      quantity: 10,
      destinationLocationId: "location-1",
      sourceLocationId: null,
      lotId: null,
      serialNumberId: null,
    };
    const existingStock = { ...baseStockLevel, onHand: 100, incoming: 20 };
    const doneMovement = { ...receiptMovement, status: "DONE", executedAt: new Date() };

    spies.select
      .mockReturnValueOnce(receiptMovement)
      .mockReturnValueOnce([receiptLine])
      .mockReturnValueOnce(existingStock) // stock level for upsert
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.update.mockReturnValue(doneMovement);

    const result = await run(db, { id: receiptMovement.id }, ctx);

    expect(result.ok).toBe(true);
    // update called for: stock level, valuation, movement status
    expect(spies.update).toHaveBeenCalled();
  });
});
