#!/usr/bin/env node
/*
 * Mobile SSO bridge smoketest — DEV ONLY, pairs with stub-idp.js.
 *
 * Drives the full native-SSO flow the Android app performs, headless, so the
 * website bridge can be verified without an emulator/IdP:
 *   1. mint PKCE (Layer B) + state
 *   2. GET  /auth/mobile/sso/start        → 302 to the stub /authorize
 *   3. follow the stub picker (inject `login_as`) → 302 to the website callback
 *   4. website callback exchanges the IdP code server-side, resolves the linked
 *      user, and 302s to the app deep link  runicgateway://auth/callback?code&state
 *   5. POST /auth/mobile/sso/exchange { code, code_verifier } → the bearer pair
 *
 * Prereqs: stub-idp.js running, seed-sso-provider.js applied, website on :3000.
 *
 * Run:  node website/scripts/dev/sso-bridge-smoketest.js [stub-colby|stub-admin]
 * Env:  BASE_URL (default http://127.0.0.1:3000), REDIRECT_URI
 *       (default runicgateway://auth/callback)
 */

'use strict'

const crypto = require('crypto')

const BASE = (process.env.BASE_URL || 'http://127.0.0.1:3000').replace(/\/$/, '')
const REDIRECT = process.env.REDIRECT_URI || 'runicgateway://auth/callback'
const PROVIDER = process.env.PROVIDER_ID || 'devstub'
const LOGIN_AS = process.argv[2] || 'stub-colby'

const b64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')

// A minimal cookie jar: name → value, updated from every Set-Cookie.
const jar = {}
function storeCookies(res) {
  const raw = res.headers.getSetCookie ? res.headers.getSetCookie() : res.headers.raw?.()['set-cookie'] || []
  for (const c of raw) {
    const [pair] = c.split(';')
    const idx = pair.indexOf('=')
    if (idx > 0) jar[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim()
  }
}
const cookieHeader = () =>
  Object.entries(jar)
    .map(([k, v]) => `${k}=${v}`)
    .join('; ')

async function step(name, url, opts = {}) {
  const res = await fetch(url, { redirect: 'manual', headers: { Cookie: cookieHeader(), ...(opts.headers || {}) }, ...opts })
  storeCookies(res)
  const loc = res.headers.get('location')
  console.log(`\n[${name}] ${res.status} ${url.split('?')[0]}`)
  if (loc) console.log(`   → Location: ${loc}`)
  return { res, loc }
}

async function main() {
  const verifier = b64url(crypto.randomBytes(32))
  const challenge = b64url(crypto.createHash('sha256').update(verifier).digest())
  const state = b64url(crypto.randomBytes(16))
  console.log(`PKCE verifier=${verifier.slice(0, 12)}…  challenge=${challenge.slice(0, 12)}…  state=${state.slice(0, 12)}…  loginAs=${LOGIN_AS}`)

  // 2. start → 302 to stub /authorize
  const startUrl =
    `${BASE}/api/v1/auth/mobile/sso/start?provider=${PROVIDER}` +
    `&code_challenge=${encodeURIComponent(challenge)}&state=${encodeURIComponent(state)}` +
    `&redirect_uri=${encodeURIComponent(REDIRECT)}`
  let { loc } = await step('start', startUrl)
  if (!loc || !loc.includes('/authorize')) throw new Error('start did not redirect to the IdP authorize endpoint')

  // 3. stub authorize: inject the account choice the picker would make.
  const authUrl = new URL(loc)
  authUrl.searchParams.set('login_as', LOGIN_AS)
  ;({ loc } = await step('idp-authorize', authUrl.toString()))
  if (!loc || !loc.includes('/sso/')) throw new Error('IdP did not redirect back to the website callback')

  // 4. website callback: server-side token+userinfo, resolve user, deep-link back.
  ;({ loc } = await step('callback', loc))
  if (!loc) throw new Error('callback produced no redirect')
  const deep = new URL(loc)
  const appCode = deep.searchParams.get('code')
  const appState = deep.searchParams.get('state')
  const appErr = deep.searchParams.get('error')
  if (appErr) throw new Error(`callback returned error to app: ${appErr}`)
  if (!appCode) throw new Error(`callback did not deep-link a code (got ${loc})`)
  if (appState !== state) throw new Error(`state mismatch: sent ${state}, got ${appState}`)
  console.log(`   ✓ deep link carries code=${appCode.slice(0, 10)}… state matches`)

  // 5. exchange the app code + PKCE verifier for the bearer pair.
  const exRes = await fetch(`${BASE}/api/v1/auth/mobile/sso/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: appCode, code_verifier: verifier, device_name: 'sso-smoketest' }),
  })
  const exBody = await exRes.json().catch(() => ({}))
  console.log(`\n[exchange] ${exRes.status}`)
  if (!exRes.ok) throw new Error(`exchange failed: ${exRes.status} ${JSON.stringify(exBody)}`)
  const hasPair = exBody.accessToken && exBody.refreshToken
  console.log(`   user: ${JSON.stringify(exBody.user)}`)
  console.log(`   accessToken: ${exBody.accessToken ? exBody.accessToken.slice(0, 16) + '…' : '(none)'}`)
  console.log(`   refreshToken: ${exBody.refreshToken ? '(present)' : '(none)'}`)
  if (!hasPair) throw new Error('exchange did not return an access/refresh pair')

  console.log('\n✅ SSO bridge smoketest PASSED')
}

main().catch((err) => {
  console.error('\n❌ SSO bridge smoketest FAILED:', err.message)
  process.exitCode = 1
})
