import { render, screen, waitFor } from '@testing-library/react'
import { useContext, useEffect } from 'react'
import { describe, expect, it, vi, beforeEach } from 'vitest'

const confirmMock = vi.hoisted(() => vi.fn())
vi.mock('@platform-modules/billing-react', () => ({
  useConfirmPayment: () => ({ confirm: confirmMock, status: 'idle', error: null }),
  isPaymentConfirmError: (e: unknown) =>
    typeof e === 'object' && e !== null && (e as { name?: string }).name === 'PaymentConfirmError',
}))

import { CheckoutProvider } from './CheckoutProvider.js'
import { CheckoutContext } from './context.js'
import { CheckoutWireError, OrderIdempotencyConflictError } from './errors.js'
import { loadRecord } from './idempotency.js'
import { useCheckout } from './useCheckout.js'
import { useCheckoutConfirm } from './useCheckoutConfirm.js'
import type { CheckoutClient } from './client.js'

const RETURN_URL = 'https://app.test/checkout/return'

beforeEach(() => {
  sessionStorage.clear()
  confirmMock.mockReset()
})

function Probe() {
  const ctx = useContext(CheckoutContext)!
  return (
    <div>
      <span data-testid="phase">{ctx.state.phase}</span>
      <span data-testid="order">{ctx.state.orderId ?? ''}</span>
      <span data-testid="secret">{ctx.state.clientSecret ?? ''}</span>
      <button onClick={() => void ctx.submit({ cartId: 'c1', priceMode: 'inclusive', currency: 'usd', buyerCountry: 'US' })}>go</button>
    </div>
  )
}

function mountWith(client: CheckoutClient, opts?: { cartVersion?: string | number }) {
  return render(
    <CheckoutProvider client={client} cartId="c1" cartVersion={opts?.cartVersion}>
      <Probe />
    </CheckoutProvider>,
  )
}

describe('resume-on-mount (§6)', () => {
  it('persisted non-terminal orderId → re-enters settling and polls getStatus', async () => {
    sessionStorage.setItem(
      'checkout:c1',
      JSON.stringify({ idempotencyKey: 'k-resume', orderId: 'o-resume' }),
    )
    const getStatus = vi.fn().mockResolvedValue({ status: 'charging' })
    const client: CheckoutClient = {
      start: vi.fn(),
      getStatus,
    }
    mountWith(client)
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('settling'))
    await waitFor(() => expect(getStatus).toHaveBeenCalledWith('o-resume'))
    expect(screen.getByTestId('order').textContent).toBe('o-resume')
  })
})

describe('CheckoutProvider.submit branching', () => {
  it('clientSecret present → awaiting_payment', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_1' }),
      getStatus: vi.fn(),
    }
    mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(screen.getByTestId('order').textContent).toBe('o1')
  })

  it('no clientSecret → settling', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o2' }),
      getStatus: vi.fn().mockResolvedValue({ status: 'paid' }),
    }
    mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(['settling', 'paid']).toContain(screen.getByTestId('phase').textContent))
  })

  it('stale wire error → stale phase', async () => {
    const stale = { removed: [], updated: [{ variantId: 'v', was: 1n, now: 2n }] }
    const client: CheckoutClient = {
      start: vi.fn().mockRejectedValue(new CheckoutWireError('stale', { stale })),
      getStatus: vi.fn(),
    }
    mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('stale'))
  })

  it('idempotency conflict → cart_changed, clears the record so the next submit mints a fresh key', async () => {
    // Seed a prior attempt's key. submit() reuses it via ensureKey, sends it to start(),
    // which rejects with the conflict; the provider then clears the record.
    sessionStorage.setItem('checkout:c1', JSON.stringify({ idempotencyKey: 'old-key' }))
    const client: CheckoutClient = {
      start: vi.fn().mockRejectedValue(new OrderIdempotencyConflictError({ idempotencyKey: 'old-key' })),
      getStatus: vi.fn(),
    }
    mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('cart_changed'))
    // record cleared → the conflicting key is never reused; the next submit's ensureKey mints fresh
    expect(sessionStorage.getItem('checkout:c1')).toBeNull()
  })
})

describe('proactive cart-change guard (§0.1 layer a)', () => {
  it('initial cartVersion on mount does NOT invalidate', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_X' }),
      getStatus: vi.fn(),
    }
    mountWith(client, { cartVersion: 'v1' })
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(screen.getByTestId('secret').textContent).toBe('cs_X')
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()
  })

  it('submit twice on unchanged cart reuses the same idempotencyKey', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_1' }),
      getStatus: vi.fn(),
    }
    mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    const startMock = client.start as ReturnType<typeof vi.fn>
    const firstKey = startMock.mock.calls[0]?.[0]?.idempotencyKey
    screen.getByText('go').click()
    await waitFor(() => expect(startMock.mock.calls.length).toBe(2))
    const secondKey = startMock.mock.calls[1]?.[0]?.idempotencyKey
    expect(firstKey).toBeDefined()
    expect(secondKey).toBeDefined()
    expect(secondKey).toBe(firstKey)
  })

  it('cartVersion change while awaiting_payment → cart_changed, clears record and clientSecret', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_X' }),
      getStatus: vi.fn(),
    }
    const { rerender } = mountWith(client, { cartVersion: 'v1' })
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()

    rerender(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v2">
        <Probe />
      </CheckoutProvider>,
    )
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('cart_changed'))
    expect(screen.getByTestId('secret').textContent).toBe('')
    expect(sessionStorage.getItem('checkout:c1')).toBeNull()
  })

  it('confirm-in-flight cart change does not double-charge / never lands cart_changed', async () => {
    let resolveConfirm!: (v: { kind: 'succeeded' }) => void
    confirmMock.mockImplementation(
      () =>
        new Promise((r) => {
          resolveConfirm = r
        }),
    )
    const getStatus = vi.fn().mockResolvedValue({ status: 'paid' })
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_X' }),
      getStatus,
    }
    const sawCartChanged = { current: false }
    const { rerender } = render(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v1">
        <ConfirmInFlightProbe sawCartChanged={sawCartChanged} />
      </CheckoutProvider>,
    )
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(screen.getByTestId('secret').textContent).toBe('cs_X')

    screen.getByText('confirm').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('confirming'))

    rerender(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v2">
        <ConfirmInFlightProbe sawCartChanged={sawCartChanged} />
      </CheckoutProvider>,
    )
    expect(screen.getByTestId('phase').textContent).toBe('confirming')
    expect(sawCartChanged.current).toBe(false)

    resolveConfirm({ kind: 'succeeded' })
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('paid'))
    expect(sawCartChanged.current).toBe(false)
    expect(getStatus).toHaveBeenCalledWith('o1')
  })

  it('synchronous starting-window close — cartVersion change during starting never exposes clientSecret (T2-002)', async () => {
    let resolveStart!: (value: { orderId: string; clientSecret: string }) => void
    const startPromise = new Promise<{ orderId: string; clientSecret: string }>((resolve) => {
      resolveStart = resolve
    })
    const client: CheckoutClient = {
      start: vi.fn().mockReturnValue(startPromise),
      getStatus: vi.fn(),
    }
    const secretEverTruthy = { current: false }
    const { rerender } = render(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v1">
        <SecretTracker probe={secretEverTruthy} />
      </CheckoutProvider>,
    )
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('starting'))

    rerender(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v2">
        <SecretTracker probe={secretEverTruthy} />
      </CheckoutProvider>,
    )
    expect(screen.getByTestId('phase').textContent).toBe('starting')

    resolveStart({ orderId: 'o1', clientSecret: 'cs_stale' })
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('cart_changed'))
    expect(screen.getByTestId('secret').textContent).toBe('')
    expect(secretEverTruthy.current).toBe(false)
    expect(sessionStorage.getItem('checkout:c1')).toBeNull()
  })

  it('reset during starting abandons in-flight start — no resurrected clientSecret (T2-001)', async () => {
    let resolveStart!: (value: { orderId: string; clientSecret: string }) => void
    const startPromise = new Promise<{ orderId: string; clientSecret: string }>((resolve) => {
      resolveStart = resolve
    })
    const client: CheckoutClient = {
      start: vi.fn().mockReturnValue(startPromise),
      getStatus: vi.fn(),
    }
    render(
      <CheckoutProvider client={client} cartId="c1">
        <ResetProbe />
      </CheckoutProvider>,
    )
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('starting'))

    screen.getByText('reset').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('idle'))

    resolveStart({ orderId: 'o1', clientSecret: 'cs_cancelled' })
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('idle'))
    expect(screen.getByTestId('secret').textContent).toBe('')
    expect(loadRecord('c1')).toBeNull()
  })

  it('no cartVersion prop → no spurious cart_changed in awaiting_payment', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_X' }),
      getStatus: vi.fn(),
    }
    const { rerender } = mountWith(client)
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()

    rerender(
      <CheckoutProvider client={client} cartId="c1">
        <Probe />
      </CheckoutProvider>,
    )
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(screen.getByTestId('secret').textContent).toBe('cs_X')
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()
  })

  it('re-render with unchanged cartVersion does NOT invalidate', async () => {
    const client: CheckoutClient = {
      start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_X' }),
      getStatus: vi.fn(),
    }
    const { rerender } = mountWith(client, { cartVersion: 'v1' })
    screen.getByText('go').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()

    rerender(
      <CheckoutProvider client={client} cartId="c1" cartVersion="v1">
        <Probe />
      </CheckoutProvider>,
    )
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
    expect(screen.getByTestId('secret').textContent).toBe('cs_X')
    expect(sessionStorage.getItem('checkout:c1')).not.toBeNull()
  })
})

function SecretTracker({ probe }: { probe: { current: boolean } }) {
  const ctx = useContext(CheckoutContext)!
  if (ctx.state.clientSecret) probe.current = true
  return (
    <div>
      <span data-testid="phase">{ctx.state.phase}</span>
      <span data-testid="secret">{ctx.state.clientSecret ?? ''}</span>
      <button onClick={() => void ctx.submit({ cartId: 'c1', priceMode: 'inclusive', currency: 'usd', buyerCountry: 'US' })}>go</button>
    </div>
  )
}

function ResetProbe() {
  const ctx = useContext(CheckoutContext)!
  return (
    <div>
      <span data-testid="phase">{ctx.state.phase}</span>
      <span data-testid="secret">{ctx.state.clientSecret ?? ''}</span>
      <button onClick={() => void ctx.submit({ cartId: 'c1', priceMode: 'inclusive', currency: 'usd', buyerCountry: 'US' })}>go</button>
      <button onClick={() => ctx.reset()}>reset</button>
    </div>
  )
}

function ConfirmInFlightProbe({ sawCartChanged }: { sawCartChanged: { current: boolean } }) {
  const { phase, submit, clientSecret } = useCheckout()
  const { confirm } = useCheckoutConfirm()
  useEffect(() => {
    if (phase === 'cart_changed') sawCartChanged.current = true
  }, [phase, sawCartChanged])
  return (
    <div>
      <span data-testid="phase">{phase}</span>
      <span data-testid="secret">{clientSecret ?? ''}</span>
      <button onClick={() => void submit({ cartId: 'c1', priceMode: 'inclusive', currency: 'usd', buyerCountry: 'US' })}>go</button>
      <button onClick={() => void confirm({ returnUrl: RETURN_URL })}>confirm</button>
    </div>
  )
}