import crypto from 'node:crypto';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { isEncrypted } from '../field-encryption';
import { FieldEncryptionService, PII_FIELD_MAP } from '../field-encryption.service';

// Mock LoggerService
const mockLogger = {
  debug: vi.fn(),
  log: vi.fn(),
  error: vi.fn(),
  warn: vi.fn(),
  verbose: vi.fn(),
  child: vi.fn(),
} as any;

const TEST_KEY = crypto.randomBytes(32).toString('hex');

describe('FieldEncryptionService', () => {
  describe('when encryption key is configured', () => {
    let service: FieldEncryptionService;

    beforeEach(() => {
      process.env['FIELD_ENCRYPTION_KEY'] = TEST_KEY;
      process.env['FIELD_ENCRYPTION_KEY_VERSION'] = '1';
      service = new FieldEncryptionService(mockLogger);
    });

    afterEach(() => {
      delete process.env['FIELD_ENCRYPTION_KEY'];
      delete process.env['FIELD_ENCRYPTION_KEY_VERSION'];
      delete process.env['KYC_ENCRYPTION_KEY'];
      delete process.env['KYC_ENCRYPTION_KEY_VERSION'];
    });

    it('should be enabled', () => {
      expect(service.isEnabled()).toBe(true);
    });

    describe('encrypt/decrypt round-trip', () => {
      it('encrypts and decrypts a string', () => {
        const original = 'test@example.com';
        const encrypted = service.encrypt(original) as string;
        expect(isEncrypted(encrypted)).toBe(true);

        const decrypted = service.decrypt(encrypted);
        expect(decrypted).toBe(original);
      });

      it('encrypts and decrypts an object', () => {
        const original = { name: 'Nguyen Van A', idNumber: '012345678901' };
        const encrypted = service.encrypt(original) as string;
        expect(isEncrypted(encrypted)).toBe(true);

        const decrypted = service.decrypt(encrypted);
        expect(decrypted).toEqual(original);
      });

      it('returns null/undefined unchanged', () => {
        expect(service.encrypt(null)).toBeNull();
        expect(service.encrypt(undefined)).toBeUndefined();
        expect(service.decrypt(null)).toBeNull();
        expect(service.decrypt(undefined)).toBeUndefined();
      });

      it('produces different ciphertext for same input (random IV)', () => {
        const value = 'same value';
        const enc1 = service.encrypt(value);
        const enc2 = service.encrypt(value);
        expect(enc1).not.toBe(enc2);
        expect(service.decrypt(enc1)).toBe(value);
        expect(service.decrypt(enc2)).toBe(value);
      });
    });

    describe('plaintext passthrough', () => {
      it('returns non-encrypted strings unchanged on decrypt', () => {
        expect(service.decrypt('plain text')).toBe('plain text');
      });

      it('returns non-string values unchanged on decrypt', () => {
        const obj = { name: 'test' };
        expect(service.decrypt(obj)).toBe(obj);
        expect(service.decrypt(42)).toBe(42);
      });
    });

    describe('isAlreadyEncrypted', () => {
      it('detects encrypted values', () => {
        const encrypted = service.encrypt('test') as string;
        expect(service.isAlreadyEncrypted(encrypted)).toBe(true);
      });

      it('rejects plaintext values', () => {
        expect(service.isAlreadyEncrypted('plain')).toBe(false);
        expect(service.isAlreadyEncrypted(null)).toBe(false);
        expect(service.isAlreadyEncrypted(42)).toBe(false);
      });
    });

    describe('computeHash', () => {
      it('produces a deterministic hex hash', () => {
        const hash1 = service.computeHash('test@example.com');
        const hash2 = service.computeHash('test@example.com');
        expect(hash1).toBe(hash2);
        expect(hash1).toMatch(/^[0-9a-f]{64}$/);
      });

      it('normalizes input (case-insensitive, trimmed)', () => {
        const hash1 = service.computeHash('Test@Example.COM');
        const hash2 = service.computeHash('  test@example.com  ');
        expect(hash1).toBe(hash2);
      });

      it('returns null for null/undefined', () => {
        expect(service.computeHash(null)).toBeNull();
        expect(service.computeHash(undefined)).toBeNull();
      });

      it('produces different hashes for different values', () => {
        const hash1 = service.computeHash('user1@example.com');
        const hash2 = service.computeHash('user2@example.com');
        expect(hash1).not.toBe(hash2);
      });
    });

    describe('key rotation support', () => {
      it('decrypts with version from encrypted string', () => {
        const encrypted = service.encrypt('secret data') as string;
        expect(encrypted).toMatch(/^enc:v1:/);
        expect(service.decrypt(encrypted)).toBe('secret data');
      });

      it('uses versioned key config', () => {
        process.env['FIELD_ENCRYPTION_KEY_VERSION'] = '3';
        const v3Service = new FieldEncryptionService(mockLogger);
        const encrypted = v3Service.encrypt('test') as string;
        expect(encrypted).toMatch(/^enc:v3:/);
        expect(v3Service.decrypt(encrypted)).toBe('test');
      });
    });

    describe('logAccess', () => {
      it('logs encrypt operations', () => {
        service.logAccess('encrypt', 'User', ['email', 'phone'], 'user-123');
        expect(mockLogger.debug).toHaveBeenCalledWith(
          expect.stringContaining('encrypt User.{email,phone}'),
          'FieldEncryptionService',
        );
      });

      it('logs decrypt operations', () => {
        service.logAccess('decrypt', 'Payment', ['callbackData']);
        expect(mockLogger.debug).toHaveBeenCalledWith(
          expect.stringContaining('decrypt Payment.{callbackData}'),
          'FieldEncryptionService',
        );
      });
    });

    describe('getFieldMap', () => {
      it('returns PII_FIELD_MAP', () => {
        expect(service.getFieldMap()).toBe(PII_FIELD_MAP);
      });
    });

    describe('getModelConfig', () => {
      it('finds config for known models', () => {
        const userConfig = service.getModelConfig('User');
        expect(userConfig).toBeDefined();
        expect(userConfig!.fields).toHaveLength(3);
      });

      it('returns undefined for unknown models', () => {
        expect(service.getModelConfig('Unknown')).toBeUndefined();
      });
    });
  });

  describe('when encryption key is NOT configured', () => {
    let service: FieldEncryptionService;

    beforeEach(() => {
      delete process.env['FIELD_ENCRYPTION_KEY'];
      delete process.env['KYC_ENCRYPTION_KEY'];
      service = new FieldEncryptionService(mockLogger);
    });

    it('should not be enabled', () => {
      expect(service.isEnabled()).toBe(false);
    });

    it('encrypt returns value unchanged', () => {
      expect(service.encrypt('test')).toBe('test');
      expect(service.encrypt({ data: 1 })).toEqual({ data: 1 });
    });

    it('decrypt returns value unchanged', () => {
      expect(service.decrypt('test')).toBe('test');
    });

    it('computeHash returns null', () => {
      expect(service.computeHash('test@example.com')).toBeNull();
    });
  });

  describe('KYC_ENCRYPTION_KEY fallback', () => {
    it('uses KYC_ENCRYPTION_KEY when FIELD_ENCRYPTION_KEY is not set', () => {
      delete process.env['FIELD_ENCRYPTION_KEY'];
      process.env['KYC_ENCRYPTION_KEY'] = TEST_KEY;
      const service = new FieldEncryptionService(mockLogger);
      expect(service.isEnabled()).toBe(true);

      const encrypted = service.encrypt('fallback test') as string;
      expect(isEncrypted(encrypted)).toBe(true);
      expect(service.decrypt(encrypted)).toBe('fallback test');

      delete process.env['KYC_ENCRYPTION_KEY'];
    });
  });

  describe('PII_FIELD_MAP correctness', () => {
    it('covers all required models', () => {
      const models = PII_FIELD_MAP.map((c) => c.model);
      expect(models).toContain('User');
      expect(models).toContain('Agent');
      expect(models).toContain('Payment');
      expect(models).toContain('Lead');
      expect(models).toContain('Inquiry');
    });

    it('User model has correct fields', () => {
      const userConfig = PII_FIELD_MAP.find((c) => c.model === 'User')!;
      const fieldNames = userConfig.fields.map((f) => f.field);
      expect(fieldNames).toContain('email');
      expect(fieldNames).toContain('phone');
      expect(fieldNames).toContain('kycData');
    });

    it('searchable fields are marked correctly', () => {
      const userConfig = PII_FIELD_MAP.find((c) => c.model === 'User')!;
      const emailField = userConfig.fields.find((f) => f.field === 'email');
      const phoneField = userConfig.fields.find((f) => f.field === 'phone');
      const kycField = userConfig.fields.find((f) => f.field === 'kycData');

      expect(emailField?.searchable).toBe(true);
      expect(phoneField?.searchable).toBe(true);
      expect(kycField?.searchable).toBeFalsy();
    });
  });
});
