/**
 * Unit tests for src/services/ComplianceService.ts
 */

import { Pool, QueryResult } from 'pg';
import type { RedisClientType } from 'redis';
import { ComplianceService } from '../../../src/services/ComplianceService';

// ── Mock AuditVerificationService (instantiated internally by ComplianceService) ──

jest.mock('../../../src/services/AuditVerificationService', () => {
  return {
    AuditVerificationService: jest.fn().mockImplementation(() => ({
      verifyChain: jest.fn().mockResolvedValue({
        verified: true,
        checkedCount: 42,
        brokenAtEventId: null,
      }),
    })),
  };
});

// ── Re-import after mock is established ──────────────────────────────────────
import { AuditVerificationService } from '../../../src/services/AuditVerificationService';

const MockAuditVerificationService = AuditVerificationService as jest.MockedClass<
  typeof AuditVerificationService
>;

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

function makePool(queryFn: jest.Mock): Pool {
  return { query: queryFn } as unknown as Pool;
}

function makeRedis(overrides: Partial<{
  getFn: jest.Mock;
  setFn: jest.Mock;
}>): RedisClientType {
  return {
    get: overrides.getFn ?? jest.fn().mockResolvedValue(null),
    set: overrides.setFn ?? jest.fn().mockResolvedValue('OK'),
  } as unknown as RedisClientType;
}

// ════════════════════════════════════════════════════════════════════════════
// ComplianceService
// ════════════════════════════════════════════════════════════════════════════

describe('ComplianceService', () => {
  beforeEach(() => {
    jest.clearAllMocks();

    // Reset AuditVerificationService mock to default passing behaviour
    MockAuditVerificationService.mockImplementation(
      () =>
        ({
          verifyChain: jest.fn().mockResolvedValue({
            verified: true,
            checkedCount: 42,
            brokenAtEventId: null,
          }),
        }) as unknown as InstanceType<typeof AuditVerificationService>,
    );
  });

  // ────────────────────────────────────────────────────────────────
  // generateReport() — cache miss
  // ────────────────────────────────────────────────────────────────

  describe('generateReport() — cache miss', () => {
    it('should build a report, store it in Redis, and return IComplianceReport structure', async () => {
      // Cache miss → null
      const getFn = jest.fn().mockResolvedValue(null);
      const setFn = jest.fn().mockResolvedValue('OK');

      // Pool returns empty agents list → agent-identity section passes trivially
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);

      const service = new ComplianceService(makePool(mockQuery), makeRedis({ getFn, setFn }));
      const report = await service.generateReport('tenant-1');

      // Redis cache miss check
      expect(getFn).toHaveBeenCalledWith('compliance:report:tenant-1');

      // Report stored in Redis after build
      expect(setFn).toHaveBeenCalledWith(
        'compliance:report:tenant-1',
        expect.any(String),
        expect.objectContaining({ EX: 300 }),
      );

      // IComplianceReport structure
      expect(report).toMatchObject({
        tenant_id: 'tenant-1',
        agntcy_schema_version: '1.0',
        sections: expect.any(Array),
        overall_status: expect.stringMatching(/^(pass|fail|warn)$/),
        generated_at: expect.any(String),
      });

      // from_cache should be absent on a freshly built report
      expect(report.from_cache).toBeUndefined();
    });
  });

  // ────────────────────────────────────────────────────────────────
  // generateReport() — cache hit
  // ────────────────────────────────────────────────────────────────

  describe('generateReport() — cache hit', () => {
    it('should return cached data with from_cache: true', async () => {
      const cachedReport = {
        generated_at: '2026-04-04T00:00:00.000Z',
        tenant_id: 'tenant-cache',
        agntcy_schema_version: '1.0',
        sections: [{ name: 'agent-identity', status: 'pass', details: 'All good.' }],
        overall_status: 'pass',
      };

      const getFn = jest.fn().mockResolvedValue(JSON.stringify(cachedReport));
      const setFn = jest.fn();
      const mockQuery = jest.fn();

      const service = new ComplianceService(makePool(mockQuery), makeRedis({ getFn, setFn }));
      const report = await service.generateReport('tenant-cache');

      expect(report.from_cache).toBe(true);
      expect(report.tenant_id).toBe('tenant-cache');
      expect(report.overall_status).toBe('pass');
      // No DB queries should be made on a cache hit
      expect(mockQuery).not.toHaveBeenCalled();
      // Redis set should not be called either
      expect(setFn).not.toHaveBeenCalled();
    });
  });

  // ────────────────────────────────────────────────────────────────
  // generateReport() — overall_status rollup
  // ────────────────────────────────────────────────────────────────

  describe('generateReport() — overall_status', () => {
    it('should return overall_status: pass when all sections pass', async () => {
      const getFn = jest.fn().mockResolvedValue(null);
      const setFn = jest.fn().mockResolvedValue('OK');

      // No agents → agent-identity passes trivially
      // AuditVerificationService mock returns verified: true (default in beforeEach)
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);

      const service = new ComplianceService(makePool(mockQuery), makeRedis({ getFn, setFn }));
      const report = await service.generateReport('tenant-all-pass');

      expect(report.overall_status).toBe('pass');
    });

    it('should return overall_status: fail when any section fails', async () => {
      const getFn = jest.fn().mockResolvedValue(null);
      const setFn = jest.fn().mockResolvedValue('OK');

      // Override AuditVerificationService to simulate broken chain → audit-trail fails
      MockAuditVerificationService.mockImplementation(
        () =>
          ({
            verifyChain: jest.fn().mockResolvedValue({
              verified: false,
              checkedCount: 10,
              brokenAtEventId: 'event-uuid-broken',
            }),
          }) as unknown as InstanceType<typeof AuditVerificationService>,
      );

      // No agents so agent-identity is 'pass'; audit-trail will be 'fail'
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);

      const service = new ComplianceService(makePool(mockQuery), makeRedis({ getFn, setFn }));
      const report = await service.generateReport('tenant-fail');

      expect(report.overall_status).toBe('fail');
      const auditSection = report.sections.find((s) => s.name === 'audit-trail');
      expect(auditSection?.status).toBe('fail');
    });
  });

  // ────────────────────────────────────────────────────────────────
  // exportAgentCards()
  // ────────────────────────────────────────────────────────────────

  describe('exportAgentCards()', () => {
    it('should return an array of IAgentCard objects with correct fields', async () => {
      const createdAt = new Date('2026-01-15T12:00:00Z');
      const agentRows = [
        {
          agent_id: 'agent-uuid-1',
          owner: 'team-alpha',
          capabilities: ['agents:read', 'tokens:issue'],
          created_at: createdAt,
          did: 'did:web:sentryagent.ai:agent-uuid-1',
        },
        {
          agent_id: 'agent-uuid-2',
          owner: 'team-beta',
          capabilities: ['agents:read'],
          created_at: createdAt,
          did: null, // no DID — id should fall back to agent_id
        },
      ];

      const mockQuery = jest.fn().mockResolvedValue({
        rows: agentRows,
      } as unknown as QueryResult);

      const service = new ComplianceService(
        makePool(mockQuery),
        makeRedis({ getFn: jest.fn(), setFn: jest.fn() }),
      );
      const cards = await service.exportAgentCards('tenant-1');

      expect(cards).toHaveLength(2);

      // Card with DID uses DID as id
      expect(cards[0]).toEqual({
        id: 'did:web:sentryagent.ai:agent-uuid-1',
        name: 'team-alpha',
        capabilities: ['agents:read', 'tokens:issue'],
        endpoint: 'https://api.sentryagent.ai/agents/agent-uuid-1',
        created_at: createdAt.toISOString(),
        agntcy_schema_version: '1.0',
      });

      // Card without DID falls back to agent_id as id
      expect(cards[1]).toEqual({
        id: 'agent-uuid-2',
        name: 'team-beta',
        capabilities: ['agents:read'],
        endpoint: 'https://api.sentryagent.ai/agents/agent-uuid-2',
        created_at: createdAt.toISOString(),
        agntcy_schema_version: '1.0',
      });
    });

    it('should return an empty array when no active agents exist', async () => {
      const mockQuery = jest.fn().mockResolvedValue({
        rows: [],
      } as unknown as QueryResult);

      const service = new ComplianceService(
        makePool(mockQuery),
        makeRedis({ getFn: jest.fn(), setFn: jest.fn() }),
      );
      const cards = await service.exportAgentCards('tenant-empty');

      expect(cards).toEqual([]);
    });

    it('should query only non-decommissioned agents scoped to the tenantId', async () => {
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);

      const service = new ComplianceService(
        makePool(mockQuery),
        makeRedis({ getFn: jest.fn(), setFn: jest.fn() }),
      );
      await service.exportAgentCards('tenant-scope-test');

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status != 'decommissioned'"),
        ['tenant-scope-test'],
      );
    });
  });
});
