import { createHash } from 'node:crypto'
import { describe, expect, it } from 'vitest'

import {
  CODEX_AUTHORIZE_URL,
  CODEX_CLIENT_ID,
  CODEX_REDIRECT_URI,
  CODEX_SCOPES,
  createAuthorizationFlow,
  decodeJwtPayload,
  needsRefresh,
  refreshTokenLikelyExpired,
} from '../codexOAuth.js'

describe('createAuthorizationFlow', () => {
  it('generates a PKCE verifier between 43 and 128 chars', () => {
    const flow = createAuthorizationFlow()
    expect(flow.verifier.length).toBeGreaterThanOrEqual(43)
    expect(flow.verifier.length).toBeLessThanOrEqual(128)
  })

  it('verifier is base64url (no +, /, =)', () => {
    const flow = createAuthorizationFlow()
    expect(flow.verifier).toMatch(/^[A-Za-z0-9\-_]+$/)
  })

  it('challenge is the S256 hash of the verifier, base64url-encoded', () => {
    const flow = createAuthorizationFlow()
    const expected = createHash('sha256')
      .update(flow.verifier)
      .digest('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')
    expect(flow.challenge).toBe(expected)
  })

  it('url points at the Codex authorize endpoint with required params', () => {
    const flow = createAuthorizationFlow()
    expect(flow.url.startsWith(CODEX_AUTHORIZE_URL)).toBe(true)
    const url = new URL(flow.url)
    expect(url.searchParams.get('client_id')).toBe(CODEX_CLIENT_ID)
    expect(url.searchParams.get('redirect_uri')).toBe(CODEX_REDIRECT_URI)
    expect(url.searchParams.get('scope')).toBe(CODEX_SCOPES)
    expect(url.searchParams.get('code_challenge_method')).toBe('S256')
    expect(url.searchParams.get('code_challenge')).toBe(flow.challenge)
    expect(url.searchParams.get('state')).toBe(flow.state)
    expect(url.searchParams.get('codex_cli_simplified_flow')).toBe('true')
  })

  it('state and verifier are unique across calls', () => {
    const a = createAuthorizationFlow()
    const b = createAuthorizationFlow()
    expect(a.verifier).not.toBe(b.verifier)
    expect(a.state).not.toBe(b.state)
  })
})

describe('decodeJwtPayload', () => {
  it('returns null for non-JWT strings', () => {
    expect(decodeJwtPayload('not-a-jwt')).toBeNull()
  })

  it('decodes a base64url JWT payload', () => {
    const payload = {
      'https://api.openai.com/auth': { chatgpt_account_id: 'acct_123' },
    }
    const encoded = Buffer.from(JSON.stringify(payload))
      .toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')
    const jwt = `h.${encoded}.sig`
    const decoded = decodeJwtPayload(jwt)
    expect(decoded).toEqual(payload)
  })
})

describe('refresh timing helpers', () => {
  const baseTokens = {
    access_token: 'a',
    refresh_token: 'r',
    account_id: null,
    expires_at: 0,
    token_type: 'Bearer' as const,
    issued_at: 0,
    last_refresh: 0,
  }

  it('needsRefresh true when within 60s of expiry', () => {
    const now = Date.now()
    expect(needsRefresh({ ...baseTokens, expires_at: now + 30_000 }, now)).toBe(true)
  })

  it('needsRefresh false when fresh', () => {
    const now = Date.now()
    expect(needsRefresh({ ...baseTokens, expires_at: now + 5 * 60_000 }, now)).toBe(false)
  })

  it('refreshTokenLikelyExpired after 27 days', () => {
    const now = Date.now()
    const old = now - 28 * 24 * 60 * 60 * 1000
    expect(refreshTokenLikelyExpired({ ...baseTokens, last_refresh: old }, now)).toBe(true)
  })
})
