/**
 * Tests for useOnlineStatus hook
 */

import React from 'react';
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { AppState, AppStateStatus } from 'react-native';
import { useOnlineStatus, useMultipleOnlineStatus } from '../useOnlineStatus';
import { onlineStatusService, BeneficiaryOnlineStatus } from '@/services/onlineStatusService';

// Mock the onlineStatusService
jest.mock('@/services/onlineStatusService', () => ({
  onlineStatusService: {
    checkBeneficiaryStatus: jest.fn(),
    checkMultipleBeneficiaries: jest.fn(),
  },
}));

// Mock AppState
jest.mock('react-native', () => {
  const listeners: ((state: AppStateStatus) => void)[] = [];
  return {
    AppState: {
      addEventListener: jest.fn((event: string, callback: (state: AppStateStatus) => void) => {
        listeners.push(callback);
        return {
          remove: jest.fn(() => {
            const index = listeners.indexOf(callback);
            if (index > -1) listeners.splice(index, 1);
          }),
        };
      }),
      // Helper to trigger state change in tests
      __triggerStateChange: (state: AppStateStatus) => {
        listeners.forEach(cb => cb(state));
      },
    },
  };
});

const mockService = onlineStatusService as jest.Mocked<typeof onlineStatusService>;

// Helper to create mock status data
const createMockStatus = (overrides: Partial<BeneficiaryOnlineStatus> = {}): BeneficiaryOnlineStatus => ({
  beneficiaryId: 'ben-123',
  totalDevices: 2,
  onlineCount: 1,
  warningCount: 0,
  offlineCount: 1,
  overallStatus: 'warning',
  devices: [
    {
      deviceId: 'dev-1',
      deviceName: 'Device 1',
      status: 'online',
      lastSeen: new Date(),
      lastSeenMinutesAgo: 2,
      lastSeenFormatted: '2 min ago',
    },
    {
      deviceId: 'dev-2',
      deviceName: 'Device 2',
      status: 'offline',
      lastSeen: new Date(Date.now() - 2 * 60 * 60 * 1000),
      lastSeenMinutesAgo: 120,
      lastSeenFormatted: '2 hours ago',
    },
  ],
  lastChecked: new Date(),
  ...overrides,
});

describe('useOnlineStatus', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  describe('initial loading', () => {
    it('should start in loading state', () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

    it('should fetch data on mount by default', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      renderHook(() => useOnlineStatus('ben-123'));

      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-123', true);
      });
    });

    it('should not fetch on mount when fetchOnMount is false', async () => {
      const { result } = renderHook(() =>
        useOnlineStatus('ben-123', { fetchOnMount: false })
      );

      expect(mockService.checkBeneficiaryStatus).not.toHaveBeenCalled();
      expect(result.current.isLoading).toBe(false);
    });

    it('should handle null beneficiaryId', async () => {
      const { result } = renderHook(() => useOnlineStatus(null));

      expect(result.current.isLoading).toBe(false);
      expect(result.current.status).toBeNull();
      expect(mockService.checkBeneficiaryStatus).not.toHaveBeenCalled();
    });
  });

  describe('successful data fetch', () => {
    it('should populate status data', async () => {
      const mockStatus = createMockStatus();
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: mockStatus,
      });

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

      expect(result.current.status).toEqual(mockStatus);
      expect(result.current.overallStatus).toBe('warning');
      expect(result.current.devices).toHaveLength(2);
      expect(result.current.onlineCount).toBe(1);
      expect(result.current.offlineCount).toBe(1);
      expect(result.current.totalDevices).toBe(2);
    });

    it('should provide device lookup functions', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

      // isDeviceOnline
      expect(result.current.isDeviceOnline('dev-1')).toBe(true);
      expect(result.current.isDeviceOnline('dev-2')).toBe(false);
      expect(result.current.isDeviceOnline('non-existent')).toBe(false);

      // getDeviceStatus
      const device1 = result.current.getDeviceStatus('dev-1');
      expect(device1?.status).toBe('online');
      expect(result.current.getDeviceStatus('non-existent')).toBeUndefined();
    });
  });

  describe('error handling', () => {
    it('should handle API errors', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: false,
        error: 'Network error',
      });

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

      expect(result.current.error).toBe('Network error');
      expect(result.current.status).toBeNull();
    });

    it('should handle exceptions', async () => {
      mockService.checkBeneficiaryStatus.mockRejectedValue(new Error('Unexpected error'));

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

      expect(result.current.error).toBe('Unexpected error');
    });
  });

  describe('refresh', () => {
    it('should allow manual refresh', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      const { result } = renderHook(() => useOnlineStatus('ben-123'));

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

      expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1);

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

      expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(2);
    });

    it('should set isRefreshing during refresh', async () => {
      let resolvePromise: (value: any) => void;
      mockService.checkBeneficiaryStatus.mockImplementation(
        () =>
          new Promise(resolve => {
            resolvePromise = resolve;
          })
      );

      const { result } = renderHook(() =>
        useOnlineStatus('ben-123', { fetchOnMount: false })
      );

      // Start refresh
      let refreshPromise: Promise<void>;
      act(() => {
        refreshPromise = result.current.refresh();
      });

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

      // Resolve the API call
      act(() => {
        resolvePromise!({ ok: true, data: createMockStatus() });
      });

      await act(async () => {
        await refreshPromise;
      });

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

  describe('polling', () => {
    it('should poll when enabled', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      renderHook(() =>
        useOnlineStatus('ben-123', {
          enablePolling: true,
          pollingInterval: 5000,
        })
      );

      // Initial fetch
      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1);
      });

      // Advance timer by polling interval
      act(() => {
        jest.advanceTimersByTime(5000);
      });

      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(2);
      });

      // Advance again
      act(() => {
        jest.advanceTimersByTime(5000);
      });

      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(3);
      });
    });

    it('should stop polling on unmount', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      const { unmount } = renderHook(() =>
        useOnlineStatus('ben-123', {
          enablePolling: true,
          pollingInterval: 5000,
        })
      );

      // Initial fetch
      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1);
      });

      // Unmount
      unmount();

      // Advance timer - should not trigger new fetch
      act(() => {
        jest.advanceTimersByTime(10000);
      });

      expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1);
    });
  });

  describe('beneficiaryId changes', () => {
    it('should refetch when beneficiaryId changes', async () => {
      mockService.checkBeneficiaryStatus.mockResolvedValue({
        ok: true,
        data: createMockStatus(),
      });

      const { rerender } = renderHook(
        ({ id }: { id: string }) => useOnlineStatus(id),
        { initialProps: { id: 'ben-123' } }
      );

      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-123', true);
      });

      // Change beneficiaryId
      rerender({ id: 'ben-456' });

      await waitFor(() => {
        expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-456', true);
      });
    });
  });
});

describe('useMultipleOnlineStatus', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should fetch statuses for multiple beneficiaries', async () => {
    const results = new Map([
      ['ben-1', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-1' }) }],
      ['ben-2', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-2' }) }],
    ]);

    mockService.checkMultipleBeneficiaries.mockResolvedValue(results);

    const { result } = renderHook(() =>
      useMultipleOnlineStatus(['ben-1', 'ben-2'])
    );

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

    expect(result.current.statuses.size).toBe(2);
    expect(result.current.getStatus('ben-1')?.beneficiaryId).toBe('ben-1');
    expect(result.current.getStatus('ben-2')?.beneficiaryId).toBe('ben-2');
  });

  it('should handle errors for individual beneficiaries', async () => {
    const results = new Map([
      ['ben-1', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-1' }) }],
      ['ben-2', { ok: false, error: 'Failed to fetch' }],
    ]);

    mockService.checkMultipleBeneficiaries.mockResolvedValue(results);

    const { result } = renderHook(() =>
      useMultipleOnlineStatus(['ben-1', 'ben-2'])
    );

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

    expect(result.current.statuses.size).toBe(1);
    expect(result.current.errors.get('ben-2')).toBe('Failed to fetch');
  });

  it('should allow manual refresh', async () => {
    mockService.checkMultipleBeneficiaries.mockResolvedValue(new Map());

    const { result } = renderHook(() =>
      useMultipleOnlineStatus(['ben-1', 'ben-2'])
    );

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

    expect(mockService.checkMultipleBeneficiaries).toHaveBeenCalledTimes(1);

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

    expect(mockService.checkMultipleBeneficiaries).toHaveBeenCalledTimes(2);
  });

  it('should handle empty beneficiary array', async () => {
    const { result } = renderHook(() => useMultipleOnlineStatus([]));

    expect(result.current.isLoading).toBe(false);
    expect(result.current.statuses.size).toBe(0);
    expect(mockService.checkMultipleBeneficiaries).not.toHaveBeenCalled();
  });
});
