import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { EncryptionService } from './encryption.service';

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        EncryptionService,
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string, defaultValue?: string) => {
              if (key === 'ENCRYPTION_KEY') return 'test-encryption-key-for-unit-tests';
              if (key === 'NODE_ENV') return defaultValue ?? 'test';
              return defaultValue;
            }),
          },
        },
      ],
    }).compile();

    service = module.get<EncryptionService>(EncryptionService);
    service.onModuleInit();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('encrypt / decrypt roundtrip', () => {
    it('should encrypt and decrypt a simple string', () => {
      const plaintext = 'Hello, World!';
      const encrypted = service.encrypt(plaintext);
      expect(encrypted).not.toBe(plaintext);
      expect(encrypted.length).toBeGreaterThan(0);

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

    it('should encrypt and decrypt a long string', () => {
      const plaintext = 'A'.repeat(10000);
      const encrypted = service.encrypt(plaintext);
      const decrypted = service.decrypt(encrypted);
      expect(decrypted).toBe(plaintext);
    });

    it('should encrypt and decrypt unicode characters', () => {
      const plaintext = 'Hallo Welt! Umlaute: aou Emojis: ';
      const encrypted = service.encrypt(plaintext);
      const decrypted = service.decrypt(encrypted);
      expect(decrypted).toBe(plaintext);
    });

    it('should produce different ciphertexts for the same plaintext (random IV)', () => {
      const plaintext = 'same input';
      const encrypted1 = service.encrypt(plaintext);
      const encrypted2 = service.encrypt(plaintext);
      expect(encrypted1).not.toBe(encrypted2);
    });

    it('should produce base64-encoded output', () => {
      const plaintext = 'test';
      const encrypted = service.encrypt(plaintext);
      // Base64 regex
      expect(encrypted).toMatch(/^[A-Za-z0-9+/]+=*$/);
    });
  });

  describe('empty string handling', () => {
    it('should return empty string when encrypting empty string', () => {
      expect(service.encrypt('')).toBe('');
    });

    it('should return empty string when decrypting empty string', () => {
      expect(service.decrypt('')).toBe('');
    });
  });

  describe('encryptObject / decryptObject roundtrip', () => {
    it('should encrypt and decrypt a simple object', () => {
      const data = { name: 'John', salary: 50000 };
      const encrypted = service.encryptObject(data);
      expect(typeof encrypted).toBe('string');
      expect(encrypted.length).toBeGreaterThan(0);

      const decrypted = service.decryptObject<typeof data>(encrypted);
      expect(decrypted).toEqual(data);
    });

    it('should encrypt and decrypt a nested object', () => {
      const data = {
        employee: {
          name: 'Jane',
          bankAccount: {
            iban: 'DE89370400440532013000',
            bic: 'COBADEFFXXX',
          },
        },
      };
      const encrypted = service.encryptObject(data);
      const decrypted = service.decryptObject<typeof data>(encrypted);
      expect(decrypted).toEqual(data);
    });

    it('should encrypt and decrypt an array', () => {
      const data = [1, 2, 3, 'four', { five: 5 }];
      const encrypted = service.encryptObject(data);
      const decrypted = service.decryptObject<typeof data>(encrypted);
      expect(decrypted).toEqual(data);
    });

    it('should encrypt and decrypt null values in objects', () => {
      const data = { key: null, other: 'value' };
      const encrypted = service.encryptObject(data);
      const decrypted = service.decryptObject<typeof data>(encrypted);
      expect(decrypted).toEqual(data);
    });
  });

  describe('decryption failure', () => {
    it('should throw on tampered data', () => {
      const encrypted = service.encrypt('secret');
      // Tamper with the encrypted data by flipping a character
      const tampered = encrypted.slice(0, -5) + 'XXXXX';
      expect(() => service.decrypt(tampered)).toThrow('Failed to decrypt data');
    });

    it('should throw on invalid base64 input', () => {
      expect(() => service.decrypt('not-valid-base64!!!')).toThrow();
    });
  });

  describe('generateKey (static)', () => {
    it('should generate a hex string of the correct length (default 32 bytes = 64 hex chars)', () => {
      const key = EncryptionService.generateKey();
      expect(key).toMatch(/^[a-f0-9]+$/);
      expect(key).toHaveLength(64);
    });

    it('should generate a hex string of custom length', () => {
      const key = EncryptionService.generateKey(16);
      expect(key).toMatch(/^[a-f0-9]+$/);
      expect(key).toHaveLength(32); // 16 bytes = 32 hex chars
    });

    it('should generate unique keys', () => {
      const key1 = EncryptionService.generateKey();
      const key2 = EncryptionService.generateKey();
      expect(key1).not.toBe(key2);
    });
  });

  describe('initialization', () => {
    it('should throw in production without ENCRYPTION_KEY', async () => {
      const module: TestingModule = await Test.createTestingModule({
        providers: [
          EncryptionService,
          {
            provide: ConfigService,
            useValue: {
              get: jest.fn((key: string, defaultValue?: string) => {
                if (key === 'ENCRYPTION_KEY') return undefined;
                if (key === 'NODE_ENV') return 'production';
                return defaultValue;
              }),
            },
          },
        ],
      }).compile();

      const svc = module.get<EncryptionService>(EncryptionService);
      expect(() => svc.onModuleInit()).toThrow('ENCRYPTION_KEY environment variable is required in production');
    });

    it('should use fallback key in development without ENCRYPTION_KEY', async () => {
      const module: TestingModule = await Test.createTestingModule({
        providers: [
          EncryptionService,
          {
            provide: ConfigService,
            useValue: {
              get: jest.fn((key: string, defaultValue?: string) => {
                if (key === 'ENCRYPTION_KEY') return undefined;
                if (key === 'NODE_ENV') return defaultValue ?? 'development';
                if (key === 'JWT_SECRET') return defaultValue ?? 'dev-fallback-key';
                return defaultValue;
              }),
            },
          },
        ],
      }).compile();

      const svc = module.get<EncryptionService>(EncryptionService);
      // Should not throw in development
      expect(() => svc.onModuleInit()).not.toThrow();

      // Service should still work
      const encrypted = svc.encrypt('test');
      expect(svc.decrypt(encrypted)).toBe('test');
    });
  });
});
