import { act, renderHook, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import type { Cart, CartLine, PriceSnapshot } from '@platform-modules/commerce-cart'
import type { CartClient } from './client.js'
import { CartProvider } from './CartProvider.js'
import { useCart } from './useCart.js'
import { useCartActions } from './useCartActions.js'
import { isCartValidationError, isMixedCurrencyError } from './errors.js'

const PRICE: PriceSnapshot = { amount: 100n, currency: 'USD', priceMode: 'exclusive' }
const ln = (over: Partial<CartLine> = {}): CartLine => ({ lineId: 'l1', variantId: 'v1', qty: 1, price: PRICE, vendorId: null, ...over })
const aCart = (over: Partial<Cart> = {}): Cart => ({ id: 'c1', currency: 'USD', lines: [], subtotal: 0n, ...over })

// Render both hooks under one provider so we can drive actions and observe cart state.
function setup(client: CartClient, initialCart?: Cart) {
  return renderHook(() => ({ cart: useCart(), act: useCartActions() }), {
    wrapper: ({ children }) => <CartProvider client={client} initialCart={initialCart}>{children}</CartProvider>,
  })
}

describe('useCartActions', () => {
  it('setQty is optimistic then reconciles to the server cart', async () => {
    const seeded = aCart({ lines: [ln({ qty: 1 })], subtotal: 100n })
    const server = aCart({ lines: [ln({ qty: 5 })], subtotal: 500n })
    let resolveApply: (c: Cart) => void = () => {}
    const client = {
      getCart: vi.fn(async () => seeded),
      apply: vi.fn(() => new Promise<Cart>((r) => { resolveApply = r })),
      merge: vi.fn(),
    }
    const { result } = setup(client, seeded)
    act(() => { void result.current.act.setQty('l1', 5) })
    // optimistic: UI shows qty 5 before the server resolves
    await waitFor(() => expect(result.current.cart.itemCount).toBe(5))
    act(() => resolveApply(server))
    await waitFor(() => expect(result.current.cart.cart?.subtotal).toBe(500n))
  })

  it('rollback on client reject (state returns to prev, error set)', async () => {
    const seeded = aCart({ lines: [ln({ qty: 1 })], subtotal: 100n })
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(async () => { throw new Error('reject') }), merge: vi.fn() }
    const { result } = setup(client, seeded)
    await act(async () => { await result.current.act.setQty('l1', 9).catch(() => {}) })
    expect(result.current.cart.itemCount).toBe(1) // rolled back
    expect(result.current.act.error?.message).toBe('reject')
  })

  it('cartReduce-throws path: setQty 0 → CartValidationError into error, no client call', async () => {
    const seeded = aCart({ lines: [ln({ qty: 1 })], subtotal: 100n })
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(async () => seeded), merge: vi.fn() }
    const { result } = setup(client, seeded)
    await act(async () => { await result.current.act.setQty('l1', 0).catch(() => {}) })
    expect(isCartValidationError(result.current.act.error)).toBe(true)
    expect(client.apply).not.toHaveBeenCalled()
  })

  it('addLine is PESSIMISTIC — no optimistic line before the client resolves; server line carries server lineId', async () => {
    const seeded = aCart()
    const server = aCart({ lines: [ln({ lineId: 'server-line', variantId: 'v1', qty: 1 })], subtotal: 100n })
    let resolveApply: (c: Cart) => void = () => {}
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(() => new Promise<Cart>((r) => { resolveApply = r })), merge: vi.fn() }
    const { result } = setup(client, seeded)
    act(() => { void result.current.act.addLine({ variantId: 'v1', qty: 1, price: PRICE }) })
    // pessimistic: NO optimistic line yet
    await new Promise((r) => setTimeout(r, 0))
    expect(result.current.cart.itemCount).toBe(0)
    act(() => resolveApply(server))
    await waitFor(() => expect(result.current.cart.cart?.lines[0]?.lineId).toBe('server-line'))
  })

  it('lineId convergence: pessimistic add yields the SERVER lineId, so a later setQty on it succeeds (no spurious CartValidationError)', async () => {
    // Original full-optimistic hazard: optimistic add mints client-id-A, server mints id-B,
    // then setQty(A) hits the server's id-B → CartValidationError. Pessimistic add removes id-A
    // entirely — the line first appears WITH the server id (the UI can only edit it after it
    // renders), so every edit targets a real, server-known id. This is the realistic UI flow.
    const seeded = aCart()
    const added = aCart({ lines: [ln({ lineId: 'server-line', qty: 1 })], subtotal: 100n })
    const reQtied = aCart({ lines: [ln({ lineId: 'server-line', qty: 4 })], subtotal: 400n })
    const apply = vi.fn()
      .mockImplementationOnce(async () => added)     // addLine
      .mockImplementationOnce(async () => reQtied)   // setQty on server-line
    const client = { getCart: vi.fn(async () => seeded), apply, merge: vi.fn() }
    const { result } = setup(client, seeded)
    let addErr: unknown, qtyErr: unknown
    await act(async () => {
      const p1 = result.current.act.addLine({ variantId: 'v1', qty: 1, price: PRICE }).catch((e) => { addErr = e })
      await p1 // line now rendered with the server id — only now is it editable
      await result.current.act.setQty('server-line', 4).catch((e) => { qtyErr = e })
    })
    expect(addErr).toBeUndefined()
    expect(qtyErr).toBeUndefined()
    expect(result.current.cart.cart?.subtotal).toBe(400n)
  })

  it('structural floor: setQty on a lineId absent from the cart throws CartValidationError at call time, no network call (a phantom line is never editable)', async () => {
    // Proves the safety mechanism behind the convergence resolution: editing a non-existent
    // line is rejected client-side with NO round-trip. Combined with pessimistic-add (a line
    // appears only with its server id), this closes the convergence hole.
    const seeded = aCart() // empty cart
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(async () => seeded), merge: vi.fn() }
    const { result } = setup(client, seeded)
    await act(async () => { await result.current.act.setQty('not-here', 2).catch(() => {}) })
    expect(isCartValidationError(result.current.act.error)).toBe(true)
    expect(client.apply).not.toHaveBeenCalled()
  })

  it('rapid same-tick taps are ALL instant — optimism is call-time, not deferred behind the persist queue', async () => {
    // THE discriminating test: fire two setQty in one tick with no await, slow first apply.
    // Call-time optimism → UI shows 3 immediately (both reduces ran). The old deferred-in-queue
    // engine FAILS here: tap2's optimistic setCart would wait behind tap1's slow apply → shows 2.
    const seeded = aCart({ lines: [ln({ qty: 1 })], subtotal: 100n })
    let resolveFirst: (c: Cart) => void = () => {}
    const apply = vi.fn()
      .mockImplementationOnce(() => new Promise<Cart>((r) => { resolveFirst = r })) // slow first persist
      .mockImplementation(async () => aCart({ lines: [ln({ qty: 3 })], subtotal: 300n }))
    const client = { getCart: vi.fn(async () => seeded), apply, merge: vi.fn() }
    const { result } = setup(client, seeded)
    act(() => {
      void result.current.act.setQty('l1', 2)
      void result.current.act.setQty('l1', 3)
    })
    // both optimistic reduces ran at call time → UI shows 3 before ANY persist resolves
    expect(result.current.cart.itemCount).toBe(3)
    expect(apply).toHaveBeenCalledTimes(1) // tap2's persist still queued behind the slow first
    act(() => resolveFirst(aCart({ lines: [ln({ qty: 2 })], subtotal: 200n })))
    await waitFor(() => expect(apply).toHaveBeenCalledTimes(2))
    // tap1's server result (qty2) is STALE → discarded by the seq guard; final reconcile = qty3 (no flicker)
    await waitFor(() => expect(result.current.cart.cart?.subtotal).toBe(300n))
  })

  it('cross-currency addLine → MixedCurrencyError surfaced into error', async () => {
    const seeded = aCart({ currency: 'USD' })
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(async () => { const { MixedCurrencyError } = await import('@platform-modules/commerce-cart'); throw new MixedCurrencyError('USD', 'EUR') }), merge: vi.fn() }
    const { result } = setup(client, seeded)
    await act(async () => { await result.current.act.addLine({ variantId: 'v1', qty: 1, price: { amount: 100n, currency: 'EUR', priceMode: 'exclusive' } }).catch(() => {}) })
    expect(isMixedCurrencyError(result.current.act.error)).toBe(true)
  })

  it('merge reconciles to the returned cart', async () => {
    const seeded = aCart()
    const merged = aCart({ lines: [ln()], subtotal: 100n })
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(), merge: vi.fn(async () => merged) }
    const { result } = setup(client, seeded)
    await act(async () => { await result.current.act.merge('guest-1') })
    expect(result.current.cart.cart?.subtotal).toBe(100n)
    expect(client.merge).toHaveBeenCalledWith('guest-1')
  })

  it('no-cart edge: action with null base skips optimistic, seeds from result', async () => {
    const pending = new Promise<Cart>(() => {}) // getCart never resolves → cart null
    const server = aCart({ lines: [ln()], subtotal: 100n })
    const client = { getCart: () => pending, apply: vi.fn(async () => server), merge: vi.fn() }
    const { result } = setup(client as never) // not seeded
    await act(async () => { await result.current.act.setQty('l1', 1) })
    expect(result.current.cart.cart?.subtotal).toBe(100n)
  })

  it('pending toggles around a mutation', async () => {
    const seeded = aCart({ lines: [ln()], subtotal: 100n })
    let resolveApply: (c: Cart) => void = () => {}
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(() => new Promise<Cart>((r) => { resolveApply = r })), merge: vi.fn() }
    const { result } = setup(client, seeded)
    act(() => { void result.current.act.setQty('l1', 2) })
    await waitFor(() => expect(result.current.act.pending).toBe(true))
    act(() => resolveApply(seeded))
    await waitFor(() => expect(result.current.act.pending).toBe(false))
  })

  it('action callbacks are referentially stable across a cart change', async () => {
    const seeded = aCart({ lines: [ln()], subtotal: 100n })
    const next = aCart({ lines: [ln({ qty: 2 })], subtotal: 200n })
    const client = { getCart: vi.fn(async () => seeded), apply: vi.fn(async () => next), merge: vi.fn() }
    const { result } = setup(client, seeded)
    const before = result.current.act.addLine
    await act(async () => { await result.current.act.setQty('l1', 2) })
    expect(result.current.cart.cart?.subtotal).toBe(200n) // cart changed
    expect(result.current.act.addLine).toBe(before)       // identity stable
  })
})