/**
 * Integration tests for compliance API endpoints.
 *
 * Tests:
 *   1. GET /compliance/controls returns 200 with 5 controls
 *   2. GET /audit/verify with audit:read token returns 200
 *   3. GET /audit/verify without token returns 401
 *   4. GET /audit/verify with invalid fromDate returns 400 VALIDATION_ERROR
 *   5. GET /audit/verify with fromDate > toDate returns 400 VALIDATION_ERROR
 */

import crypto from 'crypto';
import request from 'supertest';
import express, { Application } from 'express';
import { v4 as uuidv4 } from 'uuid';

// ============================================================================
// Environment setup — must be before any app imports
// ============================================================================

const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

process.env['NODE_ENV'] = 'test';
process.env['JWT_PRIVATE_KEY'] = privateKey;
process.env['JWT_PUBLIC_KEY'] = publicKey;

// ============================================================================
// Mock Redis — authMiddleware calls getRedisClient() for revocation check.
// Return a mock client that says no tokens are revoked.
// ============================================================================
jest.mock('../../../src/cache/redis', () => ({
  getRedisClient: jest.fn().mockResolvedValue({
    get: jest.fn().mockResolvedValue(null), // no tokens revoked
    set: jest.fn().mockResolvedValue('OK'),
    incr: jest.fn().mockResolvedValue(1),
    expire: jest.fn().mockResolvedValue(1),
  }),
  closeRedisClient: jest.fn().mockResolvedValue(undefined),
}));

// ============================================================================
// Minimal app that wires only compliance routes (avoids full DB dependency)
// ============================================================================

import { createComplianceRouter } from '../../../src/routes/compliance';
import { ComplianceController } from '../../../src/controllers/ComplianceController';
import { AuditVerificationService } from '../../../src/services/AuditVerificationService';
import { Pool } from 'pg';
import { errorHandler } from '../../../src/middleware/errorHandler';
import { signToken } from '../../../src/utils/jwt';
import { _resetAuditVerificationServiceSingleton } from '../../../src/services/AuditVerificationService';

// ============================================================================
// Helpers
// ============================================================================

/** Creates a JWT token with the given scope. */
function makeToken(scope: string = 'audit:read'): string {
  const agentId = uuidv4();
  return signToken({ sub: agentId, client_id: agentId, scope, jti: uuidv4() }, privateKey);
}

/** Creates a minimal Express app with compliance routes only. */
function createMinimalApp(mockPool: Pool): Application {
  const app = express();
  app.use(express.json());

  const auditVerificationService = new AuditVerificationService(mockPool);
  const complianceController = new ComplianceController(auditVerificationService);

  app.use('/api/v1', createComplianceRouter(complianceController));
  app.use(errorHandler);

  return app;
}

/** Creates a mock Pool that returns empty rows for any query. */
function makeEmptyPool(): Pool {
  return {
    query: jest.fn().mockResolvedValue({ rows: [] }),
  } as unknown as Pool;
}

// ============================================================================
// Tests
// ============================================================================

describe('Compliance Endpoints Integration Tests', () => {
  let app: Application;
  let mockPool: Pool;

  beforeEach(() => {
    _resetAuditVerificationServiceSingleton();
    mockPool = makeEmptyPool();
    app = createMinimalApp(mockPool);
  });

  afterEach(() => {
    _resetAuditVerificationServiceSingleton();
  });

  // ── GET /compliance/controls ──────────────────────────────────────────────

  describe('GET /api/v1/compliance/controls', () => {
    it('should return 200 with exactly 5 controls', async () => {
      const res = await request(app).get('/api/v1/compliance/controls');

      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('controls');
      expect(Array.isArray(res.body.controls)).toBe(true);
      expect(res.body.controls).toHaveLength(5);
    });

    it('should include all required control IDs', async () => {
      const res = await request(app).get('/api/v1/compliance/controls');

      expect(res.status).toBe(200);
      const ids = (res.body.controls as Array<{ id: string }>).map((c) => c.id);
      expect(ids).toContain('CC6.1');
      expect(ids).toContain('CC6.7');
      expect(ids).toContain('CC7.2');
      expect(ids).toContain('CC9.2');
      expect(ids).toContain('CC7.1');
    });

    it('should include required fields on each control', async () => {
      const res = await request(app).get('/api/v1/compliance/controls');

      expect(res.status).toBe(200);
      for (const control of res.body.controls as Array<Record<string, unknown>>) {
        expect(control).toHaveProperty('id');
        expect(control).toHaveProperty('name');
        expect(control).toHaveProperty('status');
        expect(control).toHaveProperty('lastChecked');
        expect(['passing', 'failing', 'unknown']).toContain(control['status']);
      }
    });

    it('should set Cache-Control header', async () => {
      const res = await request(app).get('/api/v1/compliance/controls');

      expect(res.status).toBe(200);
      expect(res.headers['cache-control']).toBe('public, max-age=60');
    });

    it('should not require authentication', async () => {
      // No Authorization header
      const res = await request(app).get('/api/v1/compliance/controls');
      expect(res.status).toBe(200);
    });
  });

  // ── GET /audit/verify ─────────────────────────────────────────────────────

  describe('GET /api/v1/audit/verify', () => {
    it('should return 200 with verification result when authenticated with audit:read scope', async () => {
      const token = makeToken('audit:read');

      const res = await request(app)
        .get('/api/v1/audit/verify')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('verified');
      expect(res.body).toHaveProperty('checkedCount');
      expect(res.body).toHaveProperty('brokenAtEventId');
      expect(typeof res.body.verified).toBe('boolean');
      expect(typeof res.body.checkedCount).toBe('number');
    });

    it('should return 401 when no token is provided', async () => {
      const res = await request(app)
        .get('/api/v1/audit/verify');

      expect(res.status).toBe(401);
    });

    it('should return 403 when token lacks audit:read scope', async () => {
      const token = makeToken('agents:read');

      const res = await request(app)
        .get('/api/v1/audit/verify')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(403);
      expect(res.body.code).toBe('INSUFFICIENT_SCOPE');
    });

    it('should return 400 VALIDATION_ERROR when fromDate is not a valid ISO 8601 date', async () => {
      const token = makeToken('audit:read');

      const res = await request(app)
        .get('/api/v1/audit/verify?fromDate=not-a-date')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(400);
      expect(res.body.code).toBe('VALIDATION_ERROR');
      expect(res.body.details).toHaveProperty('field', 'fromDate');
    });

    it('should return 400 VALIDATION_ERROR when toDate is not a valid ISO 8601 date', async () => {
      const token = makeToken('audit:read');

      const res = await request(app)
        .get('/api/v1/audit/verify?toDate=2026-13-99')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(400);
      expect(res.body.code).toBe('VALIDATION_ERROR');
      expect(res.body.details).toHaveProperty('field', 'toDate');
    });

    it('should return 400 VALIDATION_ERROR when fromDate is after toDate', async () => {
      const token = makeToken('audit:read');

      const res = await request(app)
        .get('/api/v1/audit/verify?fromDate=2026-03-31T00:00:00.000Z&toDate=2026-03-01T00:00:00.000Z')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(400);
      expect(res.body.code).toBe('VALIDATION_ERROR');
    });

    it('should accept valid date range params and return 200', async () => {
      const token = makeToken('audit:read');

      const res = await request(app)
        .get('/api/v1/audit/verify?fromDate=2026-03-01T00:00:00.000Z&toDate=2026-03-31T23:59:59.999Z')
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(200);
      expect(res.body.verified).toBe(true);
      expect(res.body.fromDate).toBe('2026-03-01T00:00:00.000Z');
      expect(res.body.toDate).toBe('2026-03-31T23:59:59.999Z');
    });
  });
});
