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

import { useUserStore } from '@/stores/user'

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

// Mock the config
vi.mock('@/config', () => ({
  config: {
    apiBaseUrl: 'http://localhost:8000',
    wsUrl: 'http://localhost:8000',
    oauthRedirectUri: 'http://localhost:5173/auth/callback',
    isDev: true,
    isProd: false,
  },
}))

import { apiClient } from '@/api/client'
import { ApiError } from '@/api/types'
import { useProfile } from './useProfile'

describe('useProfile', () => {
  let mockLocation: { href: string }

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

    // Mock window.location for initiateLink
    mockLocation = {
      href: 'http://localhost:5173/profile',
    }
    Object.defineProperty(window, 'location', {
      value: { ...mockLocation, origin: 'http://localhost:5173' },
      writable: true,
      configurable: true,
    })

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

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

  describe('initial state', () => {
    it('starts with null profile', () => {
      /**
       * Test that useProfile starts with no profile data.
       *
       * Before fetchProfile is called, the profile should be null
       * indicating no data has been loaded yet.
       */
      const { profile, displayName, avatarUrl, linkedAccounts } = useProfile()

      expect(profile.value).toBeNull()
      expect(displayName.value).toBe('Unknown')
      expect(avatarUrl.value).toBeUndefined()
      expect(linkedAccounts.value).toEqual([])
    })

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

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

    it('starts with no error', () => {
      /**
       * Test initial error state.
       *
       * The composable should start with no errors.
       */
      const { error } = useProfile()

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

    it('starts with null session count', () => {
      /**
       * Test initial session count state.
       *
       * Session count should be null until fetchSessionCount is called.
       */
      const { sessionCount } = useProfile()

      expect(sessionCount.value).toBeNull()
    })
  })

  describe('fetchProfile', () => {
    it('delegates to user store fetchProfile', async () => {
      /**
       * Test that fetchProfile calls through to the user store.
       *
       * The composable wraps the store, so fetchProfile should
       * delegate to the store's implementation.
       */
      const userStore = useUserStore()
      const fetchSpy = vi.spyOn(userStore, 'fetchProfile').mockResolvedValue(true)

      const { fetchProfile } = useProfile()
      const result = await fetchProfile()

      expect(fetchSpy).toHaveBeenCalled()
      expect(result).toBe(true)
    })

    it('clears local error before fetching', async () => {
      /**
       * Test error clearing on fetch.
       *
       * Starting a new fetch should clear any previous errors
       * so users don't see stale error messages.
       */
      const userStore = useUserStore()
      vi.spyOn(userStore, 'fetchProfile').mockResolvedValue(true)

      const { fetchProfile, error } = useProfile()
      await fetchProfile()

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

  describe('updateDisplayName', () => {
    it('successfully updates display name', async () => {
      /**
       * Test successful display name update.
       *
       * When the user updates their display name, the composable
       * should call the store and return success.
       */
      const userStore = useUserStore()
      vi.spyOn(userStore, 'updateDisplayName').mockResolvedValue(true)

      const { updateDisplayName } = useProfile()
      const result = await updateDisplayName('New Name')

      expect(result.success).toBe(true)
      expect(result.error).toBeUndefined()
    })

    it('trims the display name before updating', async () => {
      /**
       * Test display name trimming.
       *
       * Leading/trailing whitespace should be removed before
       * sending to the server.
       */
      const userStore = useUserStore()
      const updateSpy = vi.spyOn(userStore, 'updateDisplayName').mockResolvedValue(true)

      const { updateDisplayName } = useProfile()
      await updateDisplayName('  New Name  ')

      expect(updateSpy).toHaveBeenCalledWith('New Name')
    })

    it('returns error for empty display name', async () => {
      /**
       * Test validation of empty display name.
       *
       * Empty or whitespace-only names should be rejected
       * without making an API call.
       */
      const userStore = useUserStore()
      const updateSpy = vi.spyOn(userStore, 'updateDisplayName')

      const { updateDisplayName } = useProfile()
      const result = await updateDisplayName('   ')

      expect(result.success).toBe(false)
      expect(result.error).toBe('Display name cannot be empty')
      expect(updateSpy).not.toHaveBeenCalled()
    })

    it('returns error when store update fails', async () => {
      /**
       * Test error handling when update fails.
       *
       * If the store returns false (failed to update),
       * the composable should return an error result.
       */
      const userStore = useUserStore()
      vi.spyOn(userStore, 'updateDisplayName').mockResolvedValue(false)
      userStore.error = 'Server error'

      const { updateDisplayName } = useProfile()
      const result = await updateDisplayName('New Name')

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

    it('sets isUpdatingName while updating', async () => {
      /**
       * Test loading state during display name update.
       *
       * Components should be able to show a loading indicator
       * while the update is in progress.
       */
      const userStore = useUserStore()
      let resolveUpdate: () => void
      vi.spyOn(userStore, 'updateDisplayName').mockImplementation(
        () => new Promise((resolve) => { resolveUpdate = () => resolve(true) })
      )

      const { updateDisplayName, isUpdatingName } = useProfile()

      const promise = updateDisplayName('New Name')

      expect(isUpdatingName.value).toBe(true)

      resolveUpdate!()
      await promise

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

  describe('initiateLink', () => {
    it('redirects to Google link URL', () => {
      /**
       * Test Google account linking initiation.
       *
       * When initiating Google linking, the browser should be redirected
       * to the backend's Google link endpoint with a redirect_uri.
       */
      const { initiateLink } = useProfile()

      initiateLink('google')

      expect(window.location.href).toContain('/api/auth/link/google')
      expect(window.location.href).toContain('redirect_uri=')
    })

    it('redirects to Discord link URL', () => {
      /**
       * Test Discord account linking initiation.
       *
       * When initiating Discord linking, the browser should be redirected
       * to the backend's Discord link endpoint with a redirect_uri.
       */
      const { initiateLink } = useProfile()

      initiateLink('discord')

      expect(window.location.href).toContain('/api/auth/link/discord')
      expect(window.location.href).toContain('redirect_uri=')
    })

    it('clears any existing error', () => {
      /**
       * Test error clearing on link initiation.
       *
       * Starting a new link flow should clear any previous errors.
       */
      const { initiateLink, error } = useProfile()

      // Manually set an error state (simulate previous failure)
      initiateLink('google')

      // Error should be cleared (we can't easily test this without internal access)
      // The implementation clears localError before redirect
      expect(error.value).toBeNull()
    })
  })

  describe('unlinkAccount', () => {
    beforeEach(() => {
      // Set up user store with linked accounts
      const userStore = useUserStore()
      userStore.profile = {
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: null,
        hasStarterDeck: true,
        createdAt: '2026-01-01T00:00:00Z',
        linkedAccounts: [
          { provider: 'google', providerUserId: 'g123', email: 'test@gmail.com', linkedAt: '2026-01-01T00:00:00Z' },
          { provider: 'discord', providerUserId: 'd456', email: null, linkedAt: '2026-01-02T00:00:00Z' },
        ],
      }
    })

    it('successfully unlinks an account', async () => {
      /**
       * Test successful account unlinking.
       *
       * When unlinking a provider, the composable should call the
       * API and return success.
       */
      vi.mocked(apiClient.delete).mockResolvedValue(undefined)
      const userStore = useUserStore()
      vi.spyOn(userStore, 'fetchProfile').mockResolvedValue(true)

      const { unlinkAccount } = useProfile()
      const result = await unlinkAccount('discord')

      expect(result.success).toBe(true)
      expect(apiClient.delete).toHaveBeenCalledWith('/api/users/me/link/discord')
    })

    it('refreshes profile after unlinking', async () => {
      /**
       * Test profile refresh after unlink.
       *
       * After successfully unlinking, the profile should be refreshed
       * to get the updated linked accounts list.
       */
      vi.mocked(apiClient.delete).mockResolvedValue(undefined)
      const userStore = useUserStore()
      const fetchSpy = vi.spyOn(userStore, 'fetchProfile').mockResolvedValue(true)

      const { unlinkAccount } = useProfile()
      await unlinkAccount('discord')

      expect(fetchSpy).toHaveBeenCalled()
    })

    it('prevents unlinking the only linked account', async () => {
      /**
       * Test protection against unlinking last account.
       *
       * Users cannot unlink their only OAuth provider because
       * they would be locked out of their account.
       */
      const userStore = useUserStore()
      // Only one linked account
      userStore.profile!.linkedAccounts = [
        { provider: 'google', providerUserId: 'g123', email: 'test@gmail.com', linkedAt: '2026-01-01T00:00:00Z' },
      ]

      const { unlinkAccount } = useProfile()
      const result = await unlinkAccount('google')

      expect(result.success).toBe(false)
      expect(result.error).toContain('Cannot unlink the only linked account')
      expect(apiClient.delete).not.toHaveBeenCalled()
    })

    it('returns error on API failure', async () => {
      /**
       * Test error handling for unlink API failure.
       *
       * If the API call fails, the composable should return
       * an appropriate error message.
       */
      vi.mocked(apiClient.delete).mockRejectedValue(
        new ApiError(400, 'Bad Request', 'Cannot unlink primary provider')
      )

      const { unlinkAccount } = useProfile()
      const result = await unlinkAccount('discord')

      expect(result.success).toBe(false)
      expect(result.error).toBe('Cannot unlink primary provider')
    })

    it('sets isLoadingUnlink while unlinking', async () => {
      /**
       * Test loading state during unlink.
       *
       * Components should be able to show a loading indicator
       * while the unlink operation is in progress.
       */
      let resolveDelete: () => void
      vi.mocked(apiClient.delete).mockImplementation(
        () => new Promise((resolve) => { resolveDelete = () => resolve(undefined) })
      )
      const userStore = useUserStore()
      vi.spyOn(userStore, 'fetchProfile').mockResolvedValue(true)

      const { unlinkAccount, isLoadingUnlink } = useProfile()

      const promise = unlinkAccount('discord')

      expect(isLoadingUnlink.value).toBe(true)

      resolveDelete!()
      await promise

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

  describe('fetchSessionCount', () => {
    it('successfully fetches session count', async () => {
      /**
       * Test successful session count fetch.
       *
       * The composable should fetch the session count from the API
       * and store it for display.
       */
      vi.mocked(apiClient.get).mockResolvedValue({ activeSessionCount: 3 })

      const { fetchSessionCount, sessionCount } = useProfile()
      const result = await fetchSessionCount()

      expect(result).toBe(3)
      expect(sessionCount.value).toBe(3)
    })

    it('returns null on API failure', async () => {
      /**
       * Test graceful handling of session count fetch failure.
       *
       * Session count is not critical, so failures should be
       * handled gracefully without throwing.
       */
      vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'))

      const { fetchSessionCount, sessionCount } = useProfile()
      const result = await fetchSessionCount()

      expect(result).toBeNull()
      expect(sessionCount.value).toBeNull()
    })

    it('sets isLoadingSession while fetching', async () => {
      /**
       * Test loading state during session count fetch.
       *
       * Components may want to show a loading indicator
       * while fetching session information.
       */
      let resolveGet: (value: unknown) => void
      vi.mocked(apiClient.get).mockImplementation(
        () => new Promise((resolve) => { resolveGet = resolve })
      )

      const profile = useProfile()

      const promise = profile.fetchSessionCount()

      expect(profile.isLoadingSession.value).toBe(true)

      resolveGet!({ activeSessionCount: 2 })
      await promise

      expect(profile.isLoadingSession.value).toBe(false)
    })
  })

  describe('isProviderLinked', () => {
    it('returns true for linked providers', () => {
      /**
       * Test linked provider detection.
       *
       * The composable should correctly identify which providers
       * are currently linked to the user's account.
       */
      const userStore = useUserStore()
      userStore.profile = {
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: null,
        hasStarterDeck: true,
        createdAt: '2026-01-01T00:00:00Z',
        linkedAccounts: [
          { provider: 'google', providerUserId: 'g123', email: 'test@gmail.com', linkedAt: '2026-01-01T00:00:00Z' },
        ],
      }

      const { isProviderLinked } = useProfile()

      expect(isProviderLinked('google')).toBe(true)
      expect(isProviderLinked('discord')).toBe(false)
    })

    it('returns false when no accounts are linked', () => {
      /**
       * Test linked provider detection with no accounts.
       *
       * When no profile is loaded or no accounts are linked,
       * both providers should return false.
       */
      const { isProviderLinked } = useProfile()

      expect(isProviderLinked('google')).toBe(false)
      expect(isProviderLinked('discord')).toBe(false)
    })
  })

  describe('getLinkedAccount', () => {
    it('returns linked account for provider', () => {
      /**
       * Test getting linked account details.
       *
       * The composable should return the full account details
       * for a linked provider.
       */
      const userStore = useUserStore()
      userStore.profile = {
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: null,
        hasStarterDeck: true,
        createdAt: '2026-01-01T00:00:00Z',
        linkedAccounts: [
          { provider: 'google', providerUserId: 'g123', email: 'test@gmail.com', linkedAt: '2026-01-01T00:00:00Z' },
        ],
      }

      const { getLinkedAccount } = useProfile()

      const account = getLinkedAccount('google')
      expect(account).toBeDefined()
      expect(account?.email).toBe('test@gmail.com')
    })

    it('returns undefined for unlinked provider', () => {
      /**
       * Test getting unlinked provider details.
       *
       * When a provider is not linked, getLinkedAccount should
       * return undefined.
       */
      const userStore = useUserStore()
      userStore.profile = {
        id: 'user-1',
        displayName: 'Test User',
        avatarUrl: null,
        hasStarterDeck: true,
        createdAt: '2026-01-01T00:00:00Z',
        linkedAccounts: [],
      }

      const { getLinkedAccount } = useProfile()

      expect(getLinkedAccount('discord')).toBeUndefined()
    })
  })

  describe('clearError', () => {
    it('clears the local error state', () => {
      /**
       * Test error clearing.
       *
       * After displaying an error, components may want to clear it
       * (e.g., when user dismisses or tries again).
       */
      const { clearError, error } = useProfile()

      // Error starts as null
      expect(error.value).toBeNull()

      // Clear should not throw even if no error
      clearError()

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

  describe('computed isLoading', () => {
    it('reflects combined loading states', () => {
      /**
       * Test combined loading state.
       *
       * The isLoading computed should be true if any
       * operation is in progress.
       */
      const userStore = useUserStore()
      userStore.isLoading = true

      const { isLoading } = useProfile()

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