import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'

import { useAuthStore } from '@/stores/auth'

// Create mock router instance
const mockRouter = {
  push: vi.fn(),
  currentRoute: { value: { name: 'StarterSelection', path: '/starter' } },
}

// Mock vue-router (hoisted)
vi.mock('vue-router', () => ({
  useRouter: () => mockRouter,
  useRoute: () => ({ query: {} }),
}))

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

// Store isDev state in a way that can be modified between tests
let isDevMode = true

// Mock config with a getter for isDev so we can control it
vi.mock('@/config', () => ({
  config: {
    apiBaseUrl: 'http://localhost:8000',
    wsUrl: 'http://localhost:8000',
    oauthRedirectUri: 'http://localhost:5173/auth/callback',
    get isDev() { return isDevMode },
    isProd: false,
  },
}))

import { apiClient } from '@/api/client'
import { ApiError } from '@/api/types'
import { useStarter, STARTER_DECKS, type StarterType } from './useStarter'

describe('useStarter', () => {
  beforeEach(() => {
    setActivePinia(createPinia())

    // Reset mock router
    mockRouter.push.mockReset()

    // Reset API mock
    vi.mocked(apiClient.post).mockReset()

    // Set up authenticated user in auth store
    const authStore = useAuthStore()
    authStore.setTokens({
      accessToken: 'test-token',
      refreshToken: 'test-refresh',
      expiresAt: Date.now() + 3600000,
    })
    authStore.setUser({
      id: 'user-1',
      displayName: 'Test User',
      avatarUrl: null,
      hasStarterDeck: false,
    })

    // Set DEV mode for mock fallback tests
    isDevMode = true
  })

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

  describe('STARTER_DECKS constant', () => {
    it('contains exactly 5 starter deck options', () => {
      /**
       * Test that all 5 starter types are defined.
       *
       * The game offers exactly 5 starter deck types corresponding
       * to the main Pokemon types available at launch.
       */
      expect(STARTER_DECKS).toHaveLength(5)
    })

    it('includes grass, fire, water, psychic, and lightning types', () => {
      /**
       * Test that all expected starter types are present.
       *
       * These specific types were chosen to provide variety in
       * playstyle: healing (grass), aggro (fire), control (water),
       * status (psychic), and speed (lightning).
       */
      const types = STARTER_DECKS.map(d => d.type)
      expect(types).toContain('grass')
      expect(types).toContain('fire')
      expect(types).toContain('water')
      expect(types).toContain('psychic')
      expect(types).toContain('lightning')
    })

    it('each deck has required properties', () => {
      /**
       * Test starter deck data structure.
       *
       * Each starter deck must have a type, name, description,
       * and card count for display in the selection UI.
       */
      for (const deck of STARTER_DECKS) {
        expect(deck.type).toBeDefined()
        expect(deck.name).toBeDefined()
        expect(deck.description).toBeDefined()
        expect(deck.cardCount).toBe(40)
      }
    })
  })

  describe('initial state', () => {
    it('starts with isLoading as false', () => {
      /**
       * Test initial loading state.
       *
       * The composable should not be in a loading state until
       * a selection operation is initiated.
       */
      const { isLoading } = useStarter()

      expect(isLoading.value).toBe(false)
    })

    it('starts with no error', () => {
      /**
       * Test initial error state.
       *
       * There should be no error until an operation fails.
       */
      const { error } = useStarter()

      expect(error.value).toBeNull()
    })

    it('starts with no selected type', () => {
      /**
       * Test initial selection state.
       *
       * No deck type should be selected until the user picks one.
       */
      const { selectedType } = useStarter()

      expect(selectedType.value).toBeNull()
    })

    it('provides starterDecks constant', () => {
      /**
       * Test that starter decks are exposed.
       *
       * Components need access to the deck options for rendering.
       */
      const { starterDecks } = useStarter()

      expect(starterDecks).toBe(STARTER_DECKS)
    })

    it('hasStarterDeck reflects auth store user state', () => {
      /**
       * Test hasStarterDeck computed from auth store.
       *
       * The composable should expose the user's starter deck status
       * from the auth store for conditional rendering.
       */
      const authStore = useAuthStore()
      const { hasStarterDeck } = useStarter()

      expect(hasStarterDeck.value).toBe(false)

      authStore.setUser({
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: null,
        hasStarterDeck: true,
      })

      expect(hasStarterDeck.value).toBe(true)
    })
  })

  describe('selectStarter - success', () => {
    it('calls API with correct starter type', async () => {
      /**
       * Test API call payload.
       *
       * The API must receive the correct starter_type to assign
       * the right deck to the user.
       */
      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Forest Guardians',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const { selectStarter } = useStarter()
      await selectStarter('grass')

      expect(apiClient.post).toHaveBeenCalledWith('/api/users/me/starter-deck', {
        starter_type: 'grass',
      })
    })

    it('updates auth store hasStarterDeck on success', async () => {
      /**
       * Test auth store update on success.
       *
       * After selecting a starter deck, the auth store must be updated
       * so navigation guards and other components know the user has a deck.
       */
      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Flame Warriors',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const authStore = useAuthStore()
      const { selectStarter } = useStarter()

      expect(authStore.user?.hasStarterDeck).toBe(false)

      await selectStarter('fire')

      expect(authStore.user?.hasStarterDeck).toBe(true)
    })

    it('redirects to dashboard on success', async () => {
      /**
       * Test navigation after successful selection.
       *
       * After selecting a starter deck, the user should be taken
       * to the main dashboard to begin playing.
       */
      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Tidal Force',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const { selectStarter } = useStarter()
      await selectStarter('water')

      expect(mockRouter.push).toHaveBeenCalledWith({ name: 'Dashboard' })
    })

    it('returns success result with deck', async () => {
      /**
       * Test success result structure.
       *
       * The result should include the deck data for potential
       * display or further processing.
       */
      const mockDeck = {
        id: 'deck-1',
        name: 'Mind Masters',
        cards: [],
        isValid: true,
        cardCount: 40,
      }

      vi.mocked(apiClient.post).mockResolvedValue(mockDeck)

      const { selectStarter } = useStarter()
      const result = await selectStarter('psychic')

      expect(result.success).toBe(true)
      expect(result.deck).toEqual(mockDeck)
      expect(result.error).toBeUndefined()
    })

    it('sets selectedType during operation', async () => {
      /**
       * Test selected type tracking.
       *
       * The selectedType should be set during the operation so
       * the UI can show which deck is being selected (e.g., loading indicator).
       */
      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Storm Riders',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const { selectStarter, selectedType } = useStarter()
      await selectStarter('lightning')

      // After completion, selectedType should still reflect what was selected
      expect(selectedType.value).toBe('lightning')
    })
  })

  describe('selectStarter - error handling', () => {
    it('handles 409 conflict (already has starter)', async () => {
      /**
       * Test handling of "already has starter" error.
       *
       * If the user somehow tries to select a starter when they already
       * have one, the API returns 409. We should handle this gracefully
       * by updating state and redirecting anyway.
       */
      const conflictError = new ApiError(409, 'Conflict', 'User already has starter deck', 'ALREADY_HAS_STARTER')
      vi.mocked(apiClient.post).mockRejectedValue(conflictError)

      const authStore = useAuthStore()
      const { selectStarter } = useStarter()
      const result = await selectStarter('grass')

      // Should still count as "success" in terms of outcome
      expect(result.success).toBe(true)
      expect(authStore.user?.hasStarterDeck).toBe(true)
      expect(mockRouter.push).toHaveBeenCalledWith({ name: 'Dashboard' })
    })

    it('sets error state on API failure', async () => {
      /**
       * Test error state on API failure.
       *
       * When the API returns an error (other than 409), we should
       * set the error state so the UI can display it.
       */
      const serverError = new ApiError(500, 'Internal Server Error', 'Database error')
      vi.mocked(apiClient.post).mockRejectedValue(serverError)

      // Disable dev mode for this test
      isDevMode = false

      const { selectStarter, error } = useStarter()
      await selectStarter('fire')

      expect(error.value).toBe('Database error')
    })

    it('returns error result on failure', async () => {
      /**
       * Test error result structure.
       *
       * The result should indicate failure and include the error
       * message for display.
       */
      const serverError = new ApiError(422, 'Unprocessable Entity', 'Invalid starter type')
      vi.mocked(apiClient.post).mockRejectedValue(serverError)

      // Disable dev mode for this test
      isDevMode = false

      const { selectStarter } = useStarter()
      const result = await selectStarter('water')

      expect(result.success).toBe(false)
      expect(result.error).toBe('Invalid starter type')
      expect(result.deck).toBeUndefined()
    })

    it('does not redirect on failure', async () => {
      /**
       * Test no navigation on failure.
       *
       * When selection fails, the user should stay on the selection
       * page so they can try again or see the error.
       */
      const serverError = new ApiError(500, 'Internal Server Error', 'Server down')
      vi.mocked(apiClient.post).mockRejectedValue(serverError)

      // Disable dev mode for this test
      isDevMode = false

      const { selectStarter } = useStarter()
      await selectStarter('psychic')

      expect(mockRouter.push).not.toHaveBeenCalled()
    })

    it('handles unknown errors', async () => {
      /**
       * Test handling of non-ApiError exceptions.
       *
       * Network errors and other unexpected errors should be
       * handled gracefully with a generic message.
       */
      vi.mocked(apiClient.post).mockRejectedValue(new Error('Network failed'))

      const { selectStarter, error } = useStarter()
      const result = await selectStarter('lightning')

      expect(result.success).toBe(false)
      expect(error.value).toBe('Network failed')
    })
  })

  describe('selectStarter - dev mock fallback', () => {
    it('uses mock when API returns 404 in dev mode', async () => {
      /**
       * Test dev mock fallback on 404.
       *
       * When the backend endpoint doesn't exist yet (404), we should
       * fall back to a mock response in development mode to allow
       * testing the full UI flow.
       */
      const notFoundError = new ApiError(404, 'Not Found', 'Endpoint not found')
      vi.mocked(apiClient.post).mockRejectedValue(notFoundError)

      const authStore = useAuthStore()
      const { selectStarter } = useStarter()
      const result = await selectStarter('grass')

      expect(result.success).toBe(true)
      expect(result.wasMocked).toBe(true)
      expect(result.deck?.name).toBe('Forest Guardians')
      expect(authStore.user?.hasStarterDeck).toBe(true)
      expect(mockRouter.push).toHaveBeenCalledWith({ name: 'Dashboard' })
    })

    it('uses mock when API returns 500 in dev mode', async () => {
      /**
       * Test dev mock fallback on server error.
       *
       * Server errors in dev mode should also trigger the mock
       * since the backend may be unstable during development.
       */
      const serverError = new ApiError(500, 'Internal Server Error', 'Database not configured')
      vi.mocked(apiClient.post).mockRejectedValue(serverError)

      const { selectStarter } = useStarter()
      const result = await selectStarter('fire')

      expect(result.success).toBe(true)
      expect(result.wasMocked).toBe(true)
      expect(result.deck?.name).toBe('Flame Warriors')
    })

    it('does not use mock in production mode', async () => {
      /**
       * Test no mock in production.
       *
       * The mock fallback should ONLY work in development mode.
       * Production errors should be reported to the user.
       */
      isDevMode = false

      const serverError = new ApiError(500, 'Internal Server Error', 'Server error')
      vi.mocked(apiClient.post).mockRejectedValue(serverError)

      const { selectStarter, error } = useStarter()
      const result = await selectStarter('water')

      expect(result.success).toBe(false)
      expect(result.wasMocked).toBeUndefined()
      expect(error.value).toBe('Server error')
    })

    it('creates mock deck with correct type', async () => {
      /**
       * Test mock deck generation.
       *
       * The mock deck should match the selected type so the UI
       * can display the correct deck name and theme.
       */
      const notFoundError = new ApiError(404, 'Not Found')
      vi.mocked(apiClient.post).mockRejectedValue(notFoundError)

      const { selectStarter } = useStarter()

      const result = await selectStarter('lightning')

      expect(result.deck?.name).toBe('Storm Riders')
      expect(result.deck?.id).toContain('lightning')
    })

    it('logs warning when using mock', async () => {
      /**
       * Test console warning for mock usage.
       *
       * Developers should be warned when the mock is being used
       * so they know the real API isn't being hit.
       */
      const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

      const notFoundError = new ApiError(404, 'Not Found')
      vi.mocked(apiClient.post).mockRejectedValue(notFoundError)

      const { selectStarter } = useStarter()
      await selectStarter('psychic')

      expect(warnSpy).toHaveBeenCalledWith('[useStarter] API not available, using mock response')
    })
  })

  describe('loading states', () => {
    it('isLoading is true during selectStarter', async () => {
      /**
       * Test loading state during selection.
       *
       * Components should show loading indicators while the
       * selection is being processed.
       */
      let resolveApi: (value: unknown) => void
      vi.mocked(apiClient.post).mockImplementation(
        () => new Promise((resolve) => { resolveApi = resolve })
      )

      const { selectStarter, isLoading } = useStarter()

      // Start selection (don't await)
      const promise = selectStarter('grass')

      // Should be loading
      expect(isLoading.value).toBe(true)

      // Resolve the API call
      resolveApi!({
        id: 'deck-1',
        name: 'Forest Guardians',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      await promise

      // Should no longer be loading
      expect(isLoading.value).toBe(false)
    })

    it('isLoading is false after error', async () => {
      /**
       * Test loading state cleared on error.
       *
       * Even if the operation fails, loading should be set to false
       * so the user can try again.
       */
      // Disable dev mode so error isn't caught by mock
      isDevMode = false

      vi.mocked(apiClient.post).mockRejectedValue(new Error('Failed'))

      const { selectStarter, isLoading } = useStarter()
      await selectStarter('fire')

      expect(isLoading.value).toBe(false)
    })
  })

  describe('clearError', () => {
    it('clears the error state', async () => {
      /**
       * Test error clearing.
       *
       * After displaying an error, users may want to dismiss it
       * before trying again.
       */
      // Disable dev mode so error isn't caught by mock
      isDevMode = false

      vi.mocked(apiClient.post).mockRejectedValue(new Error('Some error'))

      const { selectStarter, error, clearError } = useStarter()
      await selectStarter('water')

      expect(error.value).toBe('Some error')

      clearError()

      expect(error.value).toBeNull()
    })
  })

  describe('edge cases', () => {
    it('handles user being null in auth store', async () => {
      /**
       * Test handling of missing user.
       *
       * If somehow the user is null (shouldn't happen on starter page),
       * the operation should still work without crashing.
       */
      const authStore = useAuthStore()
      authStore.setUser(null as unknown as Parameters<typeof authStore.setUser>[0])

      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Mind Masters',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const { selectStarter } = useStarter()
      const result = await selectStarter('psychic')

      // Should succeed without throwing
      expect(result.success).toBe(true)
    })

    it('all 5 starter types can be selected', async () => {
      /**
       * Test all starter types work.
       *
       * Each of the 5 starter types should be selectable without
       * any type errors or special handling issues.
       */
      vi.mocked(apiClient.post).mockResolvedValue({
        id: 'deck-1',
        name: 'Test Deck',
        cards: [],
        isValid: true,
        cardCount: 40,
      })

      const starterTypes: StarterType[] = ['grass', 'fire', 'water', 'psychic', 'lightning']

      for (const type of starterTypes) {
        const { selectStarter } = useStarter()
        const result = await selectStarter(type)
        expect(result.success).toBe(true)
      }
    })
  })
})
