import { afterEach, describe, expect, it, vi } from 'vitest'
import { WebhookVerificationError } from '../errors.js'
import { PaypalProvider, type PaypalCreds } from './index.js'

function jsonResponse(status: number, body: unknown): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  })
}

function textResponse(status: number, body: string): Response {
  return new Response(body, { status })
}

function createWebhookHeaders(overrides: Record<string, string> = {}): Headers {
  return new Headers({
    'paypal-auth-algo': 'SHA256withRSA',
    'paypal-cert-url': 'https://api-m.paypal.com/v1/notifications/certs/CERT-1',
    'paypal-transmission-id': 'transmission-1',
    'paypal-transmission-sig': 'c2lnbmF0dXJl',
    'paypal-transmission-time': '2026-07-04T10:00:00Z',
    ...overrides,
  })
}

function createProvider(
  fetchImpl: typeof fetch,
  subtleOverrides: Partial<SubtleCrypto> = {},
): PaypalProvider {
  vi.stubGlobal('crypto', {
    subtle: {
      importKey: vi.fn(async () => ({ key: 'paypal-cert' } as unknown as CryptoKey)),
      verify: vi.fn(async () => true),
      ...subtleOverrides,
    } satisfies Partial<SubtleCrypto>,
  })

  return new PaypalProvider({
    clientId: 'client-id',
    clientSecret: 'client-secret',
    webhookId: 'wh_123',
    fetch: fetchImpl,
  } satisfies PaypalCreds)
}

afterEach(() => {
  vi.restoreAllMocks()
  vi.unstubAllGlobals()
})

describe('billing/paypal', () => {
  it('parses a signed recurring settlement webhook with a stable chargeKey', async () => {
    const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
      const url = String(input)
      if (url.includes('/certs/')) {
        return textResponse(
          200,
          ['-----BEGIN PUBLIC KEY-----', 'AA==', '-----END PUBLIC KEY-----'].join('\n'),
        )
      }
      throw new Error(`unexpected fetch ${url}`)
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)
    const headers = createWebhookHeaders()
    const rawBody = JSON.stringify({
      id: 'WH-SETTLEMENT-1',
      event_type: 'PAYMENT.SALE.COMPLETED',
      resource: {
        id: 'SALE-1',
        billing_agreement_id: 'I-SUBSCRIPTION-1',
        create_time: '2026-07-04T10:00:00Z',
        amount: { total: '12.00', currency: 'USD' },
        billing_period: { start_date: '2026-07-01T00:00:00Z' },
      },
    })

    const event = await provider.parseWebhook(headers, rawBody)

    expect(event).toMatchObject({
      kind: 'settlement',
      eventId: 'WH-SETTLEMENT-1',
      chargeKey: 'I-SUBSCRIPTION-1:2026-07',
      providerRef: 'SALE-1',
      amount: 1200,
      currency: 'USD',
      subscriptionId: 'I-SUBSCRIPTION-1',
      period: '2026-07',
      amountMinor: 1200n,
    })
  })

  it('throws WebhookVerificationError for an invalid webhook signature', async () => {
    const fetchMock = vi.fn(async () =>
      textResponse(
        200,
        ['-----BEGIN PUBLIC KEY-----', 'AA==', '-----END PUBLIC KEY-----'].join('\n'),
      ),
    ) as unknown as typeof fetch
    const provider = createProvider(fetchMock, {
      verify: vi.fn(async () => false),
    })
    const headers = createWebhookHeaders()
    const rawBody = JSON.stringify({
      id: 'WH-BAD-1',
      event_type: 'PAYMENT.SALE.COMPLETED',
      resource: {
        id: 'SALE-1',
        billing_agreement_id: 'I-SUBSCRIPTION-1',
        amount: { total: '12.00', currency: 'USD' },
        billing_period: { start_date: '2026-07-01T00:00:00Z' },
      },
    })

    await expect(provider.parseWebhook(rawBody, headers)).rejects.toBeInstanceOf(
      WebhookVerificationError,
    )
  })

  it('maps a refund webhook to a refund ProviderEvent', async () => {
    const fetchMock = vi.fn(async () =>
      textResponse(
        200,
        ['-----BEGIN PUBLIC KEY-----', 'AA==', '-----END PUBLIC KEY-----'].join('\n'),
      ),
    ) as unknown as typeof fetch
    const provider = createProvider(fetchMock)
    const headers = createWebhookHeaders({ 'paypal-transmission-id': 'transmission-2' })
    const rawBody = JSON.stringify({
      id: 'WH-REFUND-1',
      event_type: 'PAYMENT.SALE.REFUNDED',
      resource: {
        id: 'RFND-1',
        sale_id: 'SALE-1',
        billing_agreement_id: 'I-SUBSCRIPTION-1',
        update_time: '2026-08-04T11:00:00Z',
        amount: { total: '12.00', currency: 'USD' },
        sale: {
          billing_period: { start_date: '2026-07-01T00:00:00Z' },
        },
      },
    })

    const event = await provider.parseWebhook(rawBody, headers)

    expect(event).toMatchObject({
      kind: 'refund',
      eventId: 'WH-REFUND-1',
      refundKey: 'refund:I-SUBSCRIPTION-1%3A2026-07:RFND-1',
      chargeKey: 'I-SUBSCRIPTION-1:2026-07',
      providerRef: 'RFND-1',
      amount: 1200,
      currency: 'USD',
    })
  })

  it('rejects webhook money values that are non-positive or too large before BigInt work', async () => {
    const fetchMock = vi.fn(async () =>
      textResponse(
        200,
        ['-----BEGIN PUBLIC KEY-----', 'AA==', '-----END PUBLIC KEY-----'].join('\n'),
      ),
    ) as unknown as typeof fetch
    const provider = createProvider(fetchMock)
    const base = {
      id: 'WH-SETTLEMENT-2',
      event_type: 'PAYMENT.SALE.COMPLETED',
      resource: {
        id: 'SALE-2',
        billing_agreement_id: 'I-SUBSCRIPTION-1',
        amount: { total: '0.00', currency: 'USD' },
        billing_period: { start_date: '2026-07-01T00:00:00Z' },
      },
    }

    await expect(
      provider.parseWebhook(
        JSON.stringify(base),
        createWebhookHeaders({ 'paypal-transmission-id': 'transmission-3' }),
      ),
    ).rejects.toThrow(/positive/i)
    await expect(
      provider.parseWebhook(
        JSON.stringify({
          ...base,
          id: 'WH-SETTLEMENT-3',
          resource: {
            ...base.resource,
            amount: { total: `${'1'.repeat(22)}.00`, currency: 'USD' },
          },
        }),
        createWebhookHeaders({ 'paypal-transmission-id': 'transmission-4' }),
      ),
    ).rejects.toThrow(/too many digits/i)
  })

  it('returns the caller refundKey unchanged from refund()', async () => {
    const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
      const url = String(input)
      if (url.endsWith('/v1/oauth2/token')) {
        return jsonResponse(200, { access_token: 'access-token' })
      }
      if (url.endsWith('/v2/payments/captures/CAPTURE-1')) {
        return jsonResponse(200, {
          id: 'CAPTURE-1',
          amount: { currency_code: 'USD', value: '12.00' },
        })
      }
      if (url.endsWith('/v2/payments/captures/CAPTURE-1/refund') && init?.method === 'POST') {
        return jsonResponse(201, {
          id: 'RFND-1',
          status: 'COMPLETED',
          amount: { currency_code: 'USD', value: '12.00' },
        })
      }
      throw new Error(`unexpected fetch ${url}`)
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    const result = await provider.refund({
      refundKey: 'refund-key-input',
      chargeKey: 'subscription-charge-key',
      refundId: 'CAPTURE-1',
      amount: 1200,
    })

    expect(result).toMatchObject({
      kind: 'refunded',
      refundKey: 'refund-key-input',
      chargeKey: 'subscription-charge-key',
      providerRef: 'RFND-1',
      amount: 1200,
    })
  })

  it('threads currency through settled charges and refunds', async () => {
    const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
      const url = String(input)
      if (url.endsWith('/v1/oauth2/token')) {
        return jsonResponse(200, { access_token: 'access-token' })
      }
      if (url.endsWith('/v2/checkout/orders') && init?.method === 'POST') {
        return jsonResponse(201, {
          id: 'ORDER-1',
          purchase_units: [
            {
              payments: {
                captures: [
                  {
                    id: 'CAPTURE-1',
                    status: 'COMPLETED',
                    amount: { currency_code: 'ILS', value: '10.00' },
                  },
                ],
              },
            },
          ],
        })
      }
      if (url.endsWith('/v2/payments/captures/CAPTURE-1')) {
        return jsonResponse(200, {
          id: 'CAPTURE-1',
          amount: { currency_code: 'ILS', value: '10.00' },
        })
      }
      if (url.endsWith('/v2/payments/captures/CAPTURE-1/refund') && init?.method === 'POST') {
        return jsonResponse(201, {
          id: 'RFND-1',
          status: 'COMPLETED',
          amount: { currency_code: 'ILS', value: '10.00' },
        })
      }
      throw new Error(`unexpected fetch ${url}`)
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    await expect(
      provider.charge({
        chargeKey: 'charge-key',
        amount: 1000,
        currency: 'ILS',
      }),
    ).resolves.toMatchObject({ kind: 'settled', amount: 1000, currency: 'ILS' })
    await expect(
      provider.refund({
        refundKey: 'refund-key-input',
        chargeKey: 'charge-key',
        refundId: 'CAPTURE-1',
        amount: 1000,
      }),
    ).resolves.toMatchObject({ kind: 'refunded', amount: 1000, currency: 'ILS' })
  })

  it('rejects non-positive request amounts before calling PayPal', async () => {
    const fetchMock = vi.fn(async () => {
      throw new Error('PayPal should not be called for invalid amounts')
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    await expect(
      provider.charge({
        chargeKey: 'charge-key',
        amount: 0,
        currency: 'USD',
      }),
    ).rejects.toThrow(/positive/i)
    await expect(
      provider.refund({
        refundKey: 'refund-key',
        chargeKey: 'charge-key',
        refundId: 'CAPTURE-1',
        amount: -1,
      }),
    ).rejects.toThrow(/positive/i)

    expect(fetchMock).not.toHaveBeenCalled()
  })

  it('rejects generic PayPal charge keys that can collide with subscription charge keys', async () => {
    const fetchMock = vi.fn(async () => {
      throw new Error('PayPal should not be called for invalid charge keys')
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    await expect(
      provider.charge({
        chargeKey: 'I-SUBSCRIPTION-1:2026-07',
        amount: 1000,
        currency: 'USD',
      }),
    ).rejects.toThrow(/chargeKey/i)

    expect(fetchMock).not.toHaveBeenCalled()
  })

  it('round-trips subscription create and get calls through PayPal APIs', async () => {
    const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
      const url = String(input)
      if (url.endsWith('/v1/oauth2/token')) {
        return jsonResponse(200, { access_token: 'access-token' })
      }
      if (url.endsWith('/v1/billing/subscriptions') && init?.method === 'POST') {
        return jsonResponse(201, {
          id: 'I-SUBSCRIPTION-1',
          status: 'APPROVAL_PENDING',
          plan_id: 'plan-pro',
          custom_id: '2026-07',
        })
      }
      if (url.endsWith('/v1/billing/subscriptions/I-SUBSCRIPTION-1')) {
        return jsonResponse(200, {
          id: 'I-SUBSCRIPTION-1',
          status: 'ACTIVE',
          plan_id: 'plan-pro',
          custom_id: '2026-07',
        })
      }
      throw new Error(`unexpected fetch ${url}`)
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    const created = await provider.createSubscription({
      idempotencyKey: 'idem-1',
      planId: 'plan-pro',
      currentPeriod: '2026-07',
    })
    const loaded = await provider.getSubscription(created.id)

    expect(created).toEqual({
      id: 'I-SUBSCRIPTION-1',
      status: 'APPROVAL_PENDING',
      currentPeriod: '2026-07',
      planId: 'plan-pro',
    })
    expect(loaded).toEqual({
      id: 'I-SUBSCRIPTION-1',
      status: 'ACTIVE',
      currentPeriod: '2026-07',
      planId: 'plan-pro',
    })
  })

  it('rejects invalid PayPal subscription periods and unknown statuses', async () => {
    const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
      const url = String(input)
      if (url.endsWith('/v1/oauth2/token')) {
        return jsonResponse(200, { access_token: 'access-token' })
      }
      if (url.endsWith('/v1/billing/subscriptions/I-BAD-PERIOD')) {
        return jsonResponse(200, {
          id: 'I-BAD-PERIOD',
          status: 'ACTIVE',
          plan_id: 'plan-pro',
          custom_id: 'tenant:2026-07',
        })
      }
      if (url.endsWith('/v1/billing/subscriptions/I-BAD-STATUS')) {
        return jsonResponse(200, {
          id: 'I-BAD-STATUS',
          status: 'SOMETHING_NEW',
          plan_id: 'plan-pro',
          custom_id: '2026-07',
        })
      }
      throw new Error(`unexpected fetch ${url}`)
    }) as unknown as typeof fetch
    const provider = createProvider(fetchMock)

    await expect(provider.getSubscription('I-BAD-PERIOD')).rejects.toThrow(/period/i)
    await expect(provider.getSubscription('I-BAD-STATUS')).rejects.toThrow(/status/i)
  })
})
