import { describe, expect, it } from 'vitest';
import { CheckoutService } from '../application/checkout-service.js';
import { CheckoutError } from '../domain/checkout.js';
import type { CheckoutOrderLookup, PaymentProvider } from '../domain/ports.js';
import { InMemoryCheckoutMetrics } from '../infrastructure/metrics.js';
import { StubPaymentProvider } from '../infrastructure/payment-provider.js';
import type { InventoryServicePort } from '../../inventory/index.js';
import type {
  CreateOrderCommand,
  OrderItemInput,
  OrderServicePort,
  OrderState,
  OrderView,
} from '../../orders/index.js';
import type { PricingServicePort } from '../../pricing/index.js';
import type { ShippingServicePort } from '../../shipping/index.js';

const PRICE = {
  variantId: 'v-1',
  quantity: 1,
  currency: 'EUR' as const,
  vatRate: 'general' as const,
  vatBasisPoints: 2100,
  netUnitAmountCents: 1000,
  netSubtotalCents: 1000,
  vatAmountCents: 210,
  totalCents: 1210,
};

interface Deps {
  service: CheckoutService;
  metrics: InMemoryCheckoutMetrics;
  reserved: { calls: Array<{ variantId: string; quantity: number }> };
  released: { calls: Array<{ variantId: string; quantity: number }> };
  cancelCalls: Array<{ id: string; state: OrderState }>;
}

function buildDeps(
  overrides: Partial<{
    stock: boolean;
    promo: boolean;
    shipping: boolean;
    orderId: string;
    idempotency?: { orderId: string };
  }> = {},
): Deps {
  const cart = {
    getCart: async () => ({
      userId: 'u-1',
      items: [{ productId: 'p-1', variantId: 'v-1', quantity: 1 }],
    }),
  };
  const pricing: PricingServicePort = {
    getVariantPrice: async () => undefined,
    setVariantPrice: async () => ({}) as never,
    seedVariantPrice: async () => undefined,
    calculate: async () => PRICE,
  };
  const reserved = { calls: [] as Array<{ variantId: string; quantity: number }> };
  const released = { calls: [] as Array<{ variantId: string; quantity: number }> };
  const cancelCalls: Array<{ id: string; state: OrderState }> = [];
  const inventory: InventoryServicePort = {
    checkAvailability: async () => ({ available: overrides.stock ?? true, availableQuantity: 10 }),
    reserve: async (input) => {
      reserved.calls.push(input);
      return { ...PRICE } as never;
    },
    release: async (input) => {
      released.calls.push(input);
      return { ...PRICE } as never;
    },
    confirm: async () => ({}) as never,
    setAvailable: async () => ({}) as never,
  };
  const shipping: ShippingServicePort = {
    calculate: async () =>
      overrides.shipping === false
        ? Promise.reject(
            Object.assign(new Error('not found'), { name: 'ShippingZoneNotFoundError' }),
          )
        : Promise.resolve({
            zoneId: 'z-1',
            methodId: 'm-1',
            methodName: 'Standard',
            costCents: 500,
            freeApplied: false,
          }),
  };
  const orderId = overrides.orderId ?? 'order-1';
  const orders: OrderServicePort = {
    create: async (input: CreateOrderCommand) => ({
      id: orderId,
      userId: input.userId,
      idempotencyKey: input.idempotencyKey ?? null,
      state: 'AWAITING_PAYMENT',
      currency: 'EUR',
      subtotalCents: input.totals.subtotalCents,
      discountCents: input.totals.discountCents,
      taxCents: input.totals.taxCents,
      totalCents: input.totals.totalCents,
      createdAt: new Date(),
      updatedAt: new Date(),
      items: input.items.map((item: OrderItemInput, index: number) => ({
        id: `i-${index}`,
        orderId,
        productId: item.productId,
        variantId: item.variantId,
        sku: item.sku,
        ean: null,
        name: item.name,
        unitPriceCents: item.unitPriceCents,
        discountCents: item.discountCents,
        taxCents: item.taxCents,
        quantity: item.quantity,
        createdAt: new Date(),
      })),
    }),
    listOrders: async () => [],
    transition: async (id, state) => {
      cancelCalls.push({ id, state });
      return {
        id,
        userId: 'u-1',
        state,
        currency: 'EUR',
        idempotencyKey: 'k-1',
        subtotalCents: 1210,
        discountCents: 0,
        taxCents: 210,
        totalCents: 1710,
        createdAt: new Date(),
        updatedAt: new Date(),
        items: [],
      } satisfies OrderView;
    },
    getOrder: async (id) => ({
      id,
      userId: 'u-1',
      state: 'AWAITING_PAYMENT',
      currency: 'EUR',
      idempotencyKey: 'k-1',
      subtotalCents: 1210,
      discountCents: 0,
      taxCents: 210,
      totalCents: 1710,
      createdAt: new Date(),
      updatedAt: new Date(),
      items: [],
    }),
    getOrderAdmin: async (id) => ({
      id,
      userId: 'u-1',
      state: 'AWAITING_PAYMENT',
      currency: 'EUR',
      idempotencyKey: 'k-1',
      subtotalCents: 1210,
      discountCents: 0,
      taxCents: 210,
      totalCents: 1710,
      createdAt: new Date(),
      updatedAt: new Date(),
      items: [],
    }),
    transitionAdmin: async (id, state) => ({
      id,
      userId: 'u-1',
      state,
      currency: 'EUR',
      idempotencyKey: 'k-1',
      subtotalCents: 1210,
      discountCents: 0,
      taxCents: 210,
      totalCents: 1710,
      createdAt: new Date(),
      updatedAt: new Date(),
      items: [],
    }),
  };
  const orderLookup: CheckoutOrderLookup = { findByIdempotency: async () => overrides.idempotency };
  const payments: PaymentProvider = new StubPaymentProvider();
  const metrics = new InMemoryCheckoutMetrics();
  const service = new CheckoutService({
    cart: cart as never,
    pricing,
    inventory,
    shipping,
    orders,
    payments,
    orderLookup,
    metrics,
  });
  return { service, metrics, reserved, released, cancelCalls };
}

describe('CheckoutService', () => {
  it('returns 409 when stock is unavailable (AC1)', async () => {
    const { service } = buildDeps({ stock: false });
    await expect(
      service.execute({
        userId: 'u-1',
        address: { country: 'ES', postalCode: '28001' },
        idempotencyKey: 'k-1',
      }),
    ).rejects.toBeInstanceOf(CheckoutError);
  });

  it('creates an AWAITING_PAYMENT order with reserved stock on success', async () => {
    const deps = buildDeps();
    const result = await deps.service.execute({
      userId: 'u-1',
      address: { country: 'ES', postalCode: '28001' },
      idempotencyKey: 'k-1',
    });
    expect(result.order.state).toBe('AWAITING_PAYMENT');
    expect(deps.reserved.calls).toEqual([
      {
        variantId: 'v-1',
        storeId: '00000000-0000-0000-0000-000000000001',
        quantity: 1,
      },
    ]);
    expect(deps.metrics.success).toBe(1);
  });

  it('returns the same order on idempotent retry without reserving twice (AC2)', async () => {
    const dep2 = buildDeps();
    const lookup: CheckoutOrderLookup = { findByIdempotency: async () => ({ orderId: 'order-x' }) };
    const service2 = new CheckoutService({
      cart: {
        getCart: async () => ({
          userId: 'u-1',
          items: [{ productId: 'p-1', variantId: 'v-1', quantity: 1 }],
        }),
      } as never,
      pricing: {
        getVariantPrice: async () => undefined,
        setVariantPrice: async () => ({}) as never,
        seedVariantPrice: async () => undefined,
        calculate: async () => PRICE,
      },
      inventory: {
        checkAvailability: async () => ({ available: true, availableQuantity: 10 }),
        reserve: async (i) => {
          dep2.reserved.calls.push(i);
          return {} as never;
        },
        release: async () => ({}) as never,
        confirm: async () => ({}) as never,
        setAvailable: async () => ({}) as never,
      },
      shipping: {
        calculate: async () => ({
          zoneId: 'z',
          methodId: 'm',
          methodName: 'Standard',
          costCents: 0,
          freeApplied: true,
        }),
      },
      orders: {
        create: async () => {
          throw new Error('should not create on retry');
        },
        transition: async () => {
          throw new Error('should not transition on retry');
        },
        listOrders: async () => [],
        getOrderAdmin: async () => {
          throw new Error('not used');
        },
        transitionAdmin: async () => {
          throw new Error('not used');
        },
        getOrder: async (id) => ({
          id,
          userId: 'u-1',
          state: 'AWAITING_PAYMENT',
          currency: 'EUR',
          idempotencyKey: 'k-1',
          subtotalCents: 1210,
          discountCents: 0,
          taxCents: 210,
          totalCents: 1710,
          createdAt: new Date(),
          updatedAt: new Date(),
          items: [
            {
              id: 'i-1',
              orderId: id,
              productId: 'p-1',
              variantId: 'v-1',
              sku: 'v-1',
              ean: null,
              name: 'x',
              unitPriceCents: 1000,
              discountCents: 0,
              taxCents: 210,
              quantity: 1,
              createdAt: new Date(),
            },
          ],
        }),
      } as OrderServicePort,
      payments: new StubPaymentProvider(),
      orderLookup: lookup,
      metrics: dep2.metrics,
    });
    const second = await service2.execute({
      userId: 'u-1',
      address: { country: 'ES', postalCode: '28001' },
      idempotencyKey: 'k-1',
    });
    expect(second.order.id).toBe('order-x');
    expect(dep2.reserved.calls).toHaveLength(0);
  });
});
