/**
 * Unit tests for EncryptionService — AES-256-CBC column-level encryption.
 *
 * Tests:
 *   1. Encrypt/decrypt round-trip returns original plaintext
 *   2. isEncrypted: true for base64:base64 format, false for plaintext strings
 *   3. encryptColumn produces different ciphertext on each call (IV randomness)
 *   4. Singleton reset utility works for test isolation
 */

import {
  EncryptionService,
  getEncryptionService,
  _resetEncryptionServiceSingleton,
} from '../../../src/services/EncryptionService';
import { VaultClient } from '../../../src/vault/VaultClient';

// ============================================================================
// Mock VaultClient
// ============================================================================

/** A 32-byte (64-char hex) test encryption key. */
const TEST_KEY = 'a'.repeat(64); // 64 x 'a' = valid 32-byte hex key

/**
 * Creates a mock VaultClient that returns TEST_KEY from readArbitrarySecret.
 */
function makeMockVaultClient(): VaultClient {
  const mock = {
    readArbitrarySecret: jest.fn().mockResolvedValue({ encryptionKey: TEST_KEY }),
    writeArbitrarySecret: jest.fn().mockResolvedValue(undefined),
    writeSecret: jest.fn(),
    readSecret: jest.fn(),
    verifySecret: jest.fn(),
    deleteSecret: jest.fn(),
  };
  return mock as unknown as VaultClient;
}

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

describe('EncryptionService', () => {
  let service: EncryptionService;
  let mockVaultClient: VaultClient;

  beforeEach(() => {
    _resetEncryptionServiceSingleton();
    mockVaultClient = makeMockVaultClient();
    service = new EncryptionService(mockVaultClient);
  });

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

  // ── Round-trip ────────────────────────────────────────────────────────────

  it('should encrypt and then decrypt back to the original plaintext', async () => {
    const plaintext = 'super-secret-credential-hash-value';

    const encrypted = await service.encryptColumn(plaintext);
    expect(encrypted).not.toBe(plaintext);
    expect(encrypted).toContain(':');

    const decrypted = await service.decryptColumn(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  it('should handle empty string round-trip', async () => {
    const plaintext = '';
    const encrypted = await service.encryptColumn(plaintext);
    const decrypted = await service.decryptColumn(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  it('should handle unicode strings in round-trip', async () => {
    const plaintext = 'secret/data/agentidp/agents/über-agent/credentials/cred-123';
    const encrypted = await service.encryptColumn(plaintext);
    const decrypted = await service.decryptColumn(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  // ── IV randomness ─────────────────────────────────────────────────────────

  it('should produce different ciphertext on each call (random IV)', async () => {
    const plaintext = 'same-plaintext-value';

    const encrypted1 = await service.encryptColumn(plaintext);
    const encrypted2 = await service.encryptColumn(plaintext);

    // Same plaintext but different IV → different ciphertext
    expect(encrypted1).not.toBe(encrypted2);

    // Both must still decrypt to the same plaintext
    expect(await service.decryptColumn(encrypted1)).toBe(plaintext);
    expect(await service.decryptColumn(encrypted2)).toBe(plaintext);
  });

  // ── isEncrypted ──────────────────────────────────────────────────────────

  it('should return true for a value in base64:base64 format', async () => {
    const encrypted = await service.encryptColumn('test-value');
    expect(service.isEncrypted(encrypted)).toBe(true);
  });

  it('should return false for a plaintext bcrypt hash', () => {
    const bcryptHash = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
    expect(service.isEncrypted(bcryptHash)).toBe(false);
  });

  it('should return false for a Vault path string', () => {
    expect(service.isEncrypted('secret/data/agentidp/agents/abc/credentials/xyz')).toBe(false);
  });

  it('should return false for an empty string', () => {
    expect(service.isEncrypted('')).toBe(false);
  });

  it('should return false for a plain UUID', () => {
    expect(service.isEncrypted('550e8400-e29b-41d4-a716-446655440000')).toBe(false);
  });

  it('should return true for a manually constructed base64:base64 string', () => {
    const iv = Buffer.from('deadbeef12345678', 'hex').toString('base64');
    const ct = Buffer.from('cafebabe00112233', 'hex').toString('base64');
    expect(service.isEncrypted(`${iv}:${ct}`)).toBe(true);
  });

  // ── Vault key fetching ────────────────────────────────────────────────────

  it('should call Vault readArbitrarySecret once and cache the key', async () => {
    const plaintext = 'value1';
    await service.encryptColumn(plaintext);
    await service.encryptColumn(plaintext);
    await service.encryptColumn(plaintext);

    // Key should be fetched only once
    expect(
      (mockVaultClient.readArbitrarySecret as jest.Mock).mock.calls.length,
    ).toBe(1);
  });

  it('should use the ENCRYPTION_KEY_VAULT_PATH env var for the Vault path', async () => {
    const originalPath = process.env['ENCRYPTION_KEY_VAULT_PATH'];
    process.env['ENCRYPTION_KEY_VAULT_PATH'] = 'secret/data/custom/path';

    const freshService = new EncryptionService(mockVaultClient);
    await freshService.encryptColumn('test');

    expect(
      (mockVaultClient.readArbitrarySecret as jest.Mock).mock.calls[0][0],
    ).toBe('secret/data/custom/path');

    // Restore env
    if (originalPath === undefined) {
      delete process.env['ENCRYPTION_KEY_VAULT_PATH'];
    } else {
      process.env['ENCRYPTION_KEY_VAULT_PATH'] = originalPath;
    }
  });

  // ── Error handling ────────────────────────────────────────────────────────

  it('should throw when ciphertext has no colon separator', async () => {
    await expect(service.decryptColumn('invalidformat')).rejects.toThrow(
      'Invalid encrypted column format',
    );
  });

  it('should throw when Vault returns an invalid key', async () => {
    const badVault = {
      readArbitrarySecret: jest.fn().mockResolvedValue({ encryptionKey: 'tooshort' }),
    } as unknown as VaultClient;

    const badService = new EncryptionService(badVault);
    await expect(badService.encryptColumn('test')).rejects.toThrow(
      'expected a 64-character hex string',
    );
  });

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

  it('getEncryptionService should return the same instance on repeated calls', () => {
    const instance1 = getEncryptionService(mockVaultClient);
    const instance2 = getEncryptionService(mockVaultClient);
    expect(instance1).toBe(instance2);
  });
});
