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

// Mock the billing-react confirm seam so no real Stripe provider is needed.
const confirmMock = 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 { useCheckoutConfirm } from './useCheckoutConfirm.js'
import { useCheckout } from './useCheckout.js'
import { CheckoutProvider } from './CheckoutProvider.js'
import type { CheckoutClient } from './client.js'

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

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

// A client whose start() reaches awaiting_payment (card flow: orderId + clientSecret).
function cardClient(getStatus: CheckoutClient['getStatus']): CheckoutClient {
  return {
    start: vi.fn().mockResolvedValue({ orderId: 'o1', clientSecret: 'cs_123' }),
    getStatus,
  }
}

function Harness() {
  const { phase, submit } = useCheckout()
  const { confirm } = useCheckoutConfirm()
  return (
    <div>
      <span data-testid="phase">{phase}</span>
      <button onClick={() => void submit({ cartId: 'c1', priceMode: 'inclusive', currency: 'USD', buyerCountry: 'US' })}>submit</button>
      <button onClick={() => void confirm({ returnUrl: RETURN_URL })}>confirm</button>
    </div>
  )
}

function mount(client: CheckoutClient) {
  return render(
    <CheckoutProvider client={client} cartId="c1">
      <Harness />
    </CheckoutProvider>,
  )
}

// Seed awaiting_payment first — confirm() guards on orderId+clientSecret (§6); a bare
// confirm() before submit is a no-op, so every confirm test must reach awaiting_payment.
async function reachAwaitingPayment() {
  screen.getByText('submit').click()
  await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('awaiting_payment'))
}

describe('useCheckoutConfirm', () => {
  it('forwards the host returnUrl to billing-react confirm', async () => {
    confirmMock.mockResolvedValue({ kind: 'succeeded' })
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'charging' })))
    await reachAwaitingPayment()
    screen.getByText('confirm').click()
    await waitFor(() => expect(confirmMock).toHaveBeenCalledWith({ returnUrl: RETURN_URL }))
  })

  it('client-succeeded → settling (NOT paid) — server poll owns money-truth', async () => {
    confirmMock.mockResolvedValue({ kind: 'succeeded' })
    // server still charging — stays settling, never flips to paid off client success
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'charging' })))
    await reachAwaitingPayment()
    screen.getByText('confirm').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('settling'))
    // give the poll a tick — must NOT become paid while server says charging
    await new Promise((r) => setTimeout(r, 50))
    expect(screen.getByTestId('phase').textContent).toBe('settling')
  })

  it('confirm error → failed', async () => {
    const err = Object.assign(new Error('declined'), { name: 'PaymentConfirmError' })
    confirmMock.mockResolvedValue({ kind: 'error', error: err })
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'pending' })))
    await reachAwaitingPayment()
    screen.getByText('confirm').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('failed'))
  })

  it('requires_redirect leaves confirming (browser navigates)', async () => {
    confirmMock.mockResolvedValue({ kind: 'requires_redirect' })
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'pending' })))
    await reachAwaitingPayment()
    screen.getByText('confirm').click()
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('confirming'))
  })

  it('double confirm() rapidly calls billing-react confirm once', async () => {
    let resolveConfirm!: (v: { kind: 'succeeded' }) => void
    confirmMock.mockImplementation(
      () =>
        new Promise((r) => {
          resolveConfirm = r
        }),
    )
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'charging' })))
    await reachAwaitingPayment()
    screen.getByText('confirm').click()
    screen.getByText('confirm').click()
    expect(confirmMock).toHaveBeenCalledTimes(1)
    resolveConfirm({ kind: 'succeeded' })
    await waitFor(() => expect(screen.getByTestId('phase').textContent).toBe('settling'))
  })

  it('confirm() when phase is not awaiting_payment is a no-op', async () => {
    confirmMock.mockResolvedValue({ kind: 'succeeded' })
    mount(cardClient(vi.fn().mockResolvedValue({ status: 'paid' })))
    // idle — no submit yet
    screen.getByText('confirm').click()
    await new Promise((r) => setTimeout(r, 20))
    expect(confirmMock).not.toHaveBeenCalled()
    expect(screen.getByTestId('phase').textContent).toBe('idle')
  })
})