import { createServer } from 'node:http'

import { afterEach, expect, mock, test } from 'bun:test'

import { CodexOAuthService } from './codexOAuth.js'

const originalFetch = globalThis.fetch
const originalCallbackPort = process.env.CODEX_OAUTH_CALLBACK_PORT
const originalClientId = process.env.CODEX_OAUTH_CLIENT_ID

afterEach(() => {
  mock.restore()
  globalThis.fetch = originalFetch

  if (originalCallbackPort === undefined) {
    delete process.env.CODEX_OAUTH_CALLBACK_PORT
  } else {
    process.env.CODEX_OAUTH_CALLBACK_PORT = originalCallbackPort
  }

  if (originalClientId === undefined) {
    delete process.env.CODEX_OAUTH_CLIENT_ID
  } else {
    process.env.CODEX_OAUTH_CLIENT_ID = originalClientId
  }
})

async function getFreePort(): Promise<number> {
  return await new Promise((resolve, reject) => {
    const server = createServer()

    server.once('error', reject)
    server.listen(0, '127.0.0.1', () => {
      const address = server.address()
      if (!address || typeof address === 'string') {
        server.close(() => reject(new Error('Failed to allocate test port.')))
        return
      }

      const { port } = address
      server.close(error => {
        if (error) {
          reject(error)
          return
        }
        resolve(port)
      })
    })
  })
}

function buildCallbackRequest(authUrl: string): string {
  const authorizeUrl = new URL(authUrl)
  const redirectUri = authorizeUrl.searchParams.get('redirect_uri')
  const state = authorizeUrl.searchParams.get('state')

  if (!redirectUri || !state) {
    throw new Error('Codex OAuth test did not receive a valid authorization URL.')
  }

  const callbackUrl = new URL(redirectUri)
  callbackUrl.searchParams.set('code', 'auth-code')
  callbackUrl.searchParams.set('state', state)
  return callbackUrl.toString()
}

test('serves updated success copy after a successful Codex OAuth flow', async () => {
  const callbackPort = await getFreePort()
  process.env.CODEX_OAUTH_CALLBACK_PORT = String(callbackPort)
  process.env.CODEX_OAUTH_CLIENT_ID = 'test-client-id'

  globalThis.fetch = mock(async (input, init) => {
    const url = String(input)
    if (url.startsWith('http://localhost:')) {
      return originalFetch(input, init)
    }

    return new Response(
      JSON.stringify({
        access_token: 'access-token',
        refresh_token: 'refresh-token',
      }),
      {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
      },
    )
  }) as typeof fetch

  const service = new CodexOAuthService()
  let callbackResponsePromise!: Promise<Response>

  const flowPromise = service.startOAuthFlow(async authUrl => {
    callbackResponsePromise = originalFetch(buildCallbackRequest(authUrl))
  })

  const tokens = await flowPromise
  const callbackResponse = await callbackResponsePromise
  const html = await callbackResponse.text()

  expect(tokens.accessToken).toBe('access-token')
  expect(tokens.refreshToken).toBe('refresh-token')
  expect(html).toContain('You can return to OpenClaude now.')
  expect(html).toContain(
    'OpenClaude will finish activating your new Codex OAuth login.',
  )
  expect(html).not.toContain('continue automatically')
})

test('cancellation during token exchange returns a cancelled page and rejects the flow', async () => {
  const callbackPort = await getFreePort()
  process.env.CODEX_OAUTH_CALLBACK_PORT = String(callbackPort)
  process.env.CODEX_OAUTH_CLIENT_ID = 'test-client-id'

  let resolveFetchStart!: () => void
  const fetchStarted = new Promise<void>(resolve => {
    resolveFetchStart = resolve
  })

  globalThis.fetch = mock((input, init) => {
    const url = String(input)
    if (url.startsWith('http://localhost:')) {
      return originalFetch(input, init)
    }

    return new Promise<Response>((_resolve, reject) => {
      resolveFetchStart()

      const signal = init?.signal
      if (!signal) {
        return
      }

      if (signal.aborted) {
        reject(signal.reason)
        return
      }

      signal.addEventListener(
        'abort',
        () => {
          reject(signal.reason)
        },
        { once: true },
      )
    })
  }) as typeof fetch

  const service = new CodexOAuthService()
  let callbackResponsePromise!: Promise<Response>

  const flowPromise = service.startOAuthFlow(async authUrl => {
    callbackResponsePromise = originalFetch(buildCallbackRequest(authUrl))
  })

  await fetchStarted
  service.cleanup()

  await expect(flowPromise).rejects.toThrow('Codex OAuth flow was cancelled.')

  const callbackResponse = await callbackResponsePromise
  const html = await callbackResponse.text()

  expect(html).toContain('Codex login cancelled')
  expect(html).toContain('retry in OpenClaude')
})
