import { act, render, waitFor } from '@testing-library/react'
import type { Stripe } from '@stripe/stripe-js'
import { useEffect, useRef } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { isPaymentConfirmError, isPaymentProviderError } from './errors'
import { StripePaymentProvider } from './StripePaymentProvider'
import { useConfirmPayment } from './useConfirmPayment'
import { usePaymentElement } from './usePaymentElement'

// A resolved null stub so jsdom never hits the network and Elements accepts the prop.
const stubStripeNull = Promise.resolve(null)

/** Minimal Stripe shape that passes @stripe/react-stripe-js isStripe validation in jsdom. */
function makeStripeStub(confirmPayment: ReturnType<typeof vi.fn>): Stripe {
  const elements = {}
  return {
    elements: vi.fn(() => elements),
    createToken: vi.fn(),
    createPaymentMethod: vi.fn(),
    confirmCardPayment: vi.fn(),
    confirmPayment,
  } as unknown as Stripe
}

function BridgeTestConsumer({
  onReady,
}: {
  onReady: (confirm: ReturnType<typeof useConfirmPayment>['confirm']) => void
}): null {
  const { confirm } = useConfirmPayment()
  const { ready } = usePaymentElement()
  const reported = useRef(false)
  useEffect(() => {
    if (ready && !reported.current) {
      reported.current = true
      onReady(confirm)
    }
  }, [ready, confirm, onReady])
  return null
}

describe('StripePaymentProvider', () => {
  it('throws PaymentProviderError synchronously when publishableKey is a secret key', () => {
    let thrown: unknown
    try {
      render(
        <StripePaymentProvider publishableKey="sk_live_X" clientSecret="pi_1_secret_x" stripeOverride={stubStripeNull}>
          <div>child</div>
        </StripePaymentProvider>,
      )
    } catch (e) {
      thrown = e
    }
    expect(isPaymentProviderError(thrown)).toBe(true)
  })

  it('throws PaymentProviderError when clientSecret is malformed', () => {
    let thrown: unknown
    try {
      render(
        <StripePaymentProvider publishableKey="pk_test_X" clientSecret="garbage" stripeOverride={stubStripeNull}>
          <div>child</div>
        </StripePaymentProvider>,
      )
    } catch (e) {
      thrown = e
    }
    expect(isPaymentProviderError(thrown)).toBe(true)
  })

  it('renders children with a valid pk_ + clientSecret (uses stripeOverride, no network)', () => {
    const { getByText } = render(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_1_secret_x" stripeOverride={stubStripeNull}>
        <div>child-ok</div>
      </StripePaymentProvider>,
    )
    expect(getByText('child-ok')).toBeTruthy()
  })

  it('does NOT call loadStripe when stripeOverride is supplied (stable memo seam)', async () => {
    // loadStripe is the production path; with an override it must never be invoked.
    const mod = await import('@stripe/stripe-js')
    const spy = vi.spyOn(mod, 'loadStripe')
    render(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_1_secret_x" stripeOverride={stubStripeNull}>
        <div>x</div>
      </StripePaymentProvider>,
    )
    expect(spy).not.toHaveBeenCalled()
    spy.mockRestore()
  })

  it('memoizes the Stripe promise across re-renders (Elements group does not remount)', () => {
    let mountCount = 0
    function MountProbe(): null {
      useEffect(() => {
        mountCount++
      }, [])
      return null
    }

    const stripeOverride = Promise.resolve(null)
    const { rerender } = render(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_1_secret_x" stripeOverride={stripeOverride}>
        <MountProbe />
      </StripePaymentProvider>,
    )
    expect(mountCount).toBe(1)

    rerender(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_1_secret_x" stripeOverride={stripeOverride}>
        <MountProbe />
      </StripePaymentProvider>,
    )
    expect(mountCount).toBe(1)
  })

  it('remounts the Elements group when clientSecret changes (internal key={clientSecret})', () => {
    let mountCount = 0
    function MountProbe(): null {
      useEffect(() => {
        mountCount++
      }, [])
      return null
    }

    const stripeOverride = Promise.resolve(null)
    const { rerender } = render(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_1_secret_a" stripeOverride={stripeOverride}>
        <MountProbe />
      </StripePaymentProvider>,
    )
    expect(mountCount).toBe(1)

    rerender(
      <StripePaymentProvider publishableKey="pk_test_X" clientSecret="pi_2_secret_b" stripeOverride={stripeOverride}>
        <MountProbe />
      </StripePaymentProvider>,
    )
    expect(mountCount).toBe(2)
  })

  describe('bridge confirmPayment mapping (real StripePaymentUiBridge)', () => {
    it('calls stripe.confirmPayment with redirect if_required and maps succeeded', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({
        paymentIntent: { status: 'succeeded' },
      })
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(confirmPayment).toHaveBeenCalledWith({
        elements: expect.anything(),
        confirmParams: { return_url: 'https://example.com/return' },
        redirect: 'if_required',
      })
      expect(res).toEqual({ kind: 'succeeded' })
    })

    it('maps Stripe error.code verbatim to PaymentConfirmError.code', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({
        error: { code: 'card_declined', message: 'no' },
      })
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(res?.kind).toBe('error')
      if (res?.kind === 'error') {
        expect(isPaymentConfirmError(res.error)).toBe(true)
        expect(res.error.code).toBe('card_declined')
        expect(res.error.message).toBe('no')
      }
    })

    it('falls back to error.type when error.code is omitted', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({
        error: { type: 'api_error', message: 'x' },
      })
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(res?.kind).toBe('error')
      if (res?.kind === 'error') {
        expect(res.error.code).toBe('api_error')
      }
    })

    it('maps non-succeeded paymentIntent status (processing) to requires_redirect, NOT succeeded', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({
        paymentIntent: { status: 'processing' },
      })
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(res).toEqual({ kind: 'requires_redirect' })
    })

    it('maps neither paymentIntent nor error to requires_redirect (browser redirecting)', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({})
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(res).toEqual({ kind: 'requires_redirect' })
    })

    it('falls back to payment_failed sentinel when error.code and error.type are both absent', async () => {
      const confirmPayment = vi.fn().mockResolvedValue({
        error: { message: 'mystery' },
      })
      const stripeOverride = Promise.resolve(makeStripeStub(confirmPayment))
      let confirmFn: ReturnType<typeof useConfirmPayment>['confirm'] | undefined

      render(
        <StripePaymentProvider
          publishableKey="pk_test_X"
          clientSecret="pi_1_secret_x"
          stripeOverride={stripeOverride}
        >
          <BridgeTestConsumer onReady={(c) => { confirmFn = c }} />
        </StripePaymentProvider>,
      )

      await waitFor(() => expect(confirmFn).toBeDefined())

      let res: Awaited<ReturnType<ReturnType<typeof useConfirmPayment>['confirm']>> | undefined
      await act(async () => {
        res = await confirmFn!({ returnUrl: 'https://example.com/return' })
      })

      expect(res?.kind).toBe('error')
      if (res?.kind === 'error') {
        expect(isPaymentConfirmError(res.error)).toBe(true)
        expect(res.error.code).toBe('payment_failed')
        expect(res.error.message).toBe('mystery')
      }
    })
  })
})