import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LiveLogViewer } from '../LiveLogViewer';
import * as logsApi from '../../api/logs';

// Mock the connectLiveLogs and connectSecurityLogs functions
vi.mock('../../api/logs', async () => {
  const actual = await vi.importActual('../../api/logs');
  return {
    ...actual,
    connectLiveLogs: vi.fn(),
    connectSecurityLogs: vi.fn(),
  };
});

describe('LiveLogViewer', () => {
  let mockCloseConnection: ReturnType<typeof vi.fn>;
  let mockOnMessage: ((log: logsApi.LiveLogEntry) => void) | null;
  let mockOnSecurityMessage: ((log: logsApi.SecurityLogEntry) => void) | null;
  let mockOnClose: (() => void) | null;

  beforeEach(() => {
    mockCloseConnection = vi.fn();
    mockOnMessage = null;
    mockOnSecurityMessage = null;
    mockOnClose = null;

    vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, onMessage, onOpen, _onError, onClose) => {
      mockOnMessage = onMessage;
      mockOnClose = onClose ?? null;
      // Simulate connection success
      if (onOpen) {
        setTimeout(() => onOpen(), 0);
      }
      return mockCloseConnection as () => void;
    });

    vi.mocked(logsApi.connectSecurityLogs).mockImplementation((_filters, onMessage, onOpen, _onError, onClose) => {
      mockOnSecurityMessage = onMessage;
      mockOnClose = onClose ?? null;
      // Simulate connection success
      if (onOpen) {
        setTimeout(() => onOpen(), 0);
      }
      return mockCloseConnection as () => void;
    });
  });

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

  it('renders the component with initial state', async () => {
    render(<LiveLogViewer />);

    expect(screen.getByText('Live Security Logs')).toBeTruthy();
    // Initially disconnected until WebSocket opens
    expect(screen.getByText('Disconnected')).toBeTruthy();

    // Wait for onOpen callback to be called
    await waitFor(() => {
      expect(screen.getByText('Connected')).toBeTruthy();
    });

    expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
  });

  it('displays incoming log messages', async () => {
    render(<LiveLogViewer />);

    // Simulate receiving a log
    const logEntry: logsApi.LiveLogEntry = {
      level: 'info',
      timestamp: '2025-12-09T10:30:00Z',
      message: 'Test log message',
      source: 'test',
    };

    if (mockOnMessage) {
      mockOnMessage(logEntry);
    }

    await waitFor(() => {
      expect(screen.getByText('Test log message')).toBeTruthy();
      expect(screen.getByText('INFO')).toBeTruthy();
      expect(screen.getByText('[test]')).toBeTruthy();
    });
  });

  it('filters logs by text', async () => {
    const user = userEvent.setup();
    render(<LiveLogViewer />);

    // Add multiple logs
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'First message' });
      mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Second message' });
    }

    await waitFor(() => {
      expect(screen.getByText('First message')).toBeTruthy();
      expect(screen.getByText('Second message')).toBeTruthy();
    });

    // Apply text filter
    const filterInput = screen.getByPlaceholderText('Filter by text...');
    await user.type(filterInput, 'First');

    await waitFor(() => {
      expect(screen.getByText('First message')).toBeTruthy();
      expect(screen.queryByText('Second message')).toBeFalsy();
    });
  });

  it('filters logs by level', async () => {
    const user = userEvent.setup();
    render(<LiveLogViewer />);

    // Add multiple logs
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Info message' });
      mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Error message' });
    }

    await waitFor(() => {
      expect(screen.getByText('Info message')).toBeTruthy();
      expect(screen.getByText('Error message')).toBeTruthy();
    });

    // Apply level filter
    const levelSelect = screen.getAllByRole('combobox')[0];
    await user.selectOptions(levelSelect, 'error');

    await waitFor(() => {
      expect(screen.queryByText('Info message')).toBeFalsy();
      expect(screen.getByText('Error message')).toBeTruthy();
    });
  });

  it('pauses and resumes log streaming', async () => {
    const user = userEvent.setup();
    render(<LiveLogViewer />);

    // Add initial log
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Before pause' });
    }

    await waitFor(() => {
      expect(screen.getByText('Before pause')).toBeTruthy();
    });

    // Click pause button
    const pauseButton = screen.getByTitle('Pause');
    await user.click(pauseButton);

    // Verify paused state
    await waitFor(() => {
      expect(screen.getByText('⏸ Paused')).toBeTruthy();
    });

    // Try to add log while paused (should not appear)
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'During pause' });
    }

    // Log should not appear
    expect(screen.queryByText('During pause')).toBeFalsy();

    // Resume
    const resumeButton = screen.getByTitle('Resume');
    await user.click(resumeButton);

    // Add log after resume
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'After resume' });
    }

    await waitFor(() => {
      expect(screen.getByText('After resume')).toBeTruthy();
    });
  });

  it('clears all logs', async () => {
    const user = userEvent.setup();
    render(<LiveLogViewer />);

    // Add logs
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
    }

    await waitFor(() => {
      expect(screen.getByText('Log 1')).toBeTruthy();
      expect(screen.getByText('Log 2')).toBeTruthy();
    });

    // Click clear button
    const clearButton = screen.getByTitle('Clear logs');
    await user.click(clearButton);

    await waitFor(() => {
      expect(screen.queryByText('Log 1')).toBeFalsy();
      expect(screen.queryByText('Log 2')).toBeFalsy();
      expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
    });
  });

  it('limits the number of stored logs', async () => {
    render(<LiveLogViewer maxLogs={2} />);

    // Add 3 logs (exceeding maxLogs)
    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'Log 3' });
    }

    await waitFor(() => {
      // First log should be removed, only last 2 should remain
      expect(screen.queryByText('Log 1')).toBeFalsy();
      expect(screen.getByText('Log 2')).toBeTruthy();
      expect(screen.getByText('Log 3')).toBeTruthy();
    });
  });

  it('displays log data when available', async () => {
    render(<LiveLogViewer />);

    const logWithData: logsApi.LiveLogEntry = {
      level: 'error',
      timestamp: '2025-12-09T10:30:00Z',
      message: 'Error occurred',
      data: { error_code: 500, details: 'Internal server error' },
    };

    if (mockOnMessage) {
      mockOnMessage(logWithData);
    }

    await waitFor(() => {
      expect(screen.getByText('Error occurred')).toBeTruthy();
      // Check that data is rendered as JSON
      expect(screen.getByText(/"error_code"/)).toBeTruthy();
    });
  });

  it('closes WebSocket connection on unmount', () => {
    const { unmount } = render(<LiveLogViewer />);

    expect(logsApi.connectLiveLogs).toHaveBeenCalled();

    unmount();

    expect(mockCloseConnection).toHaveBeenCalled();
  });

  it('applies custom className', () => {
    const { container } = render(<LiveLogViewer className="custom-class" />);

    const element = container.querySelector('.custom-class');
    expect(element).toBeTruthy();
  });

  it('shows correct connection status', async () => {
    let mockOnOpen: (() => void) | undefined;
    let mockOnError: ((error: Event) => void) | undefined;

    vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, _onMessage, onOpen, onError) => {
      mockOnOpen = onOpen;
      mockOnError = onError;
      return mockCloseConnection as () => void;
    });

    render(<LiveLogViewer />);

    // Initially disconnected until onOpen is called
    expect(screen.getByText('Disconnected')).toBeTruthy();

    // Simulate connection opened
    if (mockOnOpen) {
      mockOnOpen();
    }

    await waitFor(() => {
      expect(screen.getByText('Connected')).toBeTruthy();
    });

    // Simulate connection error
    if (mockOnError) {
      mockOnError(new Event('error'));
    }

    await waitFor(() => {
      expect(screen.getByText('Disconnected')).toBeTruthy();
    });
  });

  it('shows no-match message when filters exclude all logs', async () => {
    const user = userEvent.setup();
    render(<LiveLogViewer />);

    if (mockOnMessage) {
      mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Visible' });
      mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Hidden' });
    }

    await waitFor(() => expect(screen.getByText('Visible')).toBeTruthy());

    await user.type(screen.getByPlaceholderText('Filter by text...'), 'nomatch');

    await waitFor(() => {
      expect(screen.getByText('No logs match the current filters.')).toBeTruthy();
    });
  });

  it('marks connection as disconnected when WebSocket closes', async () => {
    render(<LiveLogViewer />);

    await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

    act(() => {
      mockOnClose?.();
    });

    await waitFor(() => expect(screen.getByText('Disconnected')).toBeTruthy());
  });

  // ============================================================
  // Security Mode Tests
  // ============================================================

  describe('Security Mode', () => {
    it('renders in security mode when mode="security"', async () => {
      render(<LiveLogViewer mode="security" />);

      expect(screen.getByText('Security Access Logs')).toBeTruthy();
      expect(logsApi.connectSecurityLogs).toHaveBeenCalled();
    });

    it('displays security log entries with source badges', async () => {
      render(<LiveLogViewer mode="security" />);

      // Wait for connection to establish
      await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

      const securityLog: logsApi.SecurityLogEntry = {
        timestamp: '2025-12-12T10:30:00Z',
        level: 'info',
        logger: 'http.log.access',
        client_ip: '192.168.1.100',
        method: 'GET',
        uri: '/api/test',
        status: 200,
        duration: 0.05,
        size: 1024,
        user_agent: 'TestAgent/1.0',
        host: 'example.com',
        source: 'normal',
        blocked: false,
      };

      if (mockOnSecurityMessage) {
        mockOnSecurityMessage(securityLog);
      }

      await waitFor(() => {
        expect(screen.getByText('NORMAL')).toBeTruthy();
        expect(screen.getByText('192.168.1.100')).toBeTruthy();
        expect(screen.getByText(/GET \/api\/test → 200/)).toBeTruthy();
      });
    });

    it('displays blocked requests with special styling', async () => {
      render(<LiveLogViewer mode="security" />);

      // Wait for connection to establish
      await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

      const blockedLog: logsApi.SecurityLogEntry = {
        timestamp: '2025-12-12T10:30:00Z',
        level: 'warn',
        logger: 'http.handlers.waf',
        client_ip: '10.0.0.1',
        method: 'POST',
        uri: '/admin',
        status: 403,
        duration: 0.001,
        size: 0,
        user_agent: 'Attack/1.0',
        host: 'example.com',
        source: 'waf',
        blocked: true,
        block_reason: 'SQL injection detected',
      };

      // Send message inside act to properly handle state updates
      await act(async () => {
        if (mockOnSecurityMessage) {
          mockOnSecurityMessage(blockedLog);
        }
      });

      // Use findBy queries (built-in waiting) instead of single waitFor with multiple assertions
      // This avoids race conditions where one failing assertion causes the entire block to retry
      await screen.findByText('10.0.0.1');
      await screen.findByText(/BLOCKED: SQL injection detected/);
      await screen.findByText(/\[SQL injection detected\]/);

      // For getAllByText, keep in waitFor but separate from other assertions
      await waitFor(() => {
        // Use getAllByText since 'WAF' appears both in dropdown option and source badge
        const wafElements = screen.getAllByText('WAF');
        expect(wafElements.length).toBeGreaterThanOrEqual(2); // Option + badge
      });
    }, 15000); // 15 second timeout as safeguard

    it('shows source filter dropdown in security mode', async () => {
      render(<LiveLogViewer mode="security" />);

      // Should have source filter options
      expect(screen.getByText('All Sources')).toBeTruthy();
      expect(screen.getByRole('option', { name: 'WAF' })).toBeTruthy();
      expect(screen.getByRole('option', { name: 'CrowdSec' })).toBeTruthy();
      expect(screen.getByRole('option', { name: 'Rate Limit' })).toBeTruthy();
      expect(screen.getByRole('option', { name: 'ACL' })).toBeTruthy();
    });

    it('filters by source in security mode', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="security" />);

      // Wait for connection
      await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

      // Add logs from different sources
      if (mockOnSecurityMessage) {
        mockOnSecurityMessage({
          timestamp: '2025-12-12T10:30:00Z',
          level: 'info',
          logger: 'http.log.access',
          client_ip: '192.168.1.1',
          method: 'GET',
          uri: '/normal-request',
          status: 200,
          duration: 0.01,
          size: 100,
          user_agent: 'Test/1.0',
          host: 'example.com',
          source: 'normal',
          blocked: false,
        });
        mockOnSecurityMessage({
          timestamp: '2025-12-12T10:30:01Z',
          level: 'warn',
          logger: 'http.handlers.waf',
          client_ip: '10.0.0.1',
          method: 'POST',
          uri: '/waf-blocked',
          status: 403,
          duration: 0.001,
          size: 0,
          user_agent: 'Attack/1.0',
          host: 'example.com',
          source: 'waf',
          blocked: true,
          block_reason: 'WAF block',
        });
      }

      // Wait for logs to appear - normal shows URI, blocked shows block message
      await waitFor(() => {
        expect(screen.getByText(/GET \/normal-request/)).toBeTruthy();
        expect(screen.getByText(/BLOCKED: WAF block/)).toBeTruthy();
      });

      // Filter by WAF using the source dropdown (second combobox after level)
      const sourceSelects = screen.getAllByRole('combobox');
      const sourceFilterSelect = sourceSelects[1]; // Second combobox is source filter

      await user.selectOptions(sourceFilterSelect, 'waf');

      await waitFor(() => {
        expect(screen.queryByText(/GET \/normal-request/)).toBeFalsy();
        expect(screen.getByText(/BLOCKED: WAF block/)).toBeTruthy();
      });
    });

    it('shows blocked only checkbox in security mode', async () => {
      render(<LiveLogViewer mode="security" />);

      expect(screen.getByText('Blocked only')).toBeTruthy();
      expect(screen.getByRole('checkbox')).toBeTruthy();
    });

    it('toggles blocked only filter', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="security" />);

      const checkbox = screen.getByRole('checkbox');
      await user.click(checkbox);

      // Verify checkbox is checked
      expect(checkbox).toBeChecked();
    });

    it('displays duration for security logs', async () => {
      render(<LiveLogViewer mode="security" />);

      // Wait for connection to establish
      await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

      const securityLog: logsApi.SecurityLogEntry = {
        timestamp: '2025-12-12T10:30:00Z',
        level: 'info',
        logger: 'http.log.access',
        client_ip: '192.168.1.100',
        method: 'GET',
        uri: '/api/test',
        status: 200,
        duration: 0.123,
        size: 1024,
        user_agent: 'TestAgent/1.0',
        host: 'example.com',
        source: 'normal',
        blocked: false,
      };

      if (mockOnSecurityMessage) {
        mockOnSecurityMessage(securityLog);
      }

      await waitFor(() => {
        expect(screen.getByText('123.0ms')).toBeTruthy();
      });
    });

    it('displays status code with appropriate color for security logs', async () => {
      render(<LiveLogViewer mode="security" />);

      // Wait for connection to establish
      await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());

      if (mockOnSecurityMessage) {
        mockOnSecurityMessage({
          timestamp: '2025-12-12T10:30:00Z',
          level: 'info',
          logger: 'http.log.access',
          client_ip: '192.168.1.100',
          method: 'GET',
          uri: '/ok',
          status: 200,
          duration: 0.01,
          size: 100,
          user_agent: 'Test/1.0',
          host: 'example.com',
          source: 'normal',
          blocked: false,
        });
      }

      await waitFor(() => {
        expect(screen.getByText('[200]')).toBeTruthy();
      });
    });
  });

  // ============================================================
  // Mode Toggle Tests
  // ============================================================

  describe('Mode Toggle', () => {
    it('switches from application to security mode', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="application" />);

      expect(screen.getByText('Live Security Logs')).toBeTruthy();
      expect(logsApi.connectLiveLogs).toHaveBeenCalled();

      // Click security mode button
      const securityButton = screen.getByTitle('Security access logs');
      await user.click(securityButton);

      await waitFor(() => {
        expect(screen.getByText('Security Access Logs')).toBeTruthy();
        expect(logsApi.connectSecurityLogs).toHaveBeenCalled();
      });
    });

    it('switches from security to application mode', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="security" />);

      expect(screen.getByText('Security Access Logs')).toBeTruthy();

      // Click application mode button
      const appButton = screen.getByTitle('Application logs');
      await user.click(appButton);

      await waitFor(() => {
        expect(screen.getByText('Live Security Logs')).toBeTruthy();
      });
    });

    it('clears logs when switching modes', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="application" />);

      // Add a log in application mode
      if (mockOnMessage) {
        mockOnMessage({ level: 'info', timestamp: '2025-12-12T10:30:00Z', message: 'App log' });
      }

      await waitFor(() => {
        expect(screen.getByText('App log')).toBeTruthy();
      });

      // Switch to security mode
      const securityButton = screen.getByTitle('Security access logs');
      await user.click(securityButton);

      await waitFor(() => {
        expect(screen.queryByText('App log')).toBeFalsy();
        expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
      });
    });

    it('resets filters when switching modes', async () => {
      const user = userEvent.setup();
      render(<LiveLogViewer mode="application" />);

      // Set a filter
      const filterInput = screen.getByPlaceholderText('Filter by text...');
      await user.type(filterInput, 'test');

      // Switch to security mode
      const securityButton = screen.getByTitle('Security access logs');
      await user.click(securityButton);

      await waitFor(() => {
        // Filter should be cleared
        expect(screen.getByPlaceholderText('Filter by text...')).toHaveValue('');
      });
    });
  });
});
