jest.mock('fs', () => ({
  existsSync: jest.fn().mockReturnValue(false),
  readFileSync: jest.fn().mockReturnValue('{"services":{}}'),
  writeFileSync: jest.fn(),
}));

jest.useFakeTimers();

describe('HealthChecker', () => {
  let HealthChecker, healthChecker, fs;

  beforeEach(() => {
    jest.resetModules();
    fs = require('fs');
    fs.existsSync.mockReturnValue(false);
    fs.readFileSync.mockReturnValue('{"services":{}}');
    fs.writeFileSync.mockImplementation(() => {});

    // Fresh instance each test
    HealthChecker = require('../src/monitoring/health-checker').constructor;
    healthChecker = new HealthChecker();
  });

  afterEach(() => {
    healthChecker.stop();
    jest.clearAllTimers();
  });

  describe('constructor', () => {
    it('initializes with empty state', () => {
      expect(healthChecker.currentStatus).toBeInstanceOf(Map);
      expect(healthChecker.incidents).toEqual([]);
      expect(healthChecker.checking).toBe(false);
    });

    it('loads config from file when it exists', () => {
      jest.resetModules();
      fs = require('fs');
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify({
        services: { svc1: { url: 'http://test.local', enabled: true } }
      }));

      HealthChecker = require('../src/monitoring/health-checker').constructor;
      const hc = new HealthChecker();
      expect(hc.config.services.svc1).toBeDefined();
    });

    it('returns default config on parse error', () => {
      jest.resetModules();
      fs = require('fs');
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue('invalid json');

      HealthChecker = require('../src/monitoring/health-checker').constructor;
      const hc = new HealthChecker();
      expect(hc.config).toEqual({ services: {} });
    });
  });

  describe('start / stop', () => {
    it('start sets checking to true and schedules interval', () => {
      // Mock checkAll to prevent real HTTP calls
      healthChecker.checkAll = jest.fn();
      healthChecker.start();
      expect(healthChecker.checking).toBe(true);
      expect(healthChecker.checkAll).toHaveBeenCalled();
    });

    it('start is idempotent (no-op if already checking)', () => {
      healthChecker.checkAll = jest.fn();
      healthChecker.start();
      healthChecker.start(); // second call
      expect(healthChecker.checkAll).toHaveBeenCalledTimes(1);
    });

    it('stop clears interval and resets state', () => {
      healthChecker.checkAll = jest.fn();
      healthChecker.start();
      healthChecker.stop();
      expect(healthChecker.checking).toBe(false);
      expect(healthChecker.checkInterval).toBeNull();
    });

    it('stop is idempotent (no-op if not checking)', () => {
      healthChecker.stop(); // should not throw
      expect(healthChecker.checking).toBe(false);
    });
  });

  describe('getBackoffInterval', () => {
    it('returns base interval when no failures', () => {
      const interval = healthChecker.getBackoffInterval('svc1');
      expect(interval).toBe(30000); // CHECK_INTERVAL default
    });

    it('doubles interval per consecutive failure', () => {
      healthChecker.consecutiveFailures.set('svc1', 1);
      expect(healthChecker.getBackoffInterval('svc1')).toBe(60000);

      healthChecker.consecutiveFailures.set('svc1', 2);
      expect(healthChecker.getBackoffInterval('svc1')).toBe(120000);
    });

    it('caps at MAX_CHECK_INTERVAL', () => {
      healthChecker.consecutiveFailures.set('svc1', 100);
      expect(healthChecker.getBackoffInterval('svc1')).toBe(300000);
    });
  });

  describe('evaluateHealth', () => {
    it('returns true for expected status code', () => {
      const result = healthChecker.evaluateHealth(200, '', { expectedStatusCodes: [200] });
      expect(result).toBe(true);
    });

    it('returns false for unexpected status code', () => {
      const result = healthChecker.evaluateHealth(500, '', { expectedStatusCodes: [200] });
      expect(result).toBe(false);
    });

    it('defaults to accepting common 2xx/3xx codes', () => {
      expect(healthChecker.evaluateHealth(200, '', {})).toBe(true);
      expect(healthChecker.evaluateHealth(301, '', {})).toBe(true);
      expect(healthChecker.evaluateHealth(500, '', {})).toBe(false);
    });

    it('checks body pattern with regex', () => {
      const config = { expectedBodyPattern: 'ok|healthy' };
      expect(healthChecker.evaluateHealth(200, 'status: ok', config)).toBe(true);
      expect(healthChecker.evaluateHealth(200, 'status: error', config)).toBe(false);
    });

    it('checks body contains text', () => {
      const config = { expectedBodyContains: 'alive' };
      expect(healthChecker.evaluateHealth(200, 'I am alive!', config)).toBe(true);
      expect(healthChecker.evaluateHealth(200, 'dead', config)).toBe(false);
    });
  });

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

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

    it('emits status-check event', () => {
      const handler = jest.fn();
      healthChecker.on('status-check', handler);
      const status = { serviceId: 'svc1', status: 'up' };
      healthChecker.recordStatus('svc1', status);
      expect(handler).toHaveBeenCalledWith(status);
    });
  });

  describe('checkService', () => {
    it('returns up status on successful health check', async () => {
      healthChecker._doRequest = jest.fn().mockResolvedValue({
        healthy: true, statusCode: 200, message: 'Service is healthy', details: {}
      });

      const config = { url: 'http://test.local' };
      const result = await healthChecker.checkService('svc1', config);
      expect(result.status).toBe('up');
      expect(result.serviceId).toBe('svc1');
    });

    it('returns down status on failed health check', async () => {
      healthChecker._doRequest = jest.fn().mockResolvedValue({
        healthy: false, statusCode: 500, message: 'fail', details: {}
      });

      const result = await healthChecker.checkService('svc1', { url: 'http://test.local' });
      expect(result.status).toBe('down');
    });

    it('returns down status on request error', async () => {
      healthChecker._doRequest = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));

      const result = await healthChecker.checkService('svc1', { url: 'http://test.local' });
      expect(result.status).toBe('down');
      expect(result.error).toBe('ECONNREFUSED');
    });

    it('increments consecutive failures on error', async () => {
      healthChecker._doRequest = jest.fn().mockRejectedValue(new Error('fail'));

      await healthChecker.checkService('svc1', { url: 'http://test.local' });
      expect(healthChecker.consecutiveFailures.get('svc1')).toBe(1);

      await healthChecker.checkService('svc1', { url: 'http://test.local' });
      expect(healthChecker.consecutiveFailures.get('svc1')).toBe(2);
    });

    it('clears consecutive failures on success', async () => {
      healthChecker.consecutiveFailures.set('svc1', 5);
      healthChecker._doRequest = jest.fn().mockResolvedValue({
        healthy: true, statusCode: 200, message: 'ok', details: {}
      });

      await healthChecker.checkService('svc1', { url: 'http://test.local' });
      expect(healthChecker.consecutiveFailures.has('svc1')).toBe(false);
    });
  });

  describe('performHealthCheck', () => {
    it('falls back to GET when HEAD returns 501', async () => {
      healthChecker._doRequest = jest.fn()
        .mockResolvedValueOnce({ statusCode: 501 })
        .mockResolvedValueOnce({ healthy: true, statusCode: 200 });

      const result = await healthChecker.performHealthCheck({ url: 'http://test.local', method: 'HEAD' });
      expect(healthChecker._doRequest).toHaveBeenCalledTimes(2);
      expect(result.statusCode).toBe(200);
    });

    it('falls back to GET when HEAD returns 405', async () => {
      healthChecker._doRequest = jest.fn()
        .mockResolvedValueOnce({ statusCode: 405 })
        .mockResolvedValueOnce({ healthy: true, statusCode: 200 });

      const result = await healthChecker.performHealthCheck({ url: 'http://test.local', method: 'HEAD' });
      expect(result.statusCode).toBe(200);
    });

    it('does not fallback for GET requests returning 501', async () => {
      healthChecker._doRequest = jest.fn()
        .mockResolvedValueOnce({ statusCode: 501, healthy: false });

      const result = await healthChecker.performHealthCheck({ url: 'http://test.local' });
      expect(healthChecker._doRequest).toHaveBeenCalledTimes(1);
    });
  });

  describe('incidents', () => {
    it('createIncident adds a new incident', () => {
      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');
    });

    it('createIncident increments 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).toHaveLength(1);
      expect(healthChecker.incidents[0].occurrences).toBe(2);
    });

    it('resolveIncident sets status to resolved', () => {
      const status = { timestamp: new Date().toISOString() };
      healthChecker.createIncident('svc1', 'outage', 'down', status);
      healthChecker.resolveIncident('svc1', 'outage', status);
      expect(healthChecker.incidents[0].status).toBe('resolved');
      expect(healthChecker.incidents[0].resolvedAt).toBeDefined();
    });

    it('resolveIncident is no-op for non-existent incidents', () => {
      const status = { timestamp: new Date().toISOString() };
      healthChecker.resolveIncident('svc1', 'outage', status);
      expect(healthChecker.incidents).toHaveLength(0);
    });

    it('getOpenIncidents filters resolved', () => {
      const ts = { timestamp: new Date().toISOString() };
      healthChecker.createIncident('svc1', 'outage', 'down', ts);
      healthChecker.createIncident('svc2', 'slow-response', 'slow', ts);
      healthChecker.resolveIncident('svc1', 'outage', ts);

      const open = healthChecker.getOpenIncidents();
      expect(open).toHaveLength(1);
      expect(open[0].serviceId).toBe('svc2');
    });

    it('getIncidentHistory returns recent incidents in reverse order', () => {
      const ts = { timestamp: new Date().toISOString() };
      healthChecker.createIncident('svc1', 'outage', 'first', ts);
      healthChecker.createIncident('svc2', 'outage', 'second', ts);

      const history = healthChecker.getIncidentHistory();
      expect(history[0].serviceId).toBe('svc2');
      expect(history[1].serviceId).toBe('svc1');
    });

    it('emits incident-created event', () => {
      const handler = jest.fn();
      healthChecker.on('incident-created', handler);
      healthChecker.createIncident('svc1', 'outage', 'down', { timestamp: new Date().toISOString() });
      expect(handler).toHaveBeenCalled();
    });

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

  describe('calculateSeverity', () => {
    it('returns critical for outage', () => {
      expect(healthChecker.calculateSeverity('outage')).toBe('critical');
    });
    it('returns high for sla-violation', () => {
      expect(healthChecker.calculateSeverity('sla-violation')).toBe('high');
    });
    it('returns medium for slow-response', () => {
      expect(healthChecker.calculateSeverity('slow-response')).toBe('medium');
    });
    it('returns low for unknown', () => {
      expect(healthChecker.calculateSeverity('unknown')).toBe('low');
    });
  });

  describe('checkForIncidents', () => {
    it('creates outage incident on status change up -> down', () => {
      // Simulate previous up status
      healthChecker.currentStatus.set('svc1', { status: 'up' });
      const status = { status: 'down', timestamp: new Date().toISOString(), responseTime: 100 };
      healthChecker.checkForIncidents('svc1', status, {});
      expect(healthChecker.incidents).toHaveLength(1);
      expect(healthChecker.incidents[0].type).toBe('outage');
    });

    it('resolves outage incident on status change down -> up', () => {
      healthChecker.currentStatus.set('svc1', { status: 'down' });
      const ts = { timestamp: new Date().toISOString() };
      healthChecker.createIncident('svc1', 'outage', 'was down', ts);

      const status = { status: 'up', timestamp: new Date().toISOString(), responseTime: 100 };
      healthChecker.checkForIncidents('svc1', status, {});
      expect(healthChecker.incidents[0].status).toBe('resolved');
    });

    it('creates slow-response incident when exceeding threshold', () => {
      const status = { status: 'up', timestamp: new Date().toISOString(), responseTime: 6000 };
      healthChecker.checkForIncidents('svc1', status, { slowResponseThreshold: 5000 });
      expect(healthChecker.incidents.some(i => i.type === 'slow-response')).toBe(true);
    });
  });

  describe('uptime and stats', () => {
    beforeEach(() => {
      const now = Date.now();
      healthChecker.history['svc1'] = [
        { status: 'up', responseTime: 100, timestamp: new Date(now - 3600000).toISOString() },
        { status: 'up', responseTime: 200, timestamp: new Date(now - 1800000).toISOString() },
        { status: 'down', responseTime: 5000, timestamp: new Date(now - 900000).toISOString() },
        { status: 'up', responseTime: 150, timestamp: new Date(now - 60000).toISOString() },
      ];
    });

    it('calculateUptime returns correct percentage', () => {
      const uptime = healthChecker.calculateUptime('svc1', 24);
      expect(uptime).toBe(75); // 3 out of 4 checks up
    });

    it('calculateUptime returns 100 for unknown service', () => {
      expect(healthChecker.calculateUptime('unknown', 24)).toBe(100);
    });

    it('calculateAverageResponseTime returns correct average', () => {
      const avg = healthChecker.calculateAverageResponseTime('svc1', 24);
      expect(avg).toBe((100 + 200 + 5000 + 150) / 4);
    });

    it('calculateAverageResponseTime returns 0 for unknown service', () => {
      expect(healthChecker.calculateAverageResponseTime('unknown', 24)).toBe(0);
    });

    it('getServiceHistory filters by time period', () => {
      const history = healthChecker.getServiceHistory('svc1', 24);
      expect(history.length).toBe(4);

      // Very short period should exclude older entries
      const recent = healthChecker.getServiceHistory('svc1', 0.01); // ~36 seconds
      expect(recent.length).toBeLessThan(4);
    });

    it('getServiceStats returns null for unknown service', () => {
      expect(healthChecker.getServiceStats('unknown')).toBeNull();
    });

    it('getServiceStats returns correct stats', () => {
      const stats = healthChecker.getServiceStats('svc1', 24);
      expect(stats.totalChecks).toBe(4);
      expect(stats.upChecks).toBe(3);
      expect(stats.downChecks).toBe(1);
      expect(stats.uptime).toBe(75);
      expect(stats.responseTime.min).toBe(100);
      expect(stats.responseTime.max).toBe(5000);
    });
  });

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

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

  describe('getCurrentStatus', () => {
    it('returns enriched status for all services', () => {
      healthChecker.config.services = {
        svc1: { name: 'Test Service' }
      };
      healthChecker.currentStatus.set('svc1', {
        status: 'up', responseTime: 100, timestamp: new Date().toISOString()
      });

      const result = healthChecker.getCurrentStatus();
      expect(result.svc1).toBeDefined();
      expect(result.svc1.name).toBe('Test Service');
      expect(result.svc1.uptime).toBeDefined();
      expect(result.svc1.uptime['24h']).toBeDefined();
    });
  });

  describe('configureService / removeService', () => {
    it('configureService saves config to file', () => {
      healthChecker.configureService('svc1', {
        name: 'My Service',
        url: 'http://localhost:3000',
        timeout: 10000
      });

      expect(healthChecker.config.services.svc1).toBeDefined();
      expect(healthChecker.config.services.svc1.url).toBe('http://localhost:3000');
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('removeService cleans up all traces', () => {
      healthChecker.configureService('svc1', { url: 'http://test.local' });
      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('cleanupHistory', () => {
    it('removes entries older than retention period', () => {
      const old = new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(); // 35 days ago
      const recent = new Date().toISOString();
      healthChecker.history['svc1'] = [
        { timestamp: old, status: 'up' },
        { timestamp: recent, status: 'up' },
      ];

      healthChecker.cleanupHistory();
      expect(healthChecker.history['svc1']).toHaveLength(1);
      expect(healthChecker.history['svc1'][0].timestamp).toBe(recent);
    });
  });

  describe('loadConfig / saveConfig', () => {
    it('saveConfig writes JSON to file', () => {
      healthChecker.config = { services: { svc1: { url: 'http://test' } } };
      healthChecker.saveConfig();
      expect(fs.writeFileSync).toHaveBeenCalledWith(
        expect.any(String),
        expect.stringContaining('"svc1"')
      );
    });

    it('saveConfig handles write errors gracefully', () => {
      fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
      expect(() => healthChecker.saveConfig()).not.toThrow();
    });
  });

  describe('loadHistory / saveHistory', () => {
    it('loadHistory returns empty object when file missing', () => {
      const history = healthChecker.loadHistory();
      expect(history).toEqual({});
    });

    it('loadHistory parses JSON from file', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify({ svc1: [{ status: 'up' }] }));
      const history = healthChecker.loadHistory();
      expect(history.svc1).toHaveLength(1);
    });

    it('saveHistory writes history to file', () => {
      healthChecker.history = { svc1: [{ status: 'up' }] };
      healthChecker.saveHistory();
      expect(fs.writeFileSync).toHaveBeenCalled();
    });
  });
});
