import React from 'react';
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useUserConfig } from '../../hooks/useUserConfig';
import { DEFAULT_GAME_CONFIG } from '../../types';
import type { GameConfig } from '../../types';

const mockAuthFetch = vi.fn();
const mockIsAuthenticated = vi.fn(() => true);

vi.mock('../../hooks/useAuthenticatedFetch', () => ({
  useAuthenticatedFetch: () => ({
    authFetch: mockAuthFetch,
    isAuthenticated: mockIsAuthenticated(),
  }),
}));

vi.mock('react-hot-toast', () => ({
  default: {
    success: vi.fn(),
    error: vi.fn(),
  },
}));

describe('useUserConfig', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockIsAuthenticated.mockReturnValue(true);
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  describe('initialization', () => {
    it('returns DEFAULT_GAME_CONFIG initially', () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: null }),
      });

      const { result } = renderHook(() => useUserConfig());
      
      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });

    it('fetches default config on mount when authenticated', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: null }),
      });

      renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(mockAuthFetch).toHaveBeenCalledWith('/api/users/me');
      });
    });

    it('does not fetch when not authenticated', () => {
      mockIsAuthenticated.mockReturnValue(false);

      renderHook(() => useUserConfig());

      expect(mockAuthFetch).not.toHaveBeenCalled();
    });
  });

  describe('fetchDefaultConfig - happy path', () => {
    it('updates defaultConfig when server returns config', async () => {
      const serverConfig: GameConfig = {
        ...DEFAULT_GAME_CONFIG,
        shuffleQuestions: true,
        streakBonusEnabled: true,
      };

      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: serverConfig }),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.defaultConfig).toEqual(serverConfig);
      });
    });

    it('sets loading to true during fetch', async () => {
      let resolvePromise: (value: unknown) => void;
      const pendingPromise = new Promise((resolve) => {
        resolvePromise = resolve;
      });

      mockAuthFetch.mockReturnValueOnce(pendingPromise);

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(true);
      });

      await act(async () => {
        resolvePromise!({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        });
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });
    });

    it('keeps DEFAULT_GAME_CONFIG when server returns null config', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: null }),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });
  });

  describe('fetchDefaultConfig - unhappy path', () => {
    it('handles non-ok response gracefully', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });

    it('handles network error gracefully', async () => {
      mockAuthFetch.mockRejectedValueOnce(new Error('Network error'));

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });

    it('handles 401 unauthorized gracefully', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });
  });

  describe('saveDefaultConfig - happy path', () => {
    it('successfully saves config to server', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ success: true }),
        });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      const newConfig: GameConfig = {
        ...DEFAULT_GAME_CONFIG,
        shuffleQuestions: true,
        hostParticipates: false,
      };

      await act(async () => {
        await result.current.saveDefaultConfig(newConfig);
      });

      expect(mockAuthFetch).toHaveBeenLastCalledWith('/api/users/me/default-config', {
        method: 'PUT',
        body: JSON.stringify({ defaultGameConfig: newConfig }),
      });
    });

    it('updates local state after successful save', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ success: true }),
        });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      const newConfig: GameConfig = {
        ...DEFAULT_GAME_CONFIG,
        shuffleAnswers: true,
      };

      await act(async () => {
        await result.current.saveDefaultConfig(newConfig);
      });

      expect(result.current.defaultConfig).toEqual(newConfig);
    });

    it('sets saving to true during save', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: null }),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      let resolvePromise: (value: unknown) => void;
      const pendingPromise = new Promise((resolve) => {
        resolvePromise = resolve;
      });

      mockAuthFetch.mockReturnValueOnce(pendingPromise);

      act(() => {
        result.current.saveDefaultConfig({ ...DEFAULT_GAME_CONFIG });
      });

      await waitFor(() => {
        expect(result.current.saving).toBe(true);
      });

      await act(async () => {
        resolvePromise!({
          ok: true,
          json: () => Promise.resolve({ success: true }),
        });
      });

      await waitFor(() => {
        expect(result.current.saving).toBe(false);
      });
    });
  });

  describe('saveDefaultConfig - unhappy path', () => {
    it('throws error when save fails with non-ok response', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockResolvedValueOnce({
          ok: false,
          status: 500,
        });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      await expect(
        act(async () => {
          await result.current.saveDefaultConfig({ ...DEFAULT_GAME_CONFIG });
        })
      ).rejects.toThrow('Failed to save defaults');
    });

    it('throws error when network fails', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockRejectedValueOnce(new Error('Network error'));

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      await expect(
        act(async () => {
          await result.current.saveDefaultConfig({ ...DEFAULT_GAME_CONFIG });
        })
      ).rejects.toThrow('Network error');
    });

    it('resets saving state on error', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockRejectedValueOnce(new Error('Server error'));

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      try {
        await act(async () => {
          await result.current.saveDefaultConfig({ ...DEFAULT_GAME_CONFIG });
        });
      } catch {
        // Expected to throw
      }

      expect(result.current.saving).toBe(false);
    });

    it('does not update local state on save failure', async () => {
      const initialConfig: GameConfig = {
        ...DEFAULT_GAME_CONFIG,
        shuffleQuestions: true,
      };

      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: initialConfig }),
        })
        .mockResolvedValueOnce({
          ok: false,
          status: 500,
        });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.defaultConfig).toEqual(initialConfig);
      });

      const newConfig: GameConfig = {
        ...DEFAULT_GAME_CONFIG,
        shuffleAnswers: true,
      };

      try {
        await act(async () => {
          await result.current.saveDefaultConfig(newConfig);
        });
      } catch {
        // Expected to throw
      }

      expect(result.current.defaultConfig).toEqual(initialConfig);
    });
  });

  describe('concurrent operations', () => {
    it('handles multiple save calls correctly', async () => {
      mockAuthFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ defaultGameConfig: null }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ success: true }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ success: true }),
        });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      const config1: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true };
      const config2: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true };

      await act(async () => {
        await result.current.saveDefaultConfig(config1);
      });

      await act(async () => {
        await result.current.saveDefaultConfig(config2);
      });

      expect(result.current.defaultConfig).toEqual(config2);
    });
  });

  describe('edge cases', () => {
    it('handles partial config from server', async () => {
      const partialConfig = {
        shuffleQuestions: true,
      };

      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: partialConfig }),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(partialConfig);
    });

    it('handles empty response body', async () => {
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({}),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(DEFAULT_GAME_CONFIG);
    });

    it('handles config with extra fields from server', async () => {
      const serverConfigWithExtras = {
        ...DEFAULT_GAME_CONFIG,
        unknownField: 'value',
        anotherUnknown: 123,
      };

      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: serverConfigWithExtras }),
      });

      const { result } = renderHook(() => useUserConfig());

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.defaultConfig).toEqual(serverConfigWithExtras);
    });
  });

  describe('re-authentication scenarios', () => {
    it('re-fetches config when authentication status changes', async () => {
      mockIsAuthenticated.mockReturnValue(false);

      const { result, rerender } = renderHook(() => useUserConfig());

      expect(mockAuthFetch).not.toHaveBeenCalled();

      mockIsAuthenticated.mockReturnValue(true);
      mockAuthFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ defaultGameConfig: { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true } }),
      });

      rerender();

      await waitFor(() => {
        expect(mockAuthFetch).toHaveBeenCalledWith('/api/users/me');
      });
    });
  });
});
