// src/routes/health.routes.test.ts
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
import supertest from 'supertest';
import healthRouter from './health.routes';
import * as dbConnection from '../services/db/connection.db';
import { connection as redisConnection } from '../services/queueService.server';
import fs from 'node:fs/promises';
import { createTestApp } from '../tests/utils/createTestApp';

// 1. Mock the dependencies of the health router.
vi.mock('../services/db/connection.db', () => ({
  checkTablesExist: vi.fn(),
  getPoolStatus: vi.fn(),
}));

vi.mock('node:fs/promises', () => ({
  default: {
    access: vi.fn(),
    constants: { W_OK: 1 },
  },
}));

// In this case, it's the redisConnection from the queueService.
vi.mock('../services/queueService.server', () => ({
  // We need to mock the `connection` export which is an object with a `ping` method.
  connection: {
    ping: vi.fn(),
  },
}));

// Mock the logger to keep test output clean.
vi.mock('../services/logger.server', () => ({
  logger: {
    info: vi.fn(),
    debug: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
  },
}));

// Cast the mocked import to a Mocked type for type-safe access to mock functions.
const mockedRedisConnection = redisConnection as Mocked<typeof redisConnection>;
const mockedDbConnection = dbConnection as Mocked<typeof dbConnection>;
const mockedFs = fs as Mocked<typeof fs>;

// 2. Create a minimal Express app to host the router for testing.
const app = createTestApp({ router: healthRouter, basePath: '/api/health' });

describe('Health Routes (/api/health)', () => {
  beforeEach(() => {
    // Clear mock history before each test to ensure isolation.
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.useRealTimers(); // Restore real timers after each test
  });

  describe('GET /ping', () => {
    it('should return 200 OK with "pong"', async () => {
      // Act
      const response = await supertest(app).get('/api/health/ping');

      // Assert
      expect(response.status).toBe(200);
      expect(response.text).toBe('pong');
    });
  });

  describe('GET /redis', () => {
    it('should return 200 OK if Redis ping is successful', async () => {
      // Arrange: Simulate a successful ping by having the mock resolve to 'PONG'.
      mockedRedisConnection.ping.mockResolvedValue('PONG');

      // Act: Make a request to the endpoint.
      const response = await supertest(app).get('/api/health/redis');

      // Assert: Check for the correct status and response body.
      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        success: true,
        message: 'Redis connection is healthy.',
      });
    });

    it('should return 500 if Redis ping fails', async () => {
      // Arrange: Simulate a failure by having the mock reject with an error.
      const redisError = new Error('Connection timed out');
      mockedRedisConnection.ping.mockRejectedValue(redisError);

      // Act
      const response = await supertest(app).get('/api/health/redis');

      // Assert
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('Connection timed out');
    });

    it('should return 500 if Redis ping returns an unexpected response', async () => {
      // Arrange: Simulate a successful ping but with an unexpected reply.
      mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'

      // Act
      const response = await supertest(app).get('/api/health/redis');

      // Assert
      expect(response.status).toBe(500);
      expect(response.body.message).toContain('Unexpected Redis ping response: OK');
    });
  });

  describe('GET /time', () => {
    it('should return the current server time, year, and week number', async () => {
      // Arrange: Set a specific date to test against using fake timers.
      const fakeDate = new Date('2024-03-15T10:30:00.000Z'); // This is a Friday, in the 11th week of 2024
      vi.useFakeTimers();
      vi.setSystemTime(fakeDate);

      // Act
      const response = await supertest(app).get('/api/health/time');

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.currentTime).toBe('2024-03-15T10:30:00.000Z');
      expect(response.body.year).toBe(2024);
      expect(response.body.week).toBe(11);
    });
  });

  describe('GET /db-schema', () => {
    it('should return 200 OK if all tables exist', async () => {
      // Arrange: Mock the service function to return an empty array (no missing tables)
      mockedDbConnection.checkTablesExist.mockResolvedValue([]);

      // Act
      const response = await supertest(app).get('/api/health/db-schema');

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.message).toBe('All required database tables exist.');
    });

    it('should return 500 if tables are missing', async () => {
      // Arrange: Mock the service function to return missing table names
      mockedDbConnection.checkTablesExist.mockResolvedValue(['missing_table_1', 'missing_table_2']);

      const response = await supertest(app).get('/api/health/db-schema');

      expect(response.status).toBe(500);
      expect(response.body.message).toContain('Missing tables: missing_table_1, missing_table_2');
    });

    it('should return 500 if the database check fails', async () => {
      // Arrange: Mock the service function to reject with a generic error
      const dbError = new Error('DB connection failed');
      mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);

      const response = await supertest(app).get('/api/health/db-schema');

      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB connection failed');
    });
  });

  describe('GET /storage', () => {
    it('should return 200 OK if storage is accessible and writable', async () => {
      // Arrange: Mock fs.access to resolve, indicating success.
      mockedFs.access.mockResolvedValue(undefined);

      // Act
      const response = await supertest(app).get('/api/health/storage');

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.message).toContain('is accessible and writable');
    });

    it('should return 500 if storage is not accessible or writable', async () => {
      // Arrange: Mock fs.access to reject, indicating failure.
      const accessError = new Error('EACCES: permission denied');
      mockedFs.access.mockRejectedValue(accessError);

      // Act
      const response = await supertest(app).get('/api/health/storage');

      // Assert
      expect(response.status).toBe(500);
      expect(response.body.message).toContain('Storage check failed.');
    });
  });

  describe('GET /db-pool', () => {
    it('should return 200 OK for a healthy pool status', async () => {
      // Arrange: Mock a healthy pool status (waitingCount < 5)
      mockedDbConnection.getPoolStatus.mockReturnValue({
        totalCount: 10,
        idleCount: 8,
        waitingCount: 1,
      });

      // Act
      const response = await supertest(app).get('/api/health/db-pool');

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.message).toContain('Pool Status: 10 total, 8 idle, 1 waiting.');
    });

    it('should return 500 for an unhealthy pool status', async () => {
      // Arrange: Mock an unhealthy pool status (waitingCount >= 5)
      mockedDbConnection.getPoolStatus.mockReturnValue({
        totalCount: 20,
        idleCount: 5,
        waitingCount: 15,
      });

      // Act
      const response = await supertest(app).get('/api/health/db-pool');

      // Assert
      expect(response.status).toBe(500);
      expect(response.body.success).toBe(false);
      expect(response.body.message).toContain('Pool may be under stress.');
      expect(response.body.message).toContain('Pool Status: 20 total, 5 idle, 15 waiting.');
    });
  });

  it('should return 500 if getPoolStatus throws an error', async () => {
    // Arrange: Mock getPoolStatus to throw an error
    const poolError = new Error('Pool is not initialized');
    mockedDbConnection.getPoolStatus.mockImplementation(() => { throw poolError; });

    const response = await supertest(app).get('/api/health/db-pool');

    expect(response.status).toBe(500);
    expect(response.body.message).toBe('Pool is not initialized');
  });
});