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

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

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

describe('encryption-middleware', () => {
  let service: FieldEncryptionService;

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

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

  describe('createEncryptionExtension', () => {
    it('returns a Prisma extension object', () => {
      const ext = createEncryptionExtension(service);
      expect(ext).toBeDefined();
    });

    it('returns a no-op extension when encryption is disabled', () => {
      delete process.env['FIELD_ENCRYPTION_KEY'];
      const disabledService = new FieldEncryptionService(mockLogger);
      const ext = createEncryptionExtension(disabledService);
      expect(ext).toBeDefined();
    });
  });

  describe('FieldEncryptionService integration with extension', () => {
    // Since we can't easily invoke the Prisma $extends handler in isolation
    // (it requires a real PrismaClient), we test the encrypt/decrypt behavior
    // that the extension uses via the service.

    it('encrypt + decrypt round-trip for User.email', () => {
      const original = 'user@example.com';
      const encrypted = service.encrypt(original) as string;
      expect(isEncrypted(encrypted)).toBe(true);
      expect(service.decrypt(encrypted)).toBe(original);
    });

    it('encrypt + decrypt round-trip for User.kycData', () => {
      const original = { idNumber: '012345678901', name: 'Nguyen Van A' };
      const encrypted = service.encrypt(original) as string;
      expect(isEncrypted(encrypted)).toBe(true);
      expect(service.decrypt(encrypted)).toEqual(original);
    });

    it('encrypt + decrypt round-trip for Payment.callbackData', () => {
      const original = { vnp_ResponseCode: '00', vnp_Amount: 1000000 };
      const encrypted = service.encrypt(original) as string;
      expect(isEncrypted(encrypted)).toBe(true);
      expect(service.decrypt(encrypted)).toEqual(original);
    });

    it('encrypt + decrypt round-trip for simple strings (phone, providerTxId)', () => {
      const phone = '+84912345678';
      const txId = 'txn_12345';
      expect(service.decrypt(service.encrypt(phone) as string)).toBe(phone);
      expect(service.decrypt(service.encrypt(txId) as string)).toBe(txId);
    });

    it('computes deterministic hashes for searchable fields', () => {
      const hash1 = service.computeHash('user@example.com');
      const hash2 = service.computeHash('user@example.com');
      expect(hash1).toBe(hash2);
      expect(hash1).toMatch(/^[0-9a-f]{64}$/);
    });

    it('hash is case-insensitive', () => {
      expect(service.computeHash('User@Example.COM')).toBe(
        service.computeHash('user@example.com'),
      );
    });

    it('does not double-encrypt already encrypted values', () => {
      const encrypted = service.encrypt('test') as string;
      expect(service.isAlreadyEncrypted(encrypted)).toBe(true);
      // If the middleware encounters this, it should skip
    });

    it('decrypts plaintext (non-encrypted) values unchanged', () => {
      expect(service.decrypt('plain text')).toBe('plain text');
      expect(service.decrypt(null)).toBeNull();
      expect(service.decrypt(42)).toBe(42);
    });
  });

  describe('PII field map coverage', () => {
    it('covers User model with email, phone, kycData', () => {
      const userConfig = service.getModelConfig('User');
      expect(userConfig).toBeDefined();
      const fields = userConfig!.fields.map((f) => f.field);
      expect(fields).toContain('email');
      expect(fields).toContain('phone');
      expect(fields).toContain('kycData');
    });

    it('covers Agent model with licenseNumber', () => {
      const agentConfig = service.getModelConfig('Agent');
      expect(agentConfig).toBeDefined();
      expect(agentConfig!.fields.map((f) => f.field)).toContain('licenseNumber');
    });

    it('covers Payment model with providerTxId, callbackData', () => {
      const paymentConfig = service.getModelConfig('Payment');
      expect(paymentConfig).toBeDefined();
      const fields = paymentConfig!.fields.map((f) => f.field);
      expect(fields).toContain('providerTxId');
      expect(fields).toContain('callbackData');
    });

    it('covers Lead model with phone, email', () => {
      const leadConfig = service.getModelConfig('Lead');
      expect(leadConfig).toBeDefined();
      const fields = leadConfig!.fields.map((f) => f.field);
      expect(fields).toContain('phone');
      expect(fields).toContain('email');
    });

    it('covers Inquiry model with phone', () => {
      const inquiryConfig = service.getModelConfig('Inquiry');
      expect(inquiryConfig).toBeDefined();
      expect(inquiryConfig!.fields.map((f) => f.field)).toContain('phone');
    });

    it('marks User.email and User.phone as searchable', () => {
      const userConfig = service.getModelConfig('User')!;
      expect(userConfig.fields.find((f) => f.field === 'email')?.searchable).toBe(true);
      expect(userConfig.fields.find((f) => f.field === 'phone')?.searchable).toBe(true);
      expect(userConfig.fields.find((f) => f.field === 'kycData')?.searchable).toBeFalsy();
    });

    it('marks Lead.email and Lead.phone as searchable', () => {
      const leadConfig = service.getModelConfig('Lead')!;
      expect(leadConfig.fields.find((f) => f.field === 'email')?.searchable).toBe(true);
      expect(leadConfig.fields.find((f) => f.field === 'phone')?.searchable).toBe(true);
    });
  });
});
