/**
 * Unit tests for src/routes/health.ts
 *
 * Tests the GET /health endpoint via the createHealthRouter factory.
 * PostgreSQL and Redis dependencies are fully mocked — no live services required.
 */

import express, { Application } from 'express';
import request from 'supertest';
import { Pool, PoolClient } from 'pg';
import { RedisClientType } from 'redis';
import { createHealthRouter } from '../../../src/routes/health';

// ── Mock helpers ──────────────────────────────────────────────────────────────

/** Builds a mock pg PoolClient with controllable query/release. */
function makePoolClient(queryError?: Error): jest.Mocked<Pick<PoolClient, 'query' | 'release'>> {
  return {
    query: queryError
      ? jest.fn().mockRejectedValue(queryError)
      : jest.fn().mockResolvedValue({ rows: [{ '?column?': 1 }], rowCount: 1 }),
    release: jest.fn(),
  } as unknown as jest.Mocked<Pick<PoolClient, 'query' | 'release'>>;
}

/** Builds a mock pg Pool whose connect() resolves or rejects on demand. */
function makePool(connectError?: Error, queryError?: Error): jest.Mocked<Pool> {
  return {
    connect: connectError
      ? jest.fn().mockRejectedValue(connectError)
      : jest.fn().mockResolvedValue(makePoolClient(queryError)),
  } as unknown as jest.Mocked<Pool>;
}

/** Builds a mock Redis client whose ping() resolves or rejects on demand. */
function makeRedis(pingError?: Error): jest.Mocked<RedisClientType> {
  return {
    ping: pingError
      ? jest.fn().mockRejectedValue(pingError)
      : jest.fn().mockResolvedValue('PONG'),
  } as unknown as jest.Mocked<RedisClientType>;
}

/** Creates a minimal Express app with the health router mounted at /health. */
function buildApp(pool: jest.Mocked<Pool>, redis: jest.Mocked<RedisClientType>): Application {
  const app = express();
  app.use('/health', createHealthRouter(pool as unknown as Pool, redis as unknown as RedisClientType));
  return app;
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('GET /health', () => {
  describe('when both PostgreSQL and Redis are healthy', () => {
    it('returns 200 with status ok and both services connected', async () => {
      const app = buildApp(makePool(), makeRedis());

      const response = await request(app).get('/health');

      expect(response.status).toBe(200);
      expect(response.body).toMatchObject({
        status: 'ok',
        services: {
          postgres: 'connected',
          redis: 'connected',
        },
      });
    });

    it('includes version and uptime fields in the response', async () => {
      const app = buildApp(makePool(), makeRedis());

      const response = await request(app).get('/health');

      expect(typeof response.body.version).toBe('string');
      expect(typeof response.body.uptime).toBe('number');
    });
  });

  describe('when PostgreSQL connect() throws', () => {
    it('returns 503 with status degraded and postgres disconnected, redis connected', async () => {
      const pool = makePool(new Error('PG connection refused'));
      const app = buildApp(pool, makeRedis());

      const response = await request(app).get('/health');

      expect(response.status).toBe(503);
      expect(response.body).toMatchObject({
        status: 'degraded',
        services: {
          postgres: 'disconnected',
          redis: 'connected',
        },
      });
    });
  });

  describe('when Redis ping() throws', () => {
    it('returns 503 with status degraded and postgres connected, redis disconnected', async () => {
      const app = buildApp(makePool(), makeRedis(new Error('Redis ECONNREFUSED')));

      const response = await request(app).get('/health');

      expect(response.status).toBe(503);
      expect(response.body).toMatchObject({
        status: 'degraded',
        services: {
          postgres: 'connected',
          redis: 'disconnected',
        },
      });
    });
  });

  describe('when both PostgreSQL and Redis fail', () => {
    it('returns 503 with status degraded and both services disconnected', async () => {
      const pool = makePool(new Error('PG down'));
      const app = buildApp(pool, makeRedis(new Error('Redis down')));

      const response = await request(app).get('/health');

      expect(response.status).toBe(503);
      expect(response.body).toMatchObject({
        status: 'degraded',
        services: {
          postgres: 'disconnected',
          redis: 'disconnected',
        },
      });
    });
  });

  describe('when PostgreSQL query() throws (connect succeeds but query fails)', () => {
    it('returns 503 with postgres disconnected', async () => {
      const pool = makePool(undefined, new Error('PG query error'));
      const app = buildApp(pool, makeRedis());

      const response = await request(app).get('/health');

      expect(response.status).toBe(503);
      expect(response.body).toMatchObject({
        status: 'degraded',
        services: {
          postgres: 'disconnected',
          redis: 'connected',
        },
      });
    });
  });
});
