/**
 * Tests for useProjectEvents Hook
 */

import { renderHook, act, waitFor } from '@testing-library/react';
import { useProjectEvents } from '@/lib/hooks/useProjectEvents';
import { useEventStore } from '@/lib/stores/eventStore';
import { EventType, type ProjectEvent } from '@/lib/types/events';

// Mock useAuth
const mockUseAuth = jest.fn();

jest.mock('@/lib/auth/AuthContext', () => ({
  useAuth: (selector?: (state: unknown) => unknown) => {
    const state = mockUseAuth();
    return selector ? selector(state) : state;
  },
}));

// Mock config
jest.mock('@/config/app.config', () => ({
  __esModule: true,
  default: {
    api: {
      url: 'http://localhost:8000',
    },
    debug: {
      api: false,
    },
  },
}));

// Mock EventSource
class MockEventSource {
  static instances: MockEventSource[] = [];
  url: string;
  readyState: number = 0;
  onopen: ((event: Event) => void) | null = null;
  onmessage: ((event: MessageEvent) => void) | null = null;
  onerror: ((event: Event) => void) | null = null;

  constructor(url: string) {
    this.url = url;
    MockEventSource.instances.push(this);
  }

  close() {
    this.readyState = 2; // CLOSED
  }

  addEventListener() {}
  removeEventListener() {}

  // Test helpers
  simulateOpen() {
    this.readyState = 1; // OPEN
    if (this.onopen) {
      this.onopen(new Event('open'));
    }
  }

  simulateMessage(data: string) {
    if (this.onmessage) {
      this.onmessage(new MessageEvent('message', { data }));
    }
  }

  simulateError() {
    this.readyState = 2; // CLOSED
    if (this.onerror) {
      this.onerror(new Event('error'));
    }
  }
}

// Type augmentation for EventSource
declare global {
  interface Window {
    EventSource: typeof MockEventSource;
  }
}

// @ts-expect-error - Mocking global EventSource
global.EventSource = MockEventSource;

/**
 * Helper to create mock event
 */
function createMockEvent(overrides: Partial<ProjectEvent> = {}): ProjectEvent {
  return {
    id: `event-${Math.random().toString(36).substr(2, 9)}`,
    type: EventType.AGENT_MESSAGE,
    timestamp: new Date().toISOString(),
    project_id: 'project-123',
    actor_id: 'agent-456',
    actor_type: 'agent',
    payload: { message: 'Test message' },
    ...overrides,
  };
}

describe('useProjectEvents', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    MockEventSource.instances = [];

    // Reset event store
    useEventStore.setState({
      eventsByProject: {},
      maxEvents: 100,
    });

    // Default auth state - authenticated
    mockUseAuth.mockReturnValue({
      isAuthenticated: true,
      accessToken: 'test-access-token',
    });
  });

  describe('initialization', () => {
    it('should start disconnected', () => {
      const { result } = renderHook(() =>
        useProjectEvents('project-123', { autoConnect: false })
      );

      expect(result.current.connectionState).toBe('disconnected');
      expect(result.current.isConnected).toBe(false);
      expect(result.current.events).toEqual([]);
    });

    it('should auto-connect when enabled and authenticated', async () => {
      renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];
      expect(eventSource.url).toContain('/api/v1/projects/project-123/events');
      expect(eventSource.url).toContain('token=test-access-token');
    });

    it('should not connect when not authenticated', () => {
      mockUseAuth.mockReturnValue({
        isAuthenticated: false,
        accessToken: null,
      });

      renderHook(() => useProjectEvents('project-123'));

      expect(MockEventSource.instances).toHaveLength(0);
    });

    it('should not connect when no project ID', () => {
      renderHook(() => useProjectEvents(''));

      expect(MockEventSource.instances).toHaveLength(0);
    });
  });

  describe('connection state', () => {
    it('should update to connected on open', async () => {
      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
      });

      await waitFor(() => {
        expect(result.current.connectionState).toBe('connected');
        expect(result.current.isConnected).toBe(true);
      });
    });

    it('should update to error on connection error', async () => {
      const { result } = renderHook(() =>
        useProjectEvents('project-123', {
          maxRetryAttempts: 1,
        })
      );

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateError();
      });

      await waitFor(() => {
        expect(result.current.error).not.toBeNull();
      });
    });

    it('should call onConnectionChange callback', async () => {
      const onConnectionChange = jest.fn();

      renderHook(() =>
        useProjectEvents('project-123', {
          onConnectionChange,
        })
      );

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      // Should have been called with 'connecting'
      expect(onConnectionChange).toHaveBeenCalledWith('connecting');

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
      });

      await waitFor(() => {
        expect(onConnectionChange).toHaveBeenCalledWith('connected');
      });
    });
  });

  describe('event handling', () => {
    it('should add events to store on message', async () => {
      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
      });

      const mockEvent = createMockEvent();

      act(() => {
        eventSource.simulateMessage(JSON.stringify(mockEvent));
      });

      await waitFor(() => {
        expect(result.current.events).toHaveLength(1);
        expect(result.current.events[0].id).toBe(mockEvent.id);
      });
    });

    it('should call onEvent callback', async () => {
      const onEvent = jest.fn();

      renderHook(() =>
        useProjectEvents('project-123', {
          onEvent,
        })
      );

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
      });

      const mockEvent = createMockEvent();

      act(() => {
        eventSource.simulateMessage(JSON.stringify(mockEvent));
      });

      await waitFor(() => {
        expect(onEvent).toHaveBeenCalledWith(expect.objectContaining({ id: mockEvent.id }));
      });
    });

    it('should ignore invalid JSON', async () => {
      const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();

      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
        eventSource.simulateMessage('not valid json');
      });

      expect(result.current.events).toHaveLength(0);
      expect(consoleWarnSpy).toHaveBeenCalled();

      consoleWarnSpy.mockRestore();
    });

    it('should ignore events with missing required fields', async () => {
      const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();

      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
        eventSource.simulateMessage(JSON.stringify({ message: 'missing fields' }));
      });

      expect(result.current.events).toHaveLength(0);
      expect(consoleWarnSpy).toHaveBeenCalled();

      consoleWarnSpy.mockRestore();
    });
  });

  describe('disconnect and reconnect', () => {
    it('should disconnect when called', async () => {
      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const initialInstanceCount = MockEventSource.instances.length;
      const eventSource = MockEventSource.instances[initialInstanceCount - 1];

      act(() => {
        eventSource.simulateOpen();
      });

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

      act(() => {
        result.current.disconnect();
      });

      // After disconnect, the connection state should be disconnected
      expect(result.current.connectionState).toBe('disconnected');
      expect(result.current.isConnected).toBe(false);
    });

    it('should reconnect when called', async () => {
      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      act(() => {
        result.current.disconnect();
      });

      const instanceCountBeforeReconnect = MockEventSource.instances.length;

      act(() => {
        result.current.reconnect();
      });

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(instanceCountBeforeReconnect);
      });
    });
  });

  describe('clearEvents', () => {
    it('should clear events for the project', async () => {
      const { result } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateOpen();
        eventSource.simulateMessage(JSON.stringify(createMockEvent()));
      });

      await waitFor(() => {
        expect(result.current.events).toHaveLength(1);
      });

      act(() => {
        result.current.clearEvents();
      });

      expect(result.current.events).toHaveLength(0);
    });
  });

  describe('retry behavior', () => {
    it('should increment retry count on error', async () => {
      const { result } = renderHook(() =>
        useProjectEvents('project-123', {
          maxRetryAttempts: 5,
          initialRetryDelay: 10, // Short delay for test
        })
      );

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateError();
      });

      await waitFor(() => {
        expect(result.current.retryCount).toBeGreaterThanOrEqual(0);
      });
    });

    it('should call onError callback', async () => {
      const onError = jest.fn();

      renderHook(() =>
        useProjectEvents('project-123', {
          onError,
          maxRetryAttempts: 1,
        })
      );

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      act(() => {
        eventSource.simulateError();
      });

      await waitFor(() => {
        expect(onError).toHaveBeenCalled();
      });
    });
  });

  describe('cleanup', () => {
    it('should close connection on unmount', async () => {
      const { unmount } = renderHook(() => useProjectEvents('project-123'));

      await waitFor(() => {
        expect(MockEventSource.instances.length).toBeGreaterThan(0);
      });

      const eventSource = MockEventSource.instances[0];

      unmount();

      expect(eventSource.readyState).toBe(2); // CLOSED
    });
  });
});
