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

import { useAuthStore } from '@/stores/auth'
import { socketClient } from './client'

describe('socketClient', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    // Reset socket client state
    socketClient.disconnect()
  })

  describe('initial state', () => {
    it('starts disconnected', () => {
      /**
       * Test that socket client initializes in disconnected state.
       *
       * The socket should not connect until explicitly requested,
       * to avoid unnecessary connections on pages that don't need
       * real-time updates.
       */
      expect(socketClient.connectionState).toBe('disconnected')
      expect(socketClient.isConnected).toBe(false)
      expect(socketClient.socket).toBeNull()
    })
  })

  describe('connect', () => {
    it('throws error when not authenticated', async () => {
      /**
       * Test that connection requires authentication.
       *
       * The socket server requires a valid token for authentication.
       * Attempting to connect without one should fail immediately.
       */
      // Auth store starts with no tokens
      await expect(socketClient.connect()).rejects.toThrow('not authenticated')
    })

    it('requires valid token before connecting', async () => {
      /**
       * Test that expired tokens prevent connection.
       *
       * If the token is expired and refresh fails, connection
       * should not be attempted.
       */
      const auth = useAuthStore()
      // Set expired token with no refresh token
      auth.setTokens({
        accessToken: 'expired-token',
        refreshToken: '',
        expiresAt: Date.now() - 1000, // Already expired
      })

      await expect(socketClient.connect()).rejects.toThrow('not authenticated')
    })
  })

  describe('connection state listeners', () => {
    it('notifies listeners of state changes', () => {
      /**
       * Test that connection state changes are broadcast.
       *
       * Components need to react to connection state changes
       * (e.g., showing reconnecting indicator).
       */
      const listener = vi.fn()
      const unsubscribe = socketClient.onConnectionStateChange(listener)

      // Trigger a state change by disconnecting
      socketClient.disconnect()

      expect(listener).toHaveBeenCalledWith('disconnected')

      // Cleanup
      unsubscribe()
    })

    it('unsubscribe stops notifications', () => {
      /**
       * Test that unsubscribe removes the listener.
       *
       * Components should unsubscribe when unmounting to prevent
       * memory leaks and stale callbacks.
       */
      const listener = vi.fn()
      const unsubscribe = socketClient.onConnectionStateChange(listener)

      // Unsubscribe
      unsubscribe()

      // Clear previous calls
      listener.mockClear()

      // Trigger a state change
      socketClient.disconnect()

      // Listener should not be called after unsubscribe
      expect(listener).not.toHaveBeenCalled()
    })

    it('supports multiple listeners', () => {
      /**
       * Test that multiple listeners can subscribe.
       *
       * Different components may need to track connection state
       * independently.
       */
      const listener1 = vi.fn()
      const listener2 = vi.fn()

      const unsub1 = socketClient.onConnectionStateChange(listener1)
      const unsub2 = socketClient.onConnectionStateChange(listener2)

      socketClient.disconnect()

      expect(listener1).toHaveBeenCalledWith('disconnected')
      expect(listener2).toHaveBeenCalledWith('disconnected')

      // Cleanup
      unsub1()
      unsub2()
    })
  })

  describe('disconnect', () => {
    it('sets state to disconnected', () => {
      /**
       * Test that disconnect updates the connection state.
       *
       * After disconnecting, the state should reflect that
       * we're no longer connected.
       */
      socketClient.disconnect()

      expect(socketClient.connectionState).toBe('disconnected')
      expect(socketClient.isConnected).toBe(false)
    })

    it('clears socket reference', () => {
      /**
       * Test that disconnect clears the socket.
       *
       * After disconnecting, the socket reference should be null
       * to prevent accidental use of a dead connection.
       */
      socketClient.disconnect()

      expect(socketClient.socket).toBeNull()
    })
  })

  describe('helper methods exist', () => {
    it('has all game action helpers', () => {
      /**
       * Test that all game action helper methods exist.
       *
       * These helpers provide a clean API for common game operations
       * without needing to know the exact event names.
       */
      expect(typeof socketClient.joinGame).toBe('function')
      expect(typeof socketClient.sendAction).toBe('function')
      expect(typeof socketClient.resign).toBe('function')
      expect(typeof socketClient.spectate).toBe('function')
      expect(typeof socketClient.leaveSpectate).toBe('function')
    })

    it('has all event subscription helpers', () => {
      /**
       * Test that all event subscription helper methods exist.
       *
       * These helpers provide typed subscriptions to server events
       * with automatic cleanup via returned unsubscribe functions.
       */
      expect(typeof socketClient.onGameState).toBe('function')
      expect(typeof socketClient.onActionResult).toBe('function')
      expect(typeof socketClient.onGameError).toBe('function')
      expect(typeof socketClient.onGameOver).toBe('function')
      expect(typeof socketClient.onOpponentConnected).toBe('function')
      expect(typeof socketClient.onTurnTimeoutWarning).toBe('function')
    })
  })
})
