import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

const mockSigninSilent = vi.fn();
const mockSigninRedirect = vi.fn();
const mockAuth = {
  user: {
    access_token: 'valid-token',
    expires_at: Math.floor(Date.now() / 1000) + 3600,
  },
  isAuthenticated: true,
  isLoading: false,
  signinSilent: mockSigninSilent,
  signinRedirect: mockSigninRedirect,
};

vi.mock('react-oidc-context', () => ({
  useAuth: () => mockAuth,
}));

// Get the API URL that the hook will actually use
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';

const originalFetch = global.fetch;

describe('useAuthenticatedFetch', () => {
  let useAuthenticatedFetch: typeof import('../../hooks/useAuthenticatedFetch').useAuthenticatedFetch;

  beforeEach(async () => {
    vi.clearAllMocks();
    vi.resetModules();
    
    const module = await import('../../hooks/useAuthenticatedFetch');
    useAuthenticatedFetch = module.useAuthenticatedFetch;
    
    mockAuth.user = {
      access_token: 'valid-token',
      expires_at: Math.floor(Date.now() / 1000) + 3600,
    };
    mockAuth.isAuthenticated = true;
    mockAuth.isLoading = false;
    global.fetch = vi.fn();
    Object.defineProperty(navigator, 'onLine', { value: true, writable: true });
  });

  afterEach(() => {
    global.fetch = originalFetch;
    vi.resetAllMocks();
  });

  describe('authFetch - happy path', () => {
    it('makes authenticated request with Bearer token', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ data: 'test' }),
      });

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

      let response;
      await act(async () => {
        response = await result.current.authFetch('/api/test');
      });

      expect(global.fetch).toHaveBeenCalledWith(
        `${API_URL}/api/test`,
        expect.objectContaining({
          headers: expect.objectContaining({
            Authorization: 'Bearer valid-token',
            'Content-Type': 'application/json',
          }),
        })
      );
      expect(response).toBeDefined();
    });

    it('uses full URL when provided', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

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

      await act(async () => {
        await result.current.authFetch('https://other-api.com/endpoint');
      });

      expect(global.fetch).toHaveBeenCalledWith(
        'https://other-api.com/endpoint',
        expect.any(Object)
      );
    });

    it('passes through additional options', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

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

      await act(async () => {
        await result.current.authFetch('/api/test', {
          method: 'POST',
          body: JSON.stringify({ data: 'test' }),
        });
      });

      expect(global.fetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ data: 'test' }),
        })
      );
    });
  });

  describe('token expiration handling', () => {
    it('refreshes token silently when expired', async () => {
      mockAuth.user = {
        access_token: 'expired-token',
        expires_at: Math.floor(Date.now() / 1000) - 100,
      };
      mockSigninSilent.mockResolvedValueOnce({ access_token: 'new-token' });
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

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

      await act(async () => {
        await result.current.authFetch('/api/test');
      });

      expect(mockSigninSilent).toHaveBeenCalled();
      expect(global.fetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          headers: expect.objectContaining({
            Authorization: 'Bearer new-token',
          }),
        })
      );
    });

    it('refreshes token with buffer before actual expiration', async () => {
      mockAuth.user = {
        access_token: 'almost-expired-token',
        expires_at: Math.floor(Date.now() / 1000) + 30,
      };
      mockSigninSilent.mockResolvedValueOnce({ access_token: 'refreshed-token' });
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

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

      await act(async () => {
        await result.current.authFetch('/api/test');
      });

      expect(mockSigninSilent).toHaveBeenCalled();
    });
  });

  describe('redirect scenarios', () => {
    it('throws error when silent refresh fails on expired token', async () => {
      mockAuth.user = {
        access_token: 'expired-token',
        expires_at: Math.floor(Date.now() / 1000) - 100,
      };
      mockSigninSilent.mockResolvedValueOnce(null);

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Session expired, redirecting to login');
    });

    it('throws error when 401 and refresh fails', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: false,
        status: 401,
      });
      mockSigninSilent.mockResolvedValueOnce(null);

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Session expired, redirecting to login');
    });
  });

  describe('401 handling', () => {
    it('retries with refreshed token on 401', async () => {
      (global.fetch as ReturnType<typeof vi.fn>)
        .mockResolvedValueOnce({
          ok: false,
          status: 401,
        })
        .mockResolvedValueOnce({
          ok: true,
          status: 200,
        });
      mockSigninSilent.mockResolvedValueOnce({ access_token: 'new-token' });

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

      await act(async () => {
        await result.current.authFetch('/api/test');
      });

      expect(mockSigninSilent).toHaveBeenCalled();
      expect(global.fetch).toHaveBeenCalledTimes(2);
    });
  });

  describe('error handling', () => {
    it('throws on network error', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network failed'));

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Network error. Please try again.');
    });

    it('throws when offline before request', async () => {
      Object.defineProperty(navigator, 'onLine', { value: false });

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('You appear to be offline. Please check your connection.');
    });

    it('throws server error for 5xx responses', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Server error. Please try again later.');
    });

    it('throws when not authenticated', async () => {
      mockAuth.user = null;

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Not authenticated');
    });
  });

  describe('concurrent requests', () => {
    it('reuses pending silent refresh for concurrent requests', async () => {
      mockAuth.user = {
        access_token: 'expired-token',
        expires_at: Math.floor(Date.now() / 1000) - 100,
      };
      
      let resolveRefresh: (value: unknown) => void;
      const refreshPromise = new Promise((resolve) => {
        resolveRefresh = resolve;
      });
      mockSigninSilent.mockReturnValue(refreshPromise);
      
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
        ok: true,
        status: 200,
      });

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

      let promise1: Promise<Response>;
      let promise2: Promise<Response>;

      act(() => {
        promise1 = result.current.authFetch('/api/test1');
        promise2 = result.current.authFetch('/api/test2');
      });

      await act(async () => {
        resolveRefresh!({ access_token: 'new-token' });
      });

      await act(async () => {
        await promise1!;
        await promise2!;
      });

      expect(mockSigninSilent).toHaveBeenCalledTimes(1);
    });
  });

  describe('isAuthenticated and user properties', () => {
    it('exposes isAuthenticated from auth context', () => {
      mockAuth.isAuthenticated = true;

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

      expect(result.current.isAuthenticated).toBe(true);
    });

    it('exposes isLoading from auth context', () => {
      mockAuth.isLoading = true;

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

      expect(result.current.isLoading).toBe(true);
    });

    it('exposes user from auth context', () => {
      const { result } = renderHook(() => useAuthenticatedFetch());

      expect(result.current.user).toEqual(mockAuth.user);
    });
  });

  describe('edge cases', () => {
    it('handles missing expires_at gracefully', async () => {
      mockAuth.user = {
        access_token: 'token-without-expiry',
        expires_at: undefined,
      };
      mockSigninSilent.mockResolvedValueOnce({ access_token: 'new-token' });
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

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

      await act(async () => {
        await result.current.authFetch('/api/test');
      });

      expect(mockSigninSilent).toHaveBeenCalled();
    });

    it('handles fetch throwing TypeError for network issues', async () => {
      const typeError = new TypeError('Failed to fetch');
      (global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(typeError);

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

      await expect(
        act(async () => {
          await result.current.authFetch('/api/test');
        })
      ).rejects.toThrow('Network error. Please try again.');
    });

    it('returns response for 4xx errors (non-401)', async () => {
      (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found',
      });

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

      let response;
      await act(async () => {
        response = await result.current.authFetch('/api/test');
      });

      expect(response.status).toBe(404);
    });
  });
});
