const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')

const OAUTH_STATE_TTL_MS = 10 * 60 * 1000
const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000

describe('OAuth Integration', () => {
  describe('Google OAuth', () => {
    const GOOGLE_CLIENT_ID = 'test-google-client-id'
    const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
    const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
    const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo'

    function buildGoogleAuthUrl(redirectUri, state, scope = 'email profile') {
      const url = new URL(GOOGLE_AUTH_URL)
      url.searchParams.set('client_id', GOOGLE_CLIENT_ID)
      url.searchParams.set('redirect_uri', redirectUri)
      url.searchParams.set('response_type', 'code')
      url.searchParams.set('scope', scope)
      url.searchParams.set('state', state)
      return url.toString()
    }

    function generateOAuthState() {
      return crypto.randomBytes(32).toString('hex')
    }

    test('builds correct authorization URL', () => {
      const state = generateOAuthState()
      const redirectUri = 'https://example.com/auth/google/callback'
      const authUrl = buildGoogleAuthUrl(redirectUri, state)

      expect(authUrl).toContain('accounts.google.com')
      expect(authUrl).toContain('client_id=' + GOOGLE_CLIENT_ID)
      expect(authUrl).toContain('redirect_uri=' + encodeURIComponent(redirectUri))
      expect(authUrl).toContain('response_type=code')
      expect(authUrl).toContain('scope=email+profile')
      expect(authUrl).toContain('state=' + state)
    })

    test('generates secure state parameter', () => {
      const state = generateOAuthState()
      expect(state.length).toBe(64)
      expect(/^[a-f0-9]+$/.test(state)).toBe(true)
    })

    test('generates unique state parameters', () => {
      const states = new Set()
      for (let i = 0; i < 100; i++) {
        states.add(generateOAuthState())
      }
      expect(states.size).toBe(100)
    })

    test('includes correct scopes', () => {
      const authUrl = buildGoogleAuthUrl('https://example.com/callback', 'state')
      expect(authUrl).toContain('email')
      expect(authUrl).toContain('profile')
    })

    test('parses Google user info response', () => {
      const userInfo = {
        id: 'google-user-id-123',
        email: 'user@gmail.com',
        verified_email: true,
        name: 'Test User',
        given_name: 'Test',
        family_name: 'User',
        picture: 'https://example.com/photo.jpg',
      }

      expect(userInfo.id).toBeDefined()
      expect(userInfo.email).toBeDefined()
      expect(userInfo.verified_email).toBe(true)
    })
  })

  describe('GitHub OAuth', () => {
    const GITHUB_CLIENT_ID = 'test-github-client-id'
    const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'
    const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
    const GITHUB_USERINFO_URL = 'https://api.github.com/user'

    function buildGitHubAuthUrl(redirectUri, state, scope = 'user:email') {
      const url = new URL(GITHUB_AUTH_URL)
      url.searchParams.set('client_id', GITHUB_CLIENT_ID)
      url.searchParams.set('redirect_uri', redirectUri)
      url.searchParams.set('scope', scope)
      url.searchParams.set('state', state)
      return url.toString()
    }

    test('builds correct authorization URL', () => {
      const state = crypto.randomBytes(32).toString('hex')
      const redirectUri = 'https://example.com/auth/github/callback'
      const authUrl = buildGitHubAuthUrl(redirectUri, state)

      expect(authUrl).toContain('github.com')
      expect(authUrl).toContain('client_id=' + GITHUB_CLIENT_ID)
      expect(authUrl).toContain('redirect_uri=' + encodeURIComponent(redirectUri))
      expect(authUrl).toContain('scope=user%3Aemail')
      expect(authUrl).toContain('state=' + state)
    })

    test('parses GitHub user info response', () => {
      const userInfo = {
        id: 12345678,
        login: 'testuser',
        email: 'user@example.com',
        name: 'Test User',
        avatar_url: 'https://example.com/avatar.png',
        html_url: 'https://github.com/testuser',
      }

      expect(userInfo.id).toBeDefined()
      expect(userInfo.login).toBeDefined()
    })

    test('handles GitHub email endpoint for private emails', () => {
      const emails = [
        { email: 'public@example.com', primary: false, verified: true },
        { email: 'private@example.com', primary: true, verified: true },
      ]

      const primaryEmail = emails.find(e => e.primary)
      expect(primaryEmail.email).toBe('private@example.com')
    })
  })

  describe('OAuth State Management', () => {
    const oauthStateStore = new Map()

    function storeOAuthState(state, provider, data = {}) {
      oauthStateStore.set(state, {
        provider,
        ...data,
        createdAt: Date.now(),
        expiresAt: Date.now() + OAUTH_STATE_TTL_MS,
      })
    }

    function getOAuthState(state) {
      const stored = oauthStateStore.get(state)
      if (!stored) return null
      if (stored.expiresAt < Date.now()) {
        oauthStateStore.delete(state)
        return null
      }
      return stored
    }

    function deleteOAuthState(state) {
      oauthStateStore.delete(state)
    }

    beforeEach(() => {
      oauthStateStore.clear()
    })

    test('stores OAuth state with expiry', () => {
      const state = crypto.randomBytes(32).toString('hex')
      storeOAuthState(state, 'google', { redirectUri: 'https://example.com/callback' })

      const stored = getOAuthState(state)
      expect(stored).toBeDefined()
      expect(stored.provider).toBe('google')
      expect(stored.expiresAt).toBeGreaterThan(Date.now())
    })

    test('retrieves valid OAuth state', () => {
      const state = crypto.randomBytes(32).toString('hex')
      storeOAuthState(state, 'github')

      const retrieved = getOAuthState(state)
      expect(retrieved.provider).toBe('github')
    })

    test('returns null for expired state', () => {
      const state = crypto.randomBytes(32).toString('hex')
      oauthStateStore.set(state, {
        provider: 'google',
        createdAt: Date.now() - OAUTH_STATE_TTL_MS - 1000,
        expiresAt: Date.now() - 1000,
      })

      const retrieved = getOAuthState(state)
      expect(retrieved).toBeNull()
    })

    test('returns null for invalid state', () => {
      const retrieved = getOAuthState('invalid-state')
      expect(retrieved).toBeNull()
    })

    test('deletes OAuth state after use', () => {
      const state = crypto.randomBytes(32).toString('hex')
      storeOAuthState(state, 'google')
      deleteOAuthState(state)

      const retrieved = getOAuthState(state)
      expect(retrieved).toBeNull()
    })

    test('state expires after TTL', () => {
      expect(OAUTH_STATE_TTL_MS).toBe(10 * 60 * 1000)
    })
  })

  describe('OAuth Callback Handling', () => {
    function handleOAuthCallback(provider, code, state, storedState) {
      if (!storedState) {
        return { error: 'invalid_state', message: 'OAuth state not found or expired' }
      }

      if (!code) {
        return { error: 'missing_code', message: 'Authorization code is required' }
      }

      return {
        success: true,
        provider,
        code,
        state,
        redirectUri: storedState.redirectUri,
      }
    }

    test('handles valid callback', () => {
      const state = 'test-state'
      const storedState = { provider: 'google', redirectUri: 'https://example.com/callback' }
      const result = handleOAuthCallback('google', 'auth-code-123', state, storedState)

      expect(result.success).toBe(true)
      expect(result.provider).toBe('google')
      expect(result.code).toBe('auth-code-123')
    })

    test('rejects callback with invalid state', () => {
      const result = handleOAuthCallback('google', 'auth-code-123', 'invalid-state', null)

      expect(result.error).toBe('invalid_state')
    })

    test('rejects callback without code', () => {
      const storedState = { provider: 'github' }
      const result = handleOAuthCallback('github', null, 'state', storedState)

      expect(result.error).toBe('missing_code')
    })

    test('handles error response from provider', () => {
      const errorResponse = {
        error: 'access_denied',
        error_description: 'The user denied access to your application',
      }

      expect(errorResponse.error).toBe('access_denied')
    })
  })

  describe('Token Exchange', () => {
    function mockTokenResponse(provider, accessToken, refreshToken, expiresIn) {
      return {
        access_token: accessToken,
        refresh_token: refreshToken,
        token_type: 'Bearer',
        expires_in: expiresIn,
        scope: provider === 'google' ? 'email profile' : 'user:email',
      }
    }

    test('parses Google token response', () => {
      const response = mockTokenResponse('google', 'ya29.access-token', 'refresh-token', 3600)

      expect(response.access_token).toBe('ya29.access-token')
      expect(response.token_type).toBe('Bearer')
      expect(response.expires_in).toBe(3600)
    })

    test('parses GitHub token response', () => {
      const response = mockTokenResponse('github', 'gho.access-token', null, null)

      expect(response.access_token).toBe('gho.access-token')
    })

    test('validates token response structure', () => {
      function validateTokenResponse(response) {
        if (!response || typeof response !== 'object') return false
        if (!response.access_token) return false
        if (response.token_type && response.token_type !== 'Bearer') return false
        return true
      }

      expect(validateTokenResponse({ access_token: 'token' })).toBe(true)
      expect(validateTokenResponse({ access_token: 'token', token_type: 'Bearer' })).toBe(true)
      expect(validateTokenResponse({})).toBe(false)
      expect(validateTokenResponse(null)).toBe(false)
    })
  })

  describe('User Account Linking', () => {
    function linkOAuthProvider(user, provider, providerUserId, providerData) {
      const providers = user.providers || []
      if (!providers.includes(provider)) {
        providers.push(provider)
      }

      return {
        ...user,
        providers,
        [`${provider}Id`]: providerUserId,
        [`${provider}Data`]: providerData,
      }
    }

    test('links Google provider to existing user', () => {
      const user = { id: 'user-1', email: 'user@example.com', providers: [] }
      const updated = linkOAuthProvider(user, 'google', 'google-123', { name: 'Test User' })

      expect(updated.providers).toContain('google')
      expect(updated.googleId).toBe('google-123')
    })

    test('links GitHub provider to existing user', () => {
      const user = { id: 'user-1', email: 'user@example.com', providers: ['google'] }
      const updated = linkOAuthProvider(user, 'github', 'github-456', { login: 'testuser' })

      expect(updated.providers).toContain('google')
      expect(updated.providers).toContain('github')
      expect(updated.githubId).toBe('github-456')
    })

    test('does not duplicate provider in list', () => {
      const user = { id: 'user-1', providers: ['google'] }
      const updated = linkOAuthProvider(user, 'google', 'google-123', {})

      const googleCount = updated.providers.filter(p => p === 'google').length
      expect(googleCount).toBe(1)
    })
  })

  describe('Email Verification', () => {
    function createVerificationToken() {
      return crypto.randomBytes(32).toString('hex')
    }

    test('generates verification token', () => {
      const token = createVerificationToken()
      expect(token.length).toBe(64)
      expect(/^[a-f0-9]+$/.test(token)).toBe(true)
    })

    test('verification TTL is 24 hours', () => {
      expect(EMAIL_VERIFICATION_TTL_MS).toBe(24 * 60 * 60 * 1000)
    })

    test('validates verification token expiry', () => {
      const now = Date.now()
      const validExpiry = now + EMAIL_VERIFICATION_TTL_MS
      const expiredExpiry = now - 1000

      expect(validExpiry > now).toBe(true)
      expect(expiredExpiry < now).toBe(true)
    })
  })

  describe('Password Reset', () => {
    function createResetToken() {
      return crypto.randomBytes(32).toString('hex')
    }

    function validateResetToken(token, storedToken, expiresAt) {
      if (!token || !storedToken) return { valid: false, reason: 'invalid_token' }
      if (token !== storedToken) return { valid: false, reason: 'token_mismatch' }
      if (Date.now() > expiresAt) return { valid: false, reason: 'expired' }
      return { valid: true }
    }

    test('generates reset token', () => {
      const token = createResetToken()
      expect(token.length).toBe(64)
    })

    test('reset TTL is 1 hour', () => {
      expect(PASSWORD_RESET_TTL_MS).toBe(60 * 60 * 1000)
    })

    test('validates correct reset token', () => {
      const token = createResetToken()
      const expiresAt = Date.now() + PASSWORD_RESET_TTL_MS
      const result = validateResetToken(token, token, expiresAt)

      expect(result.valid).toBe(true)
    })

    test('rejects mismatched token', () => {
      const token = createResetToken()
      const storedToken = createResetToken()
      const expiresAt = Date.now() + PASSWORD_RESET_TTL_MS
      const result = validateResetToken(token, storedToken, expiresAt)

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('token_mismatch')
    })

    test('rejects expired token', () => {
      const token = createResetToken()
      const expiresAt = Date.now() - 1000
      const result = validateResetToken(token, token, expiresAt)

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('expired')
    })
  })
})
