/**
 * Tests for DC-090: outage incidents follow the DISPLAYED (post-hysteresis)
 * status — the same signal that flips the dashboard badge.
 *
 *   - A single raw "down" blip that hysteresis suppresses opens NO outage
 *     incident (the DC-089-noted raw-transition bug).
 *   - A suppressed blip does not resolve a real open outage (UP_THRESHOLD=2).
 *   - DOWN_THRESHOLD consecutive downs open exactly ONE outage incident.
 *   - The incident payload carries the displayed snapshot, not the raw probe.
 *   - Direct callers without hysteresis state keep legacy raw semantics.
 *
 * The probe() helper replicates checkService's exact call order: capture the
 * pre-probe raw + displayed state, recordStatus (updates both maps), then
 * checkForIncidents with both previous states.
 */

'use strict';

const path = require('path');
const fs = require('fs');
const os = require('os');

// Use an isolated data dir so test history doesn't pollute the real one.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashcaddy-incpar-'));
process.env.HEALTH_DATA_DIR = tmpDir;
process.env.HEALTH_CONFIG_FILE = path.join(tmpDir, 'health-config.json');
process.env.HEALTH_HISTORY_FILE = path.join(tmpDir, 'health-history.json');

// Module exports a singleton instance, not a class. Reset per-test state by
// replacing the relevant maps on the singleton in beforeEach.
const healthCheckerSingleton = require('../src/monitoring/health-checker');
const originalDownThreshold = process.env.HEALTH_DOWN_THRESHOLD;
const originalUpThreshold = process.env.HEALTH_UP_THRESHOLD;

function restoreEnv(name, value) {
  if (value === undefined) delete process.env[name];
  else process.env[name] = value;
}

function makeUp(serviceId = 'svc1') {
  return {
    serviceId,
    timestamp: new Date().toISOString(),
    status: 'up',
    responseTime: 50,
    statusCode: 200,
    message: 'Service is healthy',
    details: { headers: {}, bodyLength: 12 }
  };
}

function makeDown(serviceId = 'svc1') {
  return {
    serviceId,
    timestamp: new Date().toISOString(),
    status: 'down',
    responseTime: 50,
    statusCode: 500,
    message: 'fail',
    details: { headers: {}, bodyLength: 0 }
  };
}

describe('DC-090: outage incidents follow the displayed (hysteresis) status', () => {
  let hc;
  let incidentCreatedSpy;
  let incidentResolvedSpy;

  beforeEach(() => {
    healthCheckerSingleton.displayedStatus = new Map();
    healthCheckerSingleton.consecutiveSinceChange = new Map();
    healthCheckerSingleton.currentStatus = new Map();
    healthCheckerSingleton.history = {};
    healthCheckerSingleton.incidents = [];
    healthCheckerSingleton.removeAllListeners('incident-created');
    healthCheckerSingleton.removeAllListeners('incident-resolved');
    incidentCreatedSpy = jest.fn();
    incidentResolvedSpy = jest.fn();
    healthCheckerSingleton.on('incident-created', incidentCreatedSpy);
    healthCheckerSingleton.on('incident-resolved', incidentResolvedSpy);
    hc = healthCheckerSingleton;
  });

  afterEach(() => {
    restoreEnv('HEALTH_DOWN_THRESHOLD', originalDownThreshold);
    restoreEnv('HEALTH_UP_THRESHOLD', originalUpThreshold);
  });

  afterAll(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  // Replicates checkService's record+incident sequence for one raw probe.
  function probe(status, config = {}) {
    const previousStatus = hc.currentStatus.get(status.serviceId);
    const previousDisplayed = hc.displayedStatus.get(status.serviceId) || null;
    hc.recordStatus(status.serviceId, status);
    hc.checkForIncidents(status.serviceId, status, config, previousStatus, previousDisplayed);
  }

  test('a single down blip between two ups opens NO outage incident', () => {
    probe(makeUp());      // baseline: displayed up
    probe(makeDown());    // blip — hysteresis keeps displayed up
    probe(makeUp());      // recovered
    expect(hc.incidents).toHaveLength(0);
    expect(incidentCreatedSpy).not.toHaveBeenCalled();
  });

  test('DOWN_THRESHOLD consecutive downs open exactly one outage incident (critical)', () => {
    probe(makeUp());
    probe(makeDown());    // counter=1, displayed still up
    probe(makeDown());    // counter=2 → displayed flips down → incident
    expect(hc.incidents).toHaveLength(1);
    const incident = hc.incidents[0];
    expect(incident.type).toBe('outage');
    expect(incident.severity).toBe('critical');
    expect(incident.status).toBe('open');
    expect(incidentCreatedSpy).toHaveBeenCalledTimes(1);

    probe(makeDown());    // still down — no new transition, no second incident
    expect(hc.incidents).toHaveLength(1);
    expect(incident.occurrences).toBe(1);  // occurrences count displayed flips, not raw probes
    expect(incidentCreatedSpy).toHaveBeenCalledTimes(1);
  });

  test('the outage incident payload carries the displayed snapshot, not the raw blip', () => {
    probe(makeUp());
    const blip = makeDown();
    blip.statusCode = 599;
    probe(blip);          // suppressed blip — must not appear in any incident
    probe(makeDown());    // flip
    expect(hc.incidents).toHaveLength(1);
    // The incident's details snapshot is the probe that FLIPPED the displayed
    // state (the second down), not the earlier suppressed blip.
    expect(hc.incidents[0].details.statusCode).not.toBe(599);
  });

  test('a suppressed up blip does not resolve a real open outage (UP_THRESHOLD=2)', () => {
    process.env.HEALTH_UP_THRESHOLD = '2';
    jest.resetModules();
    const hc2 = require('../src/monitoring/health-checker');
    hc2.displayedStatus = new Map();
    hc2.consecutiveSinceChange = new Map();
    hc2.currentStatus = new Map();
    hc2.history = {};
    hc2.incidents = [];
    hc2.removeAllListeners('incident-created');
    hc2.removeAllListeners('incident-resolved');

    const p2 = (status) => {
      const prevRaw = hc2.currentStatus.get(status.serviceId);
      const prevDisp = hc2.displayedStatus.get(status.serviceId) || null;
      hc2.recordStatus(status.serviceId, status);
      hc2.checkForIncidents(status.serviceId, status, {}, prevRaw, prevDisp);
    };

    p2(makeUp());
    p2(makeDown());
    p2(makeDown());       // displayed down → outage opens
    expect(hc2.incidents).toHaveLength(1);
    expect(hc2.incidents[0].status).toBe('open');

    p2(makeUp());         // counter=1 < UP_THRESHOLD=2 → displayed still down
    expect(hc2.displayedStatus.get('svc1').status).toBe('down');
    expect(hc2.incidents[0].status).toBe('open');   // NOT resolved by the blip

    p2(makeUp());         // counter=2 → displayed up → incident resolves
    expect(hc2.displayedStatus.get('svc1').status).toBe('up');
    expect(hc2.incidents[0].status).toBe('resolved');
  });

  test('legacy direct callers (no displayed state) keep raw transition semantics', () => {
    hc.currentStatus.set('svc1', { status: 'up' });
    const status = { status: 'down', timestamp: new Date().toISOString(), responseTime: 100 };
    hc.checkForIncidents('svc1', status, {});   // 4-arg call, no previousDisplayed
    expect(hc.incidents).toHaveLength(1);
    expect(hc.incidents[0].type).toBe('outage');
  });

  test('slow-response detection still fires per-probe regardless of hysteresis', () => {
    const slowUp = makeUp();
    slowUp.responseTime = 6000;
    probe(slowUp, { slowResponseThreshold: 5000 });
    expect(hc.incidents.some(i => i.type === 'slow-response')).toBe(true);
  });
});
