// health-checker.js exports a singleton that reads config/history from disk on construction.
// The jest.setup.js suppresses console and the files don't exist in test env, so it falls back to defaults.

const healthChecker = require('../health-checker');

beforeEach(() => {
  // Reset singleton state between tests
  healthChecker.currentStatus = new Map();
  healthChecker.incidents = [];
  healthChecker.history = {};
  healthChecker.config = { services: {} };
  healthChecker.checking = false;
  if (healthChecker.checkInterval) {
    clearInterval(healthChecker.checkInterval);
    healthChecker.checkInterval = null;
  }
});

afterAll(() => {
  healthChecker.stop();
});

describe('evaluateHealth', () => {
  test('returns true for status code in expectedStatusCodes', () => {
    expect(healthChecker.evaluateHealth(200, '', { expectedStatusCodes: [200, 201] })).toBe(true);
  });

  test('returns false for status code not in expectedStatusCodes', () => {
    expect(healthChecker.evaluateHealth(500, '', { expectedStatusCodes: [200] })).toBe(false);
  });

  test('uses default expected codes when not configured', () => {
    expect(healthChecker.evaluateHealth(200, '', {})).toBe(true);
    expect(healthChecker.evaluateHealth(301, '', {})).toBe(true);
    expect(healthChecker.evaluateHealth(500, '', {})).toBe(false);
  });

  test('returns false when expectedBodyPattern regex does not match', () => {
    expect(healthChecker.evaluateHealth(200, 'error occurred', {
      expectedBodyPattern: 'ok|healthy'
    })).toBe(false);
  });

  test('returns true when expectedBodyPattern regex matches', () => {
    expect(healthChecker.evaluateHealth(200, 'status: healthy', {
      expectedBodyPattern: 'healthy'
    })).toBe(true);
  });

  test('returns false when expectedBodyContains text is missing', () => {
    expect(healthChecker.evaluateHealth(200, 'some response', {
      expectedBodyContains: 'healthy'
    })).toBe(false);
  });

  test('returns true when expectedBodyContains text is present', () => {
    expect(healthChecker.evaluateHealth(200, 'service is healthy', {
      expectedBodyContains: 'healthy'
    })).toBe(true);
  });

  test('checks all conditions: status code AND body pattern AND body contains', () => {
    // All pass
    expect(healthChecker.evaluateHealth(200, 'healthy ok', {
      expectedStatusCodes: [200],
      expectedBodyPattern: 'healthy',
      expectedBodyContains: 'ok'
    })).toBe(true);

    // Status fails
    expect(healthChecker.evaluateHealth(500, 'healthy ok', {
      expectedStatusCodes: [200],
      expectedBodyPattern: 'healthy',
      expectedBodyContains: 'ok'
    })).toBe(false);

    // Body pattern fails
    expect(healthChecker.evaluateHealth(200, 'error', {
      expectedStatusCodes: [200],
      expectedBodyPattern: 'healthy',
      expectedBodyContains: 'error'
    })).toBe(false);
  });
});

describe('calculateSeverity', () => {
  test('returns critical for outage', () => {
    expect(healthChecker.calculateSeverity('outage')).toBe('critical');
  });

  test('returns high for sla-violation', () => {
    expect(healthChecker.calculateSeverity('sla-violation')).toBe('high');
  });

  test('returns medium for slow-response', () => {
    expect(healthChecker.calculateSeverity('slow-response')).toBe('medium');
  });

  test('returns low for unknown type', () => {
    expect(healthChecker.calculateSeverity('unknown')).toBe('low');
  });
});

describe('calculateUptime', () => {
  test('returns 100 when no history', () => {
    expect(healthChecker.calculateUptime('svc1')).toBe(100);
  });

  test('returns 100 when all checks are up', () => {
    const now = new Date().toISOString();
    healthChecker.history['svc1'] = [
      { status: 'up', timestamp: now },
      { status: 'up', timestamp: now },
      { status: 'up', timestamp: now },
    ];
    expect(healthChecker.calculateUptime('svc1')).toBe(100);
  });

  test('returns 0 when all checks are down', () => {
    const now = new Date().toISOString();
    healthChecker.history['svc1'] = [
      { status: 'down', timestamp: now },
      { status: 'down', timestamp: now },
    ];
    expect(healthChecker.calculateUptime('svc1')).toBe(0);
  });

  test('returns 50 when half are up', () => {
    const now = new Date().toISOString();
    healthChecker.history['svc1'] = [
      { status: 'up', timestamp: now },
      { status: 'down', timestamp: now },
    ];
    expect(healthChecker.calculateUptime('svc1')).toBe(50);
  });
});

describe('calculateAverageResponseTime', () => {
  test('returns 0 when no history', () => {
    expect(healthChecker.calculateAverageResponseTime('svc1')).toBe(0);
  });

  test('calculates correct average', () => {
    const now = new Date().toISOString();
    healthChecker.history['svc1'] = [
      { responseTime: 100, timestamp: now },
      { responseTime: 200, timestamp: now },
      { responseTime: 300, timestamp: now },
    ];
    expect(healthChecker.calculateAverageResponseTime('svc1')).toBe(200);
  });
});

describe('calculatePercentile', () => {
  test('returns p95 correctly', () => {
    const values = Array.from({ length: 100 }, (_, i) => i + 1);
    expect(healthChecker.calculatePercentile(values, 95)).toBe(95);
  });

  test('returns p99 correctly', () => {
    const values = Array.from({ length: 100 }, (_, i) => i + 1);
    expect(healthChecker.calculatePercentile(values, 99)).toBe(99);
  });

  test('returns 0 for empty array', () => {
    expect(healthChecker.calculatePercentile([], 95)).toBe(0);
  });

  test('handles single-element array', () => {
    expect(healthChecker.calculatePercentile([42], 95)).toBe(42);
  });

  test('sorts values before calculating', () => {
    const unsorted = [50, 10, 90, 30, 70, 20, 80, 40, 60, 100];
    expect(healthChecker.calculatePercentile(unsorted, 50)).toBe(50);
  });
});

describe('recordStatus', () => {
  test('adds status to currentStatus map', () => {
    const status = { serviceId: 'svc1', status: 'up', timestamp: new Date().toISOString() };
    healthChecker.recordStatus('svc1', status);
    expect(healthChecker.currentStatus.get('svc1')).toEqual(status);
  });

  test('creates history array for new serviceId', () => {
    const status = { serviceId: 'new-svc', status: 'up', timestamp: new Date().toISOString() };
    healthChecker.recordStatus('new-svc', status);
    expect(healthChecker.history['new-svc']).toHaveLength(1);
  });

  test('appends to existing history', () => {
    healthChecker.history['svc1'] = [{ status: 'up', timestamp: new Date().toISOString() }];
    const status = { status: 'down', timestamp: new Date().toISOString() };
    healthChecker.recordStatus('svc1', status);
    expect(healthChecker.history['svc1']).toHaveLength(2);
  });

  test('emits status-check event', () => {
    const handler = jest.fn();
    healthChecker.on('status-check', handler);
    healthChecker.recordStatus('svc1', { status: 'up', timestamp: new Date().toISOString() });
    expect(handler).toHaveBeenCalled();
    healthChecker.removeListener('status-check', handler);
  });
});

describe('createIncident', () => {
  test('creates incident with correct structure', () => {
    const status = { timestamp: new Date().toISOString() };
    healthChecker.createIncident('svc1', 'outage', 'Service down', status);
    expect(healthChecker.incidents).toHaveLength(1);
    expect(healthChecker.incidents[0].serviceId).toBe('svc1');
    expect(healthChecker.incidents[0].type).toBe('outage');
    expect(healthChecker.incidents[0].status).toBe('open');
    expect(healthChecker.incidents[0].severity).toBe('critical');
    expect(healthChecker.incidents[0].occurrences).toBe(1);
  });

  test('emits incident-created event', () => {
    const handler = jest.fn();
    healthChecker.on('incident-created', handler);
    healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: new Date().toISOString() });
    expect(handler).toHaveBeenCalledWith(expect.objectContaining({ serviceId: 'svc1' }));
    healthChecker.removeListener('incident-created', handler);
  });

  test('does not duplicate open incidents of same type', () => {
    const status = { timestamp: new Date().toISOString() };
    healthChecker.createIncident('svc1', 'outage', 'Down', status);
    healthChecker.createIncident('svc1', 'outage', 'Still down', status);
    expect(healthChecker.incidents).toHaveLength(1);
  });

  test('increments occurrences on existing open incident', () => {
    const status = { timestamp: new Date().toISOString() };
    healthChecker.createIncident('svc1', 'outage', 'Down', status);
    healthChecker.createIncident('svc1', 'outage', 'Still down', status);
    expect(healthChecker.incidents[0].occurrences).toBe(2);
  });
});

describe('resolveIncident', () => {
  test('marks incident as resolved with duration', () => {
    const created = new Date(Date.now() - 60000).toISOString();
    const resolved = new Date().toISOString();
    healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: created });
    healthChecker.resolveIncident('svc1', 'outage', { timestamp: resolved });
    expect(healthChecker.incidents[0].status).toBe('resolved');
    expect(healthChecker.incidents[0].resolvedAt).toBe(resolved);
    expect(healthChecker.incidents[0].duration).toBeGreaterThan(0);
  });

  test('emits incident-resolved event', () => {
    const handler = jest.fn();
    healthChecker.on('incident-resolved', handler);
    const ts = new Date().toISOString();
    healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: ts });
    healthChecker.resolveIncident('svc1', 'outage', { timestamp: ts });
    expect(handler).toHaveBeenCalled();
    healthChecker.removeListener('incident-resolved', handler);
  });

  test('handles no matching incident gracefully', () => {
    // Should not throw
    healthChecker.resolveIncident('nonexistent', 'outage', { timestamp: new Date().toISOString() });
    expect(healthChecker.incidents).toHaveLength(0);
  });
});

describe('configureService / removeService', () => {
  test('adds service config with defaults', () => {
    healthChecker.configureService('svc1', { url: 'http://localhost:3000', name: 'Test' });
    expect(healthChecker.config.services['svc1']).toBeDefined();
    expect(healthChecker.config.services['svc1'].method).toBe('GET');
    expect(healthChecker.config.services['svc1'].timeout).toBe(10000);
  });

  test('removes service and cleans up', () => {
    healthChecker.configureService('svc1', { url: 'http://localhost:3000' });
    healthChecker.currentStatus.set('svc1', { status: 'up' });
    healthChecker.history['svc1'] = [{ status: 'up' }];

    healthChecker.removeService('svc1');
    expect(healthChecker.config.services['svc1']).toBeUndefined();
    expect(healthChecker.currentStatus.has('svc1')).toBe(false);
    expect(healthChecker.history['svc1']).toBeUndefined();
  });
});

describe('getOpenIncidents / getIncidentHistory', () => {
  test('getOpenIncidents returns only open incidents', () => {
    const ts = new Date().toISOString();
    healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: ts });
    healthChecker.createIncident('svc2', 'slow-response', 'Slow', { timestamp: ts });
    healthChecker.resolveIncident('svc1', 'outage', { timestamp: ts });
    expect(healthChecker.getOpenIncidents()).toHaveLength(1);
    expect(healthChecker.getOpenIncidents()[0].serviceId).toBe('svc2');
  });

  test('getIncidentHistory returns reverse chronological order', () => {
    const ts = new Date().toISOString();
    healthChecker.createIncident('svc1', 'outage', 'First', { timestamp: ts });
    healthChecker.createIncident('svc2', 'outage', 'Second', { timestamp: ts });
    const history = healthChecker.getIncidentHistory();
    expect(history[0].serviceId).toBe('svc2');
  });
});

describe('getServiceStats', () => {
  test('returns null for service with no history', () => {
    expect(healthChecker.getServiceStats('nonexistent')).toBeNull();
  });

  test('returns correct stats structure', () => {
    const now = new Date().toISOString();
    healthChecker.history['svc1'] = [
      { status: 'up', responseTime: 100, timestamp: now },
      { status: 'up', responseTime: 200, timestamp: now },
      { status: 'down', responseTime: 0, timestamp: now },
    ];
    const stats = healthChecker.getServiceStats('svc1');
    expect(stats.totalChecks).toBe(3);
    expect(stats.upChecks).toBe(2);
    expect(stats.downChecks).toBe(1);
    expect(stats.responseTime.avg).toBe(100);
    expect(stats.responseTime.min).toBe(0);
    expect(stats.responseTime.max).toBe(200);
    expect(stats.responseTime).toHaveProperty('p95');
    expect(stats.responseTime).toHaveProperty('p99');
  });
});

describe('start / stop', () => {
  test('start sets checking flag', () => {
    jest.useFakeTimers();
    healthChecker.start();
    expect(healthChecker.checking).toBe(true);
    healthChecker.stop();
    jest.useRealTimers();
  });

  test('stop clears interval and checking flag', () => {
    jest.useFakeTimers();
    healthChecker.start();
    healthChecker.stop();
    expect(healthChecker.checking).toBe(false);
    expect(healthChecker.checkInterval).toBeNull();
    jest.useRealTimers();
  });

  test('start is idempotent', () => {
    jest.useFakeTimers();
    healthChecker.start();
    const firstInterval = healthChecker.checkInterval;
    healthChecker.start();
    expect(healthChecker.checkInterval).toBe(firstInterval);
    healthChecker.stop();
    jest.useRealTimers();
  });
});
