import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AdjustmentNotFoundError,
  AdjustmentInvalidStatusError,
  InsufficientOnHandError,
} from "../lib/errors.generated";
import {
  baseDraftAdjustment,
  baseApprovedAdjustment,
  baseConfirmedAdjustment,
  baseStockLevel,
  baseItemValuation,
  baseLotStockLevel,
} from "../testing/fixtures";
import { run } from "./confirmInventoryAdjustment";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

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

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

  it("returns error when already confirmed", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseConfirmedAdjustment);

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

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

  it("returns error when negative adjustment exceeds on-hand", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bigNegativeAdjustment = { ...baseDraftAdjustment, quantityDelta: -200 };
    spies.select
      .mockReturnValueOnce(bigNegativeAdjustment) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel); // stock level lookup (onHand = 100)

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

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

  it("confirms CORRECTION from DRAFT (no approval)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const confirmed = { ...baseDraftAdjustment, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(baseDraftAdjustment) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "ADJUSTMENT",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.inventoryAdjustment.status).toBe("CONFIRMED");
    }
  });

  it("confirms CORRECTION from APPROVED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const confirmed = { ...baseApprovedAdjustment, status: "CONFIRMED" };
    // approvedAdjustment has quantityDelta: 5 (positive), so no stock check
    spies.select
      .mockReturnValueOnce(baseApprovedAdjustment) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "ADJUSTMENT",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.inventoryAdjustment.status).toBe("CONFIRMED");
    }
  });

  it("confirms SCRAP from DRAFT", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const scrapDraft = {
      ...baseDraftAdjustment,
      adjustmentType: "SCRAP" as const,
      quantityDelta: -3,
      reasonCode: "expired",
    };
    const confirmed = { ...scrapDraft, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(scrapDraft) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "GOODS_ISSUE",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.inventoryAdjustment.status).toBe("CONFIRMED");
    }
  });

  it("confirms SCRAP from APPROVED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const scrapApproved = {
      ...baseApprovedAdjustment,
      adjustmentType: "SCRAP" as const,
      quantityDelta: -3,
      reasonCode: "expired",
      status: "APPROVED" as const,
    };
    const confirmed = { ...scrapApproved, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(scrapApproved) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "GOODS_ISSUE",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.inventoryAdjustment.status).toBe("CONFIRMED");
    }
  });

  it("CORRECTION generates stock movement to virtual adjustment location", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const confirmed = { ...baseDraftAdjustment, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(baseDraftAdjustment) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "ADJUSTMENT",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    // First insert is stock movement, second is stock move line
    expect(spies.insert).toHaveBeenCalled();
    expect(spies.values).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ movementType: "GOODS_RECEIPT" }),
    );
  });

  it("SCRAP generates stock movement to virtual scrap location", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const scrapDraft = {
      ...baseDraftAdjustment,
      adjustmentType: "SCRAP" as const,
      quantityDelta: -3,
      reasonCode: "expired",
    };
    const confirmed = { ...scrapDraft, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(scrapDraft) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "GOODS_ISSUE",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    expect(spies.values).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ movementType: "GOODS_ISSUE" }),
    );
  });

  it("SCRAP updates lot stock when applicable", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const scrapWithLot = {
      ...baseDraftAdjustment,
      adjustmentType: "SCRAP" as const,
      quantityDelta: -3,
      reasonCode: "expired",
      lotId: "lot-1",
    };
    const confirmed = { ...scrapWithLot, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(scrapWithLot) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseLotStockLevel) // lot stock level lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "GOODS_ISSUE",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    // update calls: lot stock level, stock level, valuation, adjustment status
    expect(spies.update).toHaveBeenCalled();
  });

  it("SCRAP transitions serial to SCRAPPED when applicable", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const scrapWithSerial = {
      ...baseDraftAdjustment,
      adjustmentType: "SCRAP" as const,
      quantityDelta: -1,
      reasonCode: "defective",
      serialNumberId: "serial-1",
    };
    const confirmed = { ...scrapWithSerial, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(scrapWithSerial) // adjustment lookup
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "GOODS_ISSUE",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    // One of the update set calls should contain status: "SCRAPPED"
    expect(spies.set).toHaveBeenCalledWith(expect.objectContaining({ status: "SCRAPPED" }));
  });

  it("updates on-hand quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const confirmed = { ...baseDraftAdjustment, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(baseDraftAdjustment) // adjustment lookup (quantityDelta: -5)
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check (onHand: 100)
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "ADJUSTMENT",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    // Stock level update should set onHand to 100 + (-5) = 95
    expect(spies.set).toHaveBeenCalledWith(expect.objectContaining({ onHand: 95 }));
  });

  it("updates inventory valuation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const confirmed = { ...baseDraftAdjustment, status: "CONFIRMED" };
    spies.select
      .mockReturnValueOnce(baseDraftAdjustment) // adjustment lookup (quantityDelta: -5)
      .mockReturnValueOnce(baseStockLevel) // stock level for insufficient check
      .mockReturnValueOnce(baseStockLevel) // stock level for update
      .mockReturnValueOnce(baseItemValuation); // valuation lookup (totalQuantity: 100, costPerUnit: 10)
    spies.insert.mockReturnValue({
      id: "movement-new",
      movementType: "ADJUSTMENT",
      status: "DONE",
    });
    spies.update.mockReturnValue(confirmed);

    await run(db, { id: "adjustment-1" }, ctx);

    // Valuation update should set totalQuantity to 100 + (-5) = 95, totalValue to 95 * 10 = 950
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ totalQuantity: 95, totalValue: 950 }),
    );
  });
});
