/**
 * DC-083: Branch coverage tests for the new /system/health endpoint in routes/health.js.
 *
 * The endpoint at GET /api/system/health aggregates four checks (services, memory,
 * diskSpace, incidents) into an overall status. It has many uncovered branches:
 *  - status === 'ok' / 'degraded' / 'down' in the services check
 *  - status === 'ok' / 'warning' in the memory check
 *  - status === 'ok' / 'warning' / 'critical' in the diskSpace check
 *  - status === 'ok' / 'degraded' in the incidents check
 *  - each check has a try/catch → unknown fallback
 *  - overall status computation (unhealthy / degraded / healthy)
 *
 * Also covers additional uncovered branches in the /health-checks/* endpoints:
 *  - unhealthy filter in /health-checks/status
 *  - incidents open/non-empty
 *  - incidents/history with pagination params
 *  - /health/probe with and without ?url
 *  - /health/services with array vs object services data, error paths
 */
const express = require('express');
const request = require('supertest');

// Minimal asyncHandler that catches errors
function asyncHandler(fn) {
  return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}

// ---- Mocks (mirrors health.routes.test.js) ----
jest.mock('child_process', () => ({ execSync: jest.fn() }));
jest.mock('../../platform-paths', () => ({
  caCertDir: '/mock/ca',
  pkiRootCert: '/mock/pki/root.crt',
  dataDir: '/mock/data',
}));
jest.mock('../../src/utilities/fs-helpers', () => ({ exists: jest.fn().mockResolvedValue(true) }));
jest.mock('../../src/utilities/url-resolver', () => ({
  resolveServiceUrl: jest.fn((id) => `https://${id}.test`),
}));
jest.mock('../../src/utilities/pagination', () => ({
  paginate: jest.fn((data, params) => ({ data, pagination: params ? { page: 1, limit: 10, total: data.length } : null })),
  parsePaginationParams: jest.fn(() => null),
}));

const { exists } = require('../../src/utilities/fs-helpers');
const { resolveServiceUrl } = require('../../src/utilities/url-resolver');
const { execSync } = require('child_process');
const platformPaths = require('../../platform-paths');

function createApp(depsOverride = {}) {
  const defaultDeps = {
    fetchT: jest.fn().mockResolvedValue({ ok: true, status: 200, json: () => ({}) }),
    SERVICES_FILE: '/tmp/services.json',
    servicesStateManager: {
      read: jest.fn().mockResolvedValue([]),
      write: jest.fn().mockResolvedValue(),
      update: jest.fn().mockResolvedValue([]),
    },
    siteConfig: { tld: 'sami' },
    buildServiceUrl: jest.fn(id => `https://${id}.sami`),
    asyncHandler,
    logError: jest.fn(),
    healthChecker: {
      getCurrentStatus: jest.fn().mockReturnValue({}),
      getServiceStats: jest.fn().mockReturnValue(null),
      configureService: jest.fn(),
      removeService: jest.fn(),
      getOpenIncidents: jest.fn().mockReturnValue([]),
      getIncidentHistory: jest.fn().mockReturnValue([]),
    },
  };
  const deps = { ...defaultDeps, ...depsOverride };
  const healthRoutes = require('../../routes/health');
  const app = express();
  app.use(express.json());
  app.use('/api', healthRoutes(deps));
  app.use((err, req, res, next) => {
    const status = err.statusCode || 500;
    res.status(status).json({ success: false, error: err.message });
  });
  return { app, deps };
}

describe('System health endpoint (DC-083)', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    exists.mockResolvedValue(true);
    execSync.mockReturnValue('notAfter=Dec 22 12:00:00 2034 GMT');
  });

  describe('GET /api/system/health', () => {
    it('returns healthy overall when all checks pass', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: { status: 'up' },
          svc2: { status: 'healthy' },
          svc3: { status: 'online' },
        }),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      // disk: 40% used → ok. df output format: header line + data line.
      // parts[0]='40%', parseInt → 40
      execSync.mockReturnValue('Use%  Size  Avail\n 40%  100G  60G');
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.status).toBe(200);
      expect(res.body.status).toBe('healthy');
      expect(res.body.checks.services.status).toBe('ok');
      expect(res.body.checks.services.healthy).toBe(3);
      expect(res.body.checks.memory.status).toBe('ok');
      expect(res.body.checks.diskSpace.status).toBe('ok');
      expect(res.body.checks.incidents.status).toBe('ok');
    });

    it('returns degraded when some services are unhealthy (mixed)', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: { status: 'up' },
          svc2: { status: 'down' },
        }),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.status).toBe(200);
      expect(res.body.checks.services.status).toBe('degraded');
      expect(res.body.checks.services.unhealthy).toBe(1);
      expect(res.body.checks.services.unknown).toBe(0);
      // Overall degraded because services degraded
      expect(res.body.status).toBe('degraded');
    });

    it('returns down when ALL services are unhealthy', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: { status: 'down' },
          svc2: { status: 'offline' },
        }),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.services.status).toBe('down');
      // Overall unhealthy because services down
      expect(res.body.status).toBe('unhealthy');
    });

    it('counts unknown status values (not up/down/healthy/etc.)', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: { state: 'starting' },     // unknown state value
          svc2: { status: 'paused' },      // unknown status value
          svc3: { },                        // no status/state → unknown
        }),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.services.total).toBe(3);
      expect(res.body.checks.services.healthy).toBe(0);
      expect(res.body.checks.services.unhealthy).toBe(0);
      expect(res.body.checks.services.unknown).toBe(3);
    });

    it('returns degraded when incidents are open', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getOpenIncidents: jest.fn().mockReturnValue([{ id: 'inc1' }, { id: 'inc2' }]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.incidents.status).toBe('degraded');
      expect(res.body.checks.incidents.count).toBe(2);
      expect(res.body.status).toBe('degraded');
    });

    it('returns warning when disk usage between 90-95%', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      execSync.mockReturnValue('Use%  Size  Avail\n 92%  100G  8G');
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.diskSpace.status).toBe('warning');
      expect(res.body.checks.diskSpace.usedPercent).toBe(92);
      expect(res.body.status).toBe('degraded');
    });

    it('returns critical when disk usage >= 95%', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      execSync.mockReturnValue('Use%  Size  Avail\n 97%  100G  3G');
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.diskSpace.status).toBe('critical');
      expect(res.body.status).toBe('unhealthy');
    });

    it('falls back to unknown for services when getCurrentStatus throws', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockImplementation(() => { throw new Error('boom'); }),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.services.status).toBe('unknown');
      // unknown → degraded overall
      expect(res.body.status).toBe('degraded');
    });

    it('falls back to unknown for disk when execSync throws', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      execSync.mockImplementation(() => { throw new Error('df failed'); });
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.diskSpace.status).toBe('unknown');
    });

    it('falls back to unknown for incidents when getOpenIncidents throws', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getOpenIncidents: jest.fn().mockImplementation(() => { throw new Error('inc fail'); }),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.incidents.status).toBe('unknown');
      expect(res.body.checks.incidents.count).toBe(0);
    });

    it('sets Cache-Control: no-store header', async () => {
      const { app } = createApp();
      const res = await request(app).get('/api/system/health');
      expect(res.headers['cache-control']).toBe('no-store');
    });

    it('includes uptime block with seconds and human-readable', async () => {
      const { app } = createApp();
      const res = await request(app).get('/api/system/health');
      expect(res.body.checks.uptime).toHaveProperty('seconds');
      expect(res.body.checks.uptime).toHaveProperty('human');
      expect(typeof res.body.checks.uptime.seconds).toBe('number');
    });

    it('handles empty df output (only header line) — no diskSpace block set to ok', async () => {
      // df returns just one line → lines.length < 2 → diskSpace not assigned in try
      // (stays undefined → overall status considers it). Actually the try block
      // does NOT set diskSpace when lines.length < 2, so diskSpace is undefined
      // and Object.values(checks) excludes it. Verify no crash.
      execSync.mockReturnValue('Use%  Size  Avail');
      const { app } = createApp();
      const res = await request(app).get('/api/system/health');
      expect(res.status).toBe(200);
    });
  });

  // ---- Coverage for health-checks/status unhealthy filter ----
  describe('GET /api/health-checks/status — unhealthy filter coverage', () => {
    it('counts unhealthy services via various status/state tokens', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: { status: 'down' },
          svc2: { state: 'unhealthy' },
          svc3: { status: 'offline' },
          svc4: { status: 'error' },
          svc5: { status: 'up' },
        }),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/health-checks/status');
      expect(res.status).toBe(200);
      expect(res.body.summary.unhealthy).toBe(4);
      expect(res.body.summary.healthy).toBe(1);
      expect(res.body.summary.unknown).toBe(0);
      expect(res.body.summary.total).toBe(5);
    });

    it('handles null/undefined status entries', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({
          svc1: null,
          svc2: {},
          svc3: { status: 'up' },
        }),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getOpenIncidents: jest.fn().mockReturnValue([]),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/health-checks/status');
      expect(res.status).toBe(200);
      // null and {} are not healthy or unhealthy → unknown
      expect(res.body.summary.unknown).toBe(2);
      expect(res.body.summary.healthy).toBe(1);
    });
  });

  // ---- Coverage for /health/probe ----
  describe('GET /api/health/probe', () => {
    it('returns 400 when url query param missing', async () => {
      const { app } = createApp();
      const res = await request(app).get('/api/health/probe');
      expect(res.status).toBe(400);
    });

    it('returns probe result when url provided and fetch succeeds', async () => {
      const fetchT = jest.fn().mockResolvedValue({ ok: true, status: 200, json: () => ({}) });
      const { app } = createApp({ fetchT });
      const res = await request(app).get('/api/health/probe?url=https://example.com');
      expect(res.status).toBe(200);
      expect(res.body.status).toBe('healthy');
      expect(res.body.statusCode).toBe(200);
    });

    it('returns unhealthy when probe fetch fails completely', async () => {
      const fetchT = jest.fn().mockRejectedValue(new Error('timeout'));
      const { app } = createApp({ fetchT });
      const res = await request(app).get('/api/health/probe?url=https://down.example');
      expect(res.status).toBe(200);
      expect(res.body.status).toBe('unhealthy');
      expect(res.body.reason).toBe('fetch failed');
    });

    it('marks status as unhealthy when statusCode >= 500', async () => {
      const fetchT = jest.fn().mockResolvedValue({ ok: false, status: 503 });
      const { app } = createApp({ fetchT });
      const res = await request(app).get('/api/health/probe?url=https://500.example');
      expect(res.body.status).toBe('unhealthy');
      expect(res.body.statusCode).toBe(503);
    });

    it('marks status as healthy when statusCode is 401/403 (auth wall)', async () => {
      const fetchT = jest.fn().mockResolvedValue({ ok: false, status: 401 });
      const { app } = createApp({ fetchT });
      const res = await request(app).get('/api/health/probe?url=https://auth.example');
      expect(res.body.status).toBe('healthy');
      expect(res.body.statusCode).toBe(401);
    });
  });

  // ---- Coverage for /health/services with various service shapes ----
  describe('GET /api/health/services — service shape branches', () => {
    it('handles services as object with .services array', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue({ services: [{ id: 'svc1', name: 'S1' }] }),
        write: jest.fn(),
        update: jest.fn(),
      };
      const fetchT = jest.fn().mockResolvedValue({ ok: true, status: 200 });
      const { app } = createApp({ servicesStateManager: stateManager, fetchT });
      const res = await request(app).get('/api/health/services');
      expect(res.status).toBe(200);
      expect(res.body.health).toHaveProperty('svc1');
    });

    it('uses service.name (lowercased) as id when service.id absent', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ name: 'MyService' }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const fetchT = jest.fn().mockResolvedValue({ ok: true, status: 200 });
      const { app } = createApp({ servicesStateManager: stateManager, fetchT });
      const res = await request(app).get('/api/health/services');
      expect(res.status).toBe(200);
      expect(res.body.health).toHaveProperty('myservice');
    });

    it('skips services with no id and no name', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ port: 8080 }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/health/services');
      expect(res.status).toBe(200);
      expect(res.body.health).toEqual({});
    });

    it('marks service as unknown when URL resolves to null', async () => {
      resolveServiceUrl.mockReturnValue(null);
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ id: 'novurl', name: 'No URL' }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/health/services');
      expect(res.body.health.novurl.status).toBe('unknown');
      expect(res.body.health.novurl.reason).toMatch(/No URL/);
      resolveServiceUrl.mockReturnValue('https://fallback.test');
    });

    it('uses pylon relay when direct check fails and pylon configured', async () => {
      // Direct HEAD and GET both throw → falls through to pylon
      const fetchT = jest.fn()
        .mockRejectedValueOnce(new Error('HEAD fail'))   // HEAD
        .mockRejectedValueOnce(new Error('GET fail'))    // GET (fallback in checkDirect)
        .mockResolvedValueOnce({                          // pylon probe
          ok: true, status: 200,
          json: () => ({ status: 'healthy', statusCode: 200, responseTime: 42 }),
        });
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ id: 'svc1', name: 'S1' }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({
        servicesStateManager: stateManager,
        fetchT,
        siteConfig: { tld: 'sami', pylon: { url: 'http://pylon.test', key: 'k' } },
      });
      const res = await request(app).get('/api/health/services');
      expect(res.body.health.svc1.via).toBe('pylon');
      expect(res.body.health.svc1.status).toBe('healthy');
    });

    it('marks unhealthy when both direct and pylon fail (pylon configured)', async () => {
      const fetchT = jest.fn()
        .mockRejectedValueOnce(new Error('HEAD fail'))
        .mockRejectedValueOnce(new Error('GET fail'))
        .mockRejectedValueOnce(new Error('pylon fail'));
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ id: 'svc1', name: 'S1' }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({
        servicesStateManager: stateManager,
        fetchT,
        siteConfig: { tld: 'sami', pylon: { url: 'http://pylon.test' } },
      });
      const res = await request(app).get('/api/health/services');
      expect(res.body.health.svc1.status).toBe('unhealthy');
      expect(res.body.health.svc1.reason).toMatch(/direct \+ pylon/);
    });

    it('catches errors thrown by resolveServiceUrl and marks as error', async () => {
      resolveServiceUrl.mockImplementation(() => { throw new Error('resolver exploded'); });
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ id: 'svc1', name: 'S1' }]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/health/services');
      expect(res.body.health.svc1.status).toBe('error');
      expect(res.body.health.svc1.reason).toMatch(/resolver exploded/);
      resolveServiceUrl.mockReturnValue('https://fallback.test');
    });
  });

  // ---- Coverage for /health-checks/incidents and history with pagination ----
  describe('GET /api/health-checks/incidents — non-empty', () => {
    it('returns incidents list', async () => {
      const healthChecker = {
        getCurrentStatus: jest.fn().mockReturnValue({}),
        getServiceStats: jest.fn(),
        configureService: jest.fn(),
        removeService: jest.fn(),
        getOpenIncidents: jest.fn().mockReturnValue([{ id: 'inc1', serviceId: 'svc1' }]),
        getIncidentHistory: jest.fn().mockReturnValue([]),
      };
      const { app } = createApp({ healthChecker });
      const res = await request(app).get('/api/health-checks/incidents');
      expect(res.status).toBe(200);
      expect(res.body.incidents).toHaveLength(1);
    });
  });
});
