import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { setActivePinia, createPinia } from 'pinia'

import AuthCallbackPage from './AuthCallbackPage.vue'
import { useAuthStore } from '@/stores/auth'

// Mock the API client
vi.mock('@/api/client', () => ({
  apiClient: {
    get: vi.fn(),
  },
}))

import { apiClient } from '@/api/client'

describe('AuthCallbackPage', () => {
  let router: ReturnType<typeof createRouter>
  let mockLocation: { hash: string; pathname: string }

  beforeEach(() => {
    setActivePinia(createPinia())

    // Mock window.location
    mockLocation = { hash: '', pathname: '/auth/callback' }
    Object.defineProperty(window, 'location', {
      value: mockLocation,
      writable: true,
    })

    // Mock history.replaceState
    vi.spyOn(window.history, 'replaceState').mockImplementation(() => {})

    // Reset mocks
    vi.mocked(apiClient.get).mockReset()

    router = createRouter({
      history: createWebHistory(),
      routes: [
        { path: '/auth/callback', name: 'AuthCallback', component: AuthCallbackPage },
        { path: '/login', name: 'Login', component: { template: '<div>Login</div>' } },
        { path: '/', name: 'Dashboard', component: { template: '<div>Dashboard</div>' } },
        { path: '/starter', name: 'StarterSelection', component: { template: '<div>Starter</div>' } },
      ],
    })
  })

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

  describe('token parsing', () => {
    it('extracts tokens from URL hash fragment', async () => {
      /**
       * Test that tokens are correctly parsed from the URL hash.
       *
       * The OAuth provider redirects back with tokens in the fragment
       * (after #) for security. We must parse access_token, refresh_token,
       * and expires_in from this fragment.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Test User',
        avatar_url: null,
        has_starter_deck: true,
      })

      await router.push('/auth/callback')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      const auth = useAuthStore()
      expect(auth.accessToken).toBe('abc123')
      expect(auth.refreshToken).toBe('xyz789')
      expect(auth.expiresAt).toBeGreaterThan(Date.now())
    })

    it('shows error when tokens are missing', async () => {
      /**
       * Test error handling for malformed OAuth response.
       *
       * If the URL fragment is missing required tokens, we should
       * show an error rather than crash or behave unexpectedly.
       */
      mockLocation.hash = '#access_token=abc123' // Missing refresh_token and expires_in

      await router.push('/auth/callback')
      await router.isReady()

      const wrapper = mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(wrapper.text()).toContain('Authentication Failed')
      expect(wrapper.text()).toContain('Missing tokens')
    })

    it('shows error when hash is empty', async () => {
      /**
       * Test error handling when no hash fragment is present.
       *
       * If the OAuth provider redirects without tokens (empty hash),
       * we should show an appropriate error.
       */
      mockLocation.hash = ''

      await router.push('/auth/callback')
      await router.isReady()

      const wrapper = mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(wrapper.text()).toContain('Authentication Failed')
    })
  })

  describe('error handling from OAuth', () => {
    it('redirects to login with error from query params', async () => {
      /**
       * Test that OAuth errors are forwarded to the login page.
       *
       * When OAuth fails, the backend redirects to callback with
       * error details in query params. We should forward these
       * to the login page for display.
       */
      await router.push('/auth/callback?error=access_denied&message=User%20cancelled')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(router.currentRoute.value.name).toBe('Login')
      expect(router.currentRoute.value.query.error).toBe('access_denied')
      expect(router.currentRoute.value.query.message).toBe('User cancelled')
    })

    it('uses default error message when message param is missing', async () => {
      /**
       * Test fallback error message.
       *
       * If the backend only provides an error code without a message,
       * we should use a sensible default message.
       */
      await router.push('/auth/callback?error=unknown_error')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(router.currentRoute.value.name).toBe('Login')
      expect(router.currentRoute.value.query.message).toBe('Authentication failed. Please try again.')
    })
  })

  describe('user profile handling', () => {
    it('fetches user profile after storing tokens', async () => {
      /**
       * Test that user profile is fetched after successful token storage.
       *
       * After storing tokens, we need to fetch the user's profile
       * to know their display name, avatar, and starter deck status.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Test User',
        avatar_url: 'https://example.com/avatar.png',
        has_starter_deck: true,
      })

      await router.push('/auth/callback')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(apiClient.get).toHaveBeenCalledWith('/api/users/me')

      const auth = useAuthStore()
      expect(auth.user).toEqual({
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: 'https://example.com/avatar.png',
        hasStarterDeck: true,
      })
    })

    it('redirects to starter selection if user has no starter deck', async () => {
      /**
       * Test redirect flow for new users without starter decks.
       *
       * New users must select a starter deck before accessing the main app.
       * If has_starter_deck is false, redirect to the selection page.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'New User',
        avatar_url: null,
        has_starter_deck: false,
      })

      await router.push('/auth/callback')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(router.currentRoute.value.name).toBe('StarterSelection')
    })

    it('redirects to dashboard if user has starter deck', async () => {
      /**
       * Test redirect flow for existing users with starter decks.
       *
       * Users who already have a starter deck should go directly
       * to the dashboard after successful login.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Existing User',
        avatar_url: null,
        has_starter_deck: true,
      })

      await router.push('/auth/callback')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(router.currentRoute.value.name).toBe('Dashboard')
    })

    it('redirects to intended destination after login', async () => {
      /**
       * Test redirect to originally requested page.
       *
       * If a user was redirected to login while trying to access
       * a protected route, they should return there after auth.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Test User',
        avatar_url: null,
        has_starter_deck: true,
      })

      await router.push('/auth/callback?redirect=/decks')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(router.currentRoute.value.path).toBe('/decks')
    })
  })

  describe('API errors', () => {
    it('shows error when profile fetch fails', async () => {
      /**
       * Test error handling for profile fetch failures.
       *
       * If we successfully get tokens but fail to fetch the profile
       * (network error, server error, etc.), show an error state.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'))

      await router.push('/auth/callback')
      await router.isReady()

      const wrapper = mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(wrapper.text()).toContain('Authentication Failed')
      expect(wrapper.text()).toContain('Network error')
    })
  })

  describe('security', () => {
    it('rejects protocol-relative redirect URLs', async () => {
      /**
       * Test that protocol-relative URLs are rejected as redirects.
       *
       * An attacker could try to use //evil.com as a redirect, which
       * passes a naive startsWith('/') check but redirects to another domain.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Test User',
        avatar_url: null,
        has_starter_deck: true,
      })

      await router.push('/auth/callback?redirect=//evil.com')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      // Should go to dashboard, not the malicious redirect
      expect(router.currentRoute.value.name).toBe('Dashboard')
    })

    it('clears tokens from URL after parsing', async () => {
      /**
       * Test that tokens are removed from browser history.
       *
       * Tokens in the URL should be cleared after parsing to prevent
       * them from appearing in browser history or being logged.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      vi.mocked(apiClient.get).mockResolvedValue({
        id: 'user-1',
        display_name: 'Test User',
        avatar_url: null,
        has_starter_deck: true,
      })

      await router.push('/auth/callback')
      await router.isReady()

      mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      expect(window.history.replaceState).toHaveBeenCalledWith(
        null,
        '',
        '/auth/callback'
      )
    })
  })

  describe('UI states', () => {
    it('shows processing state while handling callback', async () => {
      /**
       * Test loading state during authentication.
       *
       * Users should see a loading indicator while tokens are being
       * processed and profile is being fetched.
       */
      mockLocation.hash = '#access_token=abc123&refresh_token=xyz789&expires_in=3600'

      // Don't resolve the promise immediately
      vi.mocked(apiClient.get).mockImplementation(() => new Promise(() => {}))

      await router.push('/auth/callback')
      await router.isReady()

      const wrapper = mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      expect(wrapper.text()).toContain('Completing login')
      expect(wrapper.text()).toContain('Please wait')
    })

    it('shows back to login button on error', async () => {
      /**
       * Test error recovery UI.
       *
       * When authentication fails, users should have a clear
       * way to return to the login page and try again.
       */
      mockLocation.hash = '' // No tokens = error

      await router.push('/auth/callback')
      await router.isReady()

      const wrapper = mount(AuthCallbackPage, {
        global: { plugins: [router] },
      })

      await flushPromises()

      const button = wrapper.find('button')
      expect(button.exists()).toBe(true)
      expect(button.text()).toContain('Back to Login')

      await button.trigger('click')
      await flushPromises()

      expect(router.currentRoute.value.name).toBe('Login')
    })
  })
})
