/**
 * Unit tests for AuditVerificationService — audit chain integrity verification.
 *
 * Tests:
 *   1. Intact chain: correct hashes → { verified: true, checkedCount: N, brokenAtEventId: null }
 *   2. Tampered chain: one wrong hash → { verified: false, brokenAtEventId: <event_id> }
 *   3. Empty log: no rows → { verified: true, checkedCount: 0, brokenAtEventId: null }
 *   4. Date range params are propagated to SQL query
 *   5. previous_hash mismatch is detected
 */

import crypto from 'crypto';
import { Pool } from 'pg';
import {
  AuditVerificationService,
  IChainVerificationResult,
  _resetAuditVerificationServiceSingleton,
  getAuditVerificationService,
} from '../../../src/services/AuditVerificationService';

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

/**
 * Computes the SHA-256 hash of an audit event — must match the algorithm in
 * AuditVerificationService and AuditRepository.
 */
function computeHash(
  eventId: string,
  timestamp: Date,
  action: string,
  outcome: string,
  agentId: string,
  organizationId: string,
  previousHash: string,
): string {
  return crypto
    .createHash('sha256')
    .update(
      eventId +
      timestamp.toISOString() +
      action +
      outcome +
      agentId +
      organizationId +
      previousHash,
    )
    .digest('hex');
}

/** Generates a minimal audit chain row with correct hash linkage. */
function makeRow(
  eventId: string,
  timestamp: Date,
  action: string,
  outcome: string,
  agentId: string,
  organizationId: string,
  previousHash: string,
) {
  const hash = computeHash(eventId, timestamp, action, outcome, agentId, organizationId, previousHash);
  return {
    event_id: eventId,
    timestamp,
    action,
    outcome,
    agent_id: agentId,
    organization_id: organizationId,
    hash,
    previous_hash: previousHash,
  };
}

/** Creates a mock pg.Pool whose query() returns the given rows. */
function mockPool(rows: unknown[]): Pool {
  return {
    query: jest.fn().mockResolvedValue({ rows }),
  } as unknown as Pool;
}

// ============================================================================
// Test data
// ============================================================================

const ORG = 'org_test';
const AGENT = 'agent-abc-123';
const T1 = new Date('2026-03-01T10:00:00.000Z');
const T2 = new Date('2026-03-01T10:01:00.000Z');
const T3 = new Date('2026-03-01T10:02:00.000Z');

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

describe('AuditVerificationService', () => {
  afterEach(() => {
    _resetAuditVerificationServiceSingleton();
  });

  // ── Intact chain ──────────────────────────────────────────────────────────

  it('should return verified: true for an intact 3-event chain', async () => {
    const row1 = makeRow('evt-001', T1, 'agent.created', 'success', AGENT, ORG, '');
    const row2 = makeRow('evt-002', T2, 'credential.generated', 'success', AGENT, ORG, row1.hash);
    const row3 = makeRow('evt-003', T3, 'token.issued', 'success', AGENT, ORG, row2.hash);

    const pool = mockPool([row1, row2, row3]);
    const service = new AuditVerificationService(pool);

    const result: IChainVerificationResult = await service.verifyChain();

    expect(result.verified).toBe(true);
    expect(result.checkedCount).toBe(3);
    expect(result.brokenAtEventId).toBeNull();
  });

  it('should return verified: true for a single-event chain', async () => {
    const row1 = makeRow('evt-001', T1, 'agent.created', 'success', AGENT, ORG, '');
    const pool = mockPool([row1]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(true);
    expect(result.checkedCount).toBe(1);
    expect(result.brokenAtEventId).toBeNull();
  });

  // ── Empty log ─────────────────────────────────────────────────────────────

  it('should return verified: true with checkedCount 0 for an empty log', async () => {
    const pool = mockPool([]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(true);
    expect(result.checkedCount).toBe(0);
    expect(result.brokenAtEventId).toBeNull();
  });

  // ── Tampered hash ─────────────────────────────────────────────────────────

  it('should detect a tampered hash on the second event', async () => {
    const row1 = makeRow('evt-001', T1, 'agent.created', 'success', AGENT, ORG, '');
    const row2 = makeRow('evt-002', T2, 'credential.generated', 'success', AGENT, ORG, row1.hash);

    // Tamper: replace hash on row2 with garbage
    const tamperedRow2 = { ...row2, hash: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' };

    const pool = mockPool([row1, tamperedRow2]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(false);
    expect(result.brokenAtEventId).toBe('evt-002');
    expect(result.checkedCount).toBe(1); // row1 was checked before break detected
  });

  it('should detect a previous_hash mismatch', async () => {
    const row1 = makeRow('evt-001', T1, 'agent.created', 'success', AGENT, ORG, '');

    // row2 references wrong previous_hash
    const row2 = makeRow('evt-002', T2, 'credential.generated', 'success', AGENT, ORG, 'wrongprevhash');

    const pool = mockPool([row1, row2]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(false);
    expect(result.brokenAtEventId).toBe('evt-002');
  });

  it('should stop at the first break and not report subsequent events', async () => {
    const row1 = makeRow('evt-001', T1, 'agent.created', 'success', AGENT, ORG, '');
    const row2 = makeRow('evt-002', T2, 'credential.generated', 'success', AGENT, ORG, row1.hash);
    const row3 = makeRow('evt-003', T3, 'token.issued', 'success', AGENT, ORG, row2.hash);

    // Tamper row2 hash
    const tamperedRow2 = { ...row2, hash: 'aaaa' + row2.hash.slice(4) };

    const pool = mockPool([row1, tamperedRow2, row3]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(false);
    expect(result.brokenAtEventId).toBe('evt-002');
    // row3 was never checked
  });

  // ── Pre-migration rows (empty hashes) ─────────────────────────────────────

  it('should skip pre-migration rows with empty hashes', async () => {
    // Simulate rows written before migration 020 (hash = '', previous_hash = '')
    const legacyRow = {
      event_id: 'evt-legacy',
      timestamp: T1,
      action: 'agent.created',
      outcome: 'success',
      agent_id: AGENT,
      organization_id: ORG,
      hash: '',
      previous_hash: '',
    };

    const pool = mockPool([legacyRow]);
    const service = new AuditVerificationService(pool);

    const result = await service.verifyChain();

    expect(result.verified).toBe(true);
    expect(result.checkedCount).toBe(1);
    expect(result.brokenAtEventId).toBeNull();
  });

  // ── Date range params ─────────────────────────────────────────────────────

  it('should propagate fromDate and toDate to the SQL query', async () => {
    const pool = mockPool([]);
    const service = new AuditVerificationService(pool);

    const fromDate = '2026-03-01T00:00:00.000Z';
    const toDate = '2026-03-31T23:59:59.999Z';

    const result = await service.verifyChain(fromDate, toDate);

    // Verify the query was called with date params
    const queryMock = pool.query as jest.Mock;
    expect(queryMock).toHaveBeenCalledTimes(1);

    const callArgs = queryMock.mock.calls[0] as [string, unknown[]];
    expect(callArgs[0]).toContain('timestamp >=');
    expect(callArgs[0]).toContain('timestamp <=');
    expect(callArgs[1]).toEqual([new Date(fromDate), new Date(toDate)]);

    // fromDate/toDate are echoed back in result
    expect(result.fromDate).toBe(fromDate);
    expect(result.toDate).toBe(toDate);
  });

  it('should include only fromDate in query when toDate is omitted', async () => {
    const pool = mockPool([]);
    const service = new AuditVerificationService(pool);

    const fromDate = '2026-03-01T00:00:00.000Z';
    const result = await service.verifyChain(fromDate, undefined);

    const queryMock = pool.query as jest.Mock;
    const callArgs = queryMock.mock.calls[0] as [string, unknown[]];
    expect(callArgs[0]).toContain('timestamp >=');
    expect(callArgs[0]).not.toContain('timestamp <=');
    expect(result.fromDate).toBe(fromDate);
    expect(result.toDate).toBeUndefined();
  });

  it('should include no WHERE clause when no date range is provided', async () => {
    const pool = mockPool([]);
    const service = new AuditVerificationService(pool);

    await service.verifyChain();

    const queryMock = pool.query as jest.Mock;
    const callArgs = queryMock.mock.calls[0] as [string, unknown[]];
    expect(callArgs[0]).not.toContain('WHERE');
    expect(callArgs[1]).toEqual([]);
  });

  // ── Singleton ─────────────────────────────────────────────────────────────

  it('getAuditVerificationService should return the same instance on repeated calls', () => {
    const pool = mockPool([]);
    const instance1 = getAuditVerificationService(pool);
    const instance2 = getAuditVerificationService(pool);
    expect(instance1).toBe(instance2);
  });
});
