import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { HealthMonitor } from '../src/health.js';
import type { UpstreamConnection, JsonRpcRequest, JsonRpcResponse } from '../src/types.js';

function mockUpstream(name: string, alive = true): UpstreamConnection {
  return {
    name,
    isAlive: vi.fn(() => alive),
    close: vi.fn(async () => {}),
    send: vi.fn(async (req: JsonRpcRequest): Promise<JsonRpcResponse> => ({
      jsonrpc: '2.0',
      id: req.id,
      error: { code: -32601, message: 'Method not found' },
    })),
  };
}

describe('HealthMonitor', () => {
  let monitor: HealthMonitor;

  beforeEach(() => {
    monitor = new HealthMonitor({ intervalMs: 100, failureThreshold: 2 });
  });

  afterEach(() => {
    monitor.stop();
  });

  it('tracks upstream and reports initial healthy state', () => {
    const upstream = mockUpstream('slack');
    monitor.track(upstream);

    const status = monitor.getStatus('slack');
    expect(status).toBeDefined();
    expect(status?.state).toBe('healthy');
    expect(status?.consecutiveFailures).toBe(0);
  });

  it('reports disconnected for dead upstream on track', () => {
    const upstream = mockUpstream('slack', false);
    monitor.track(upstream);

    expect(monitor.getStatus('slack')?.state).toBe('disconnected');
  });

  it('marks upstream healthy when ping succeeds', async () => {
    const upstream = mockUpstream('slack');
    monitor.track(upstream);

    await monitor.checkAll();

    expect(monitor.isHealthy('slack')).toBe(true);
    expect(monitor.getStatus('slack')?.consecutiveFailures).toBe(0);
  });

  it('marks upstream degraded after one failure', async () => {
    const upstream = mockUpstream('slack');
    vi.mocked(upstream.send).mockRejectedValue(new Error('timeout'));
    monitor.track(upstream);

    await monitor.checkAll();

    expect(monitor.getStatus('slack')?.state).toBe('degraded');
    expect(monitor.getStatus('slack')?.consecutiveFailures).toBe(1);
  });

  it('marks upstream disconnected after threshold failures', async () => {
    const upstream = mockUpstream('slack');
    vi.mocked(upstream.send).mockRejectedValue(new Error('timeout'));
    monitor.track(upstream);

    await monitor.checkAll();
    await monitor.checkAll();

    expect(monitor.getStatus('slack')?.state).toBe('disconnected');
    expect(monitor.getStatus('slack')?.consecutiveFailures).toBe(2);
  });

  it('recovers from degraded to healthy', async () => {
    const upstream = mockUpstream('slack');
    vi.mocked(upstream.send).mockRejectedValueOnce(new Error('timeout'));
    monitor.track(upstream);

    await monitor.checkAll();
    expect(monitor.getStatus('slack')?.state).toBe('degraded');

    // Next check succeeds
    await monitor.checkAll();
    expect(monitor.getStatus('slack')?.state).toBe('healthy');
    expect(monitor.getStatus('slack')?.consecutiveFailures).toBe(0);
  });

  it('emits change events on state transitions', async () => {
    const upstream = mockUpstream('slack');
    vi.mocked(upstream.send).mockRejectedValue(new Error('timeout'));
    monitor.track(upstream);

    const changes: Array<{ name: string; oldState: string; newState: string }> = [];
    monitor.on('change', (change) => changes.push(change));

    await monitor.checkAll();
    expect(changes).toHaveLength(1);
    expect(changes[0]).toEqual({ name: 'slack', oldState: 'healthy', newState: 'degraded' });

    await monitor.checkAll();
    expect(changes).toHaveLength(2);
    expect(changes[1]).toEqual({ name: 'slack', oldState: 'degraded', newState: 'disconnected' });
  });

  it('does not emit when state stays the same', async () => {
    const upstream = mockUpstream('slack');
    monitor.track(upstream);

    const changes: unknown[] = [];
    monitor.on('change', (change) => changes.push(change));

    await monitor.checkAll();
    await monitor.checkAll();

    expect(changes).toHaveLength(0);
  });

  it('reports disconnected when upstream is not alive', async () => {
    const upstream = mockUpstream('slack');
    monitor.track(upstream);

    vi.mocked(upstream.isAlive).mockReturnValue(false);

    const changes: Array<{ name: string; oldState: string; newState: string }> = [];
    monitor.on('change', (change) => changes.push(change));

    await monitor.checkAll();
    expect(monitor.getStatus('slack')?.state).toBe('disconnected');
    expect(changes).toHaveLength(1);
  });

  it('returns all statuses', () => {
    monitor.track(mockUpstream('slack'));
    monitor.track(mockUpstream('github'));

    const statuses = monitor.getAllStatuses();
    expect(statuses).toHaveLength(2);
    expect(statuses.map((s) => s.name)).toEqual(['slack', 'github']);
  });

  it('untracks upstream', () => {
    monitor.track(mockUpstream('slack'));
    monitor.untrack('slack');

    expect(monitor.getStatus('slack')).toBeUndefined();
    expect(monitor.getAllStatuses()).toHaveLength(0);
  });
});
