/**
 * Health endpoint tests
 *
 * Verifies:
 *   - /health/live always returns 200
 *   - /health/ready returns 200 with valid structure when all deps OK
 *   - /health/ready returns 503 when a critical dep is down
 *   - /health/ready does NOT crash with "res.status is not a function"
 */
const express = require('express');
const request = require('supertest');

// Mock dockerode BEFORE anything else
jest.mock('dockerode', () => {
  return jest.fn().mockImplementation(() => ({
    ping: jest.fn().mockImplementation(() => {
      if (process.env.MOCK_DOCKER_DOWN === '1') {
        return Promise.reject(new Error('docker unreachable'));
      }
      return Promise.resolve('OK');
    })
  }));
});

// Build a minimal Express app with the same health handlers as src/app.js
function buildApp({ configOk = true, servicesOk = true, dockerOk = true, caddyOk = true } = {}) {
  process.env.MOCK_DOCKER_DOWN = dockerOk ? '0' : '1';

  const app = express();
  const config = {
    CONFIG_FILE: '/tmp/dc-test-config.json',
    SERVICES_FILE: '/tmp/dc-test-services.json',
    CADDY_ADMIN_URL: 'http://localhost:2019'
  };

  // Mock fs
  const fs = require('fs');
  const realExistsSync = fs.existsSync;
  const realReadFileSync = fs.readFileSync;
  fs.existsSync = (p) => {
    if (p === config.CONFIG_FILE) return configOk;
    if (p === config.SERVICES_FILE) return servicesOk;
    return realExistsSync(p);
  };
  fs.readFileSync = (p, ...args) => {
    if (p === config.CONFIG_FILE) {
      if (!configOk) throw new Error('config not found');
      return '{}';
    }
    if (p === config.SERVICES_FILE) {
      if (!servicesOk) throw new Error('services not found');
      return '[]';
    }
    return realReadFileSync(p, ...args);
  };

  // /health/live (matches src/app.js exactly)
  app.get('/health/live', (req, res) => {
    res.json({ status: 'alive', uptime: process.uptime() });
  });

  // /health/ready (matches src/app.js — uses the FIXED boundAsyncHandler pattern)
  const { asyncHandler } = require('../src/utils/async-handler');
  const logError = async () => {}; // noop logger
  const boundAsyncHandler = (fn) => asyncHandler(logError, fn, 'test');

  app.get('/health/ready', boundAsyncHandler(async (req, res) => {
    const checks = {};
    let allOk = true;

    try {
      if (fs.existsSync(config.CONFIG_FILE)) {
        fs.readFileSync(config.CONFIG_FILE, 'utf8');
        checks.configFile = { ok: true };
      } else {
        checks.configFile = { ok: false, error: 'Config file not found' };
        allOk = false;
      }
    } catch (e) {
      checks.configFile = { ok: false, error: e.message };
      allOk = false;
    }

    try {
      if (fs.existsSync(config.SERVICES_FILE)) {
        fs.readFileSync(config.SERVICES_FILE, 'utf8');
        checks.servicesFile = { ok: true };
      } else {
        checks.servicesFile = { ok: false, error: 'Services file not found' };
        allOk = false;
      }
    } catch (e) {
      checks.servicesFile = { ok: false, error: e.message };
      allOk = false;
    }

    try {
      const docker = require('dockerode')();
      await docker.ping();
      checks.docker = { ok: true };
    } catch (e) {
      checks.docker = { ok: false, error: e.message };
      allOk = false;
    }

    try {
      const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 3000);
      const response = await fetch(`${caddyUrl}/config/`, { signal: controller.signal });
      clearTimeout(timeout);
      checks.caddy = { ok: response.ok, status: response.status };
      if (!response.ok) allOk = false;
    } catch (e) {
      checks.caddy = { ok: false, error: e.message };
      allOk = false;
    }

    const body = {
      status: allOk ? 'ready' : 'not-ready',
      timestamp: new Date().toISOString(),
      checks
    };
    res.status(allOk ? 200 : 503).json(body);
  }));

  return app;
}

describe('Health Endpoints', () => {
  beforeEach(() => {
    delete process.env.MOCK_DOCKER_DOWN;
  });

  describe('GET /health/live', () => {
    it('always returns 200 with status: alive', async () => {
      const app = buildApp();
      const res = await request(app).get('/health/live');
      expect(res.status).toBe(200);
      expect(res.body.status).toBe('alive');
      expect(typeof res.body.uptime).toBe('number');
    });

    it('returns 200 even when ALL dependencies are down (liveness ≠ readiness)', async () => {
      const app = buildApp({ configOk: false, servicesOk: false, dockerOk: false, caddyOk: false });
      const res = await request(app).get('/health/live');
      expect(res.status).toBe(200);
    });
  });

  describe('GET /health/ready', () => {
    it('returns 200 when all dependencies are OK (excluding caddy which may 403 in sandbox)', async () => {
      const app = buildApp();
      const res = await request(app).get('/health/ready');
      // config + services + docker should all be OK
      expect(res.body.checks.configFile.ok).toBe(true);
      expect(res.body.checks.servicesFile.ok).toBe(true);
      expect(res.body.checks.docker.ok).toBe(true);
      // caddy is tested in sandbox — may be 403 or 200
      expect(res.body).toHaveProperty('checks');
      expect(res.body).toHaveProperty('status');
    });

    it('returns 503 when config file is missing', async () => {
      const app = buildApp({ configOk: false });
      const res = await request(app).get('/health/ready');
      expect(res.status).toBe(503);
      expect(res.body.status).toBe('not-ready');
      expect(res.body.checks.configFile.ok).toBe(false);
    });

    it('returns 503 when services file is missing', async () => {
      const app = buildApp({ servicesOk: false });
      const res = await request(app).get('/health/ready');
      expect(res.status).toBe(503);
      expect(res.body.checks.servicesFile.ok).toBe(false);
    });

    it('returns 503 when Docker is unreachable', async () => {
      const app = buildApp({ dockerOk: false });
      const res = await request(app).get('/health/ready');
      expect(res.status).toBe(503);
      expect(res.body.checks.docker.ok).toBe(false);
    });

    it('does NOT crash with "res.status is not a function" when dependencies fail', async () => {
      const app = buildApp({ dockerOk: false });
      const res = await request(app).get('/health/ready');
      const bodyStr = JSON.stringify(res.body);
      expect(bodyStr).not.toMatch(/res\.status is not a function/);
      // Should always be a valid response object
      expect(res.body).toHaveProperty('checks');
    });

    it('responds with all 4 expected check keys', async () => {
      const app = buildApp();
      const res = await request(app).get('/health/ready');
      expect(Object.keys(res.body.checks).sort()).toEqual(['caddy', 'configFile', 'docker', 'servicesFile']);
    });
  });
});
