/**
 * Tests for sidecar health monitoring.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  startHealthMonitoring,
  stopHealthMonitoring,
  getHealthState,
  getHealthHistory,
  checkHealthNow,
  isHealthy,
  getHealthSummary,
  resetHealthState,
} from './sidecar-monitor.js';

// Mock the python-bridge module
vi.mock('../../lib/python-bridge.js', () => ({
  sidecarHealth: vi.fn(),
}));

import { sidecarHealth } from '../../lib/python-bridge.js';
const mockSidecarHealth = vi.mocked(sidecarHealth);

describe('sidecar-monitor', () => {
  beforeEach(() => {
    stopHealthMonitoring();
    resetHealthState();
    vi.clearAllMocks();
  });

  describe('checkHealthNow', () => {
    it('records successful health check', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok', version: '0.1.0' });

      const check = await checkHealthNow();

      expect(check.status).toBe('healthy');
      expect(check.response).toEqual({ status: 'ok', version: '0.1.0' });
      expect(check.responseTimeMs).toBeGreaterThanOrEqual(0);
      expect(check.error).toBeUndefined();
    });

    it('records failed health check', async () => {
      mockSidecarHealth.mockRejectedValue(new Error('Connection refused'));

      const check = await checkHealthNow();

      expect(check.status).toBe('unhealthy');
      expect(check.error).toBe('Connection refused');
      expect(check.response).toBeUndefined();
    });

    it('updates health state', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      await checkHealthNow();
      const state = getHealthState();

      expect(state.totalChecks).toBe(1);
      expect(state.consecutiveSuccesses).toBe(1);
      expect(state.lastSuccessAt).toBeDefined();
    });
  });

  describe('getHealthState', () => {
    it('returns initial unknown state', () => {
      const state = getHealthState();

      expect(state.current).toBe('unknown');
      expect(state.totalChecks).toBe(0);
      expect(state.consecutiveFailures).toBe(0);
      expect(state.consecutiveSuccesses).toBe(0);
    });

    it('tracks consecutive failures', async () => {
      mockSidecarHealth.mockRejectedValue(new Error('Error'));

      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();

      const state = getHealthState();
      expect(state.consecutiveFailures).toBe(3);
      expect(state.consecutiveSuccesses).toBe(0);
    });

    it('resets consecutive failures on success', async () => {
      mockSidecarHealth
        .mockRejectedValueOnce(new Error('Error'))
        .mockRejectedValueOnce(new Error('Error'))
        .mockResolvedValueOnce({ status: 'ok' });

      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();

      const state = getHealthState();
      expect(state.consecutiveFailures).toBe(0);
      expect(state.consecutiveSuccesses).toBe(1);
    });
  });

  describe('getHealthHistory', () => {
    it('returns limited history', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      for (let i = 0; i < 5; i++) {
        await checkHealthNow();
      }

      const history = getHealthHistory(3);
      expect(history).toHaveLength(3);
    });

    it('returns all available history if less than limit', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      await checkHealthNow();
      await checkHealthNow();

      const history = getHealthHistory(10);
      expect(history).toHaveLength(2);
    });
  });

  describe('isHealthy', () => {
    it('returns false initially', () => {
      expect(isHealthy()).toBe(false);
    });

    it('returns true after successful check', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      await checkHealthNow();

      expect(isHealthy()).toBe(true);
    });

    it('returns false after failures exceed threshold', async () => {
      mockSidecarHealth.mockRejectedValue(new Error('Error'));

      // Default threshold is 3 failures
      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();

      expect(isHealthy()).toBe(false);
    });
  });

  describe('getHealthSummary', () => {
    it('includes all summary fields', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      await checkHealthNow();

      const summary = getHealthSummary();
      expect(summary.status).toBeDefined();
      expect(summary.uptime).toBeDefined();
      expect(summary.availability).toBeDefined();
      expect(summary.avgResponseTimeMs).toBeDefined();
      expect(summary.checkIntervalMs).toBeDefined();
    });

    it('calculates availability percentage', async () => {
      mockSidecarHealth
        .mockResolvedValueOnce({ status: 'ok' })
        .mockResolvedValueOnce({ status: 'ok' })
        .mockRejectedValueOnce(new Error('Error'));

      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();

      const summary = getHealthSummary();
      expect(summary.availability).toBeGreaterThanOrEqual(0);
      expect(summary.availability).toBeLessThanOrEqual(100);
    });
  });

  describe('startHealthMonitoring', () => {
    it('starts periodic checks', () => {
      vi.useFakeTimers({ shouldAdvanceTime: true });
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      startHealthMonitoring();

      // Fast forward past first interval
      vi.advanceTimersByTime(30000);

      // Should have made at least one check
      expect(mockSidecarHealth).toHaveBeenCalled();

      vi.useRealTimers();
    });

    it('calls alert callbacks on state changes', async () => {
      mockSidecarHealth.mockRejectedValue(new Error('Error'));

      const onUnhealthy = vi.fn();
      const onDegraded = vi.fn();

      startHealthMonitoring({ onUnhealthy, onDegraded });

      // Trigger multiple health checks manually
      for (let i = 0; i < 5; i++) {
        await checkHealthNow();
      }

      expect(onUnhealthy).toHaveBeenCalled();
    });
  });

  describe('resetHealthState', () => {
    it('clears all state', async () => {
      mockSidecarHealth.mockResolvedValue({ status: 'ok' });

      await checkHealthNow();
      await checkHealthNow();

      resetHealthState();

      const state = getHealthState();
      expect(state.totalChecks).toBe(0);
      expect(state.current).toBe('unknown');
      expect(state.history).toHaveLength(0);
    });
  });

  describe('alert handlers', () => {
    it('calls onRecovered when health is restored', async () => {
      mockSidecarHealth
        .mockRejectedValueOnce(new Error('Error'))
        .mockRejectedValueOnce(new Error('Error'))
        .mockRejectedValueOnce(new Error('Error'))
        .mockResolvedValueOnce({ status: 'ok' });

      const onRecovered = vi.fn();

      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();
      await checkHealthNow();

      // Manually trigger with recovered handler
      startHealthMonitoring({ onRecovered });

      // State should be healthy after success
      expect(getHealthState().current).toBe('healthy');
    });
  });
});
