// crypto-utils exports a module that calls loadOrCreateKey() at load time (line 263).
// The jest.setup.js sets DASHCADDY_ENCRYPTION_KEY env var so it uses a deterministic key.

const cryptoUtils = require('../crypto-utils');

describe('encrypt / decrypt', () => {
  test('round-trips a string', () => {
    const plaintext = 'hello world';
    const encrypted = cryptoUtils.encrypt(plaintext);
    const decrypted = cryptoUtils.decrypt(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  test('round-trips an object via JSON', () => {
    const obj = { user: 'admin', pass: 'secret123' };
    const encrypted = cryptoUtils.encrypt(obj);
    const decrypted = JSON.parse(cryptoUtils.decrypt(encrypted));
    expect(decrypted).toEqual(obj);
  });

  test('encrypted output differs from plaintext', () => {
    const plaintext = 'sensitive data';
    const encrypted = cryptoUtils.encrypt(plaintext);
    expect(encrypted).not.toBe(plaintext);
  });

  test('encrypted format is iv:authTag:ciphertext (3 colon-separated parts)', () => {
    const encrypted = cryptoUtils.encrypt('test');
    const parts = encrypted.split(':');
    expect(parts.length).toBe(3);
  });

  test('each encryption produces different output (random IV)', () => {
    const plaintext = 'same input';
    const enc1 = cryptoUtils.encrypt(plaintext);
    const enc2 = cryptoUtils.encrypt(plaintext);
    expect(enc1).not.toBe(enc2);
    // But both decrypt to same value
    expect(cryptoUtils.decrypt(enc1)).toBe(plaintext);
    expect(cryptoUtils.decrypt(enc2)).toBe(plaintext);
  });

  test('throws on tampered ciphertext', () => {
    const encrypted = cryptoUtils.encrypt('test');
    const parts = encrypted.split(':');
    parts[2] = 'AAAA' + parts[2].slice(4); // tamper with ciphertext
    expect(() => cryptoUtils.decrypt(parts.join(':'))).toThrow();
  });

  test('throws on tampered authTag', () => {
    const encrypted = cryptoUtils.encrypt('test');
    const parts = encrypted.split(':');
    parts[1] = 'AAAA' + parts[1].slice(4); // tamper with auth tag
    expect(() => cryptoUtils.decrypt(parts.join(':'))).toThrow();
  });

  test('throws on invalid encrypted format (wrong number of parts)', () => {
    expect(() => cryptoUtils.decrypt('only:two')).toThrow('Invalid encrypted data format');
    expect(() => cryptoUtils.decrypt('just-one')).toThrow('Invalid encrypted data format');
  });

  test('handles empty string', () => {
    const encrypted = cryptoUtils.encrypt('');
    expect(cryptoUtils.decrypt(encrypted)).toBe('');
  });

  test('handles special characters', () => {
    const special = 'p@$$w0rd!<>&"\';DROP TABLE--';
    expect(cryptoUtils.decrypt(cryptoUtils.encrypt(special))).toBe(special);
  });
});

describe('isEncrypted', () => {
  test('returns true for encrypted strings', () => {
    const encrypted = cryptoUtils.encrypt('test');
    expect(cryptoUtils.isEncrypted(encrypted)).toBe(true);
  });

  test('returns false for plain strings', () => {
    expect(cryptoUtils.isEncrypted('hello world')).toBe(false);
  });

  test('returns false for non-string input', () => {
    expect(cryptoUtils.isEncrypted(123)).toBe(false);
    expect(cryptoUtils.isEncrypted(null)).toBe(false);
    expect(cryptoUtils.isEncrypted(undefined)).toBe(false);
  });

  test('returns false for string with wrong number of colons', () => {
    expect(cryptoUtils.isEncrypted('one:two')).toBe(false);
    expect(cryptoUtils.isEncrypted('one:two:three:four')).toBe(false);
  });
});

describe('encryptFields', () => {
  test('encrypts only specified fields', () => {
    const obj = { username: 'admin', password: 'secret', role: 'user' };
    const result = cryptoUtils.encryptFields(obj, ['password']);
    expect(result.username).toBe('admin');
    expect(result.role).toBe('user');
    expect(result.password).not.toBe('secret');
    expect(cryptoUtils.isEncrypted(result.password)).toBe(true);
  });

  test('leaves non-specified fields unchanged', () => {
    const obj = { a: '1', b: '2', c: '3' };
    const result = cryptoUtils.encryptFields(obj, ['a']);
    expect(result.b).toBe('2');
    expect(result.c).toBe('3');
  });

  test('adds _encrypted marker', () => {
    const result = cryptoUtils.encryptFields({ x: 'y' }, ['x']);
    expect(result._encrypted).toBe(true);
  });

  test('adds _encryptedFields list', () => {
    const result = cryptoUtils.encryptFields({ x: 'y' }, ['x']);
    expect(result._encryptedFields).toEqual(['x']);
  });

  test('does not double-encrypt already-encrypted fields', () => {
    const obj = { password: 'secret' };
    const first = cryptoUtils.encryptFields(obj, ['password']);
    const second = cryptoUtils.encryptFields(first, ['password']);
    // Should still be decryptable to original
    expect(cryptoUtils.decrypt(second.password)).toBe('secret');
  });

  test('skips null/undefined fields', () => {
    const obj = { a: null, b: undefined, c: 'val' };
    const result = cryptoUtils.encryptFields(obj, ['a', 'b', 'c']);
    expect(result.a).toBeNull();
    expect(result.b).toBeUndefined();
    expect(cryptoUtils.isEncrypted(result.c)).toBe(true);
  });
});

describe('decryptFields', () => {
  test('decrypts specified fields', () => {
    const encrypted = cryptoUtils.encryptFields({ password: 'secret', name: 'test' }, ['password']);
    const decrypted = cryptoUtils.decryptFields(encrypted, ['password']);
    expect(decrypted.password).toBe('secret');
    expect(decrypted.name).toBe('test');
  });

  test('returns object without encryption markers', () => {
    const encrypted = cryptoUtils.encryptFields({ x: 'y' }, ['x']);
    const decrypted = cryptoUtils.decryptFields(encrypted);
    expect(decrypted._encrypted).toBeUndefined();
    expect(decrypted._encryptedFields).toBeUndefined();
  });

  test('returns object as-is when _encrypted is false/absent', () => {
    const obj = { a: '1', b: '2' };
    const result = cryptoUtils.decryptFields(obj);
    expect(result).toEqual(obj);
  });

  test('uses _encryptedFields when fields param is null', () => {
    const encrypted = cryptoUtils.encryptFields({ password: 'secret', token: 'abc' }, ['password', 'token']);
    const decrypted = cryptoUtils.decryptFields(encrypted);
    expect(decrypted.password).toBe('secret');
    expect(decrypted.token).toBe('abc');
  });
});

describe('encryptFields + decryptFields round-trip', () => {
  test('full round-trip preserves all field values', () => {
    const original = { user: 'admin', pass: 'p@ss', apiKey: 'key123', role: 'editor' };
    const fields = ['pass', 'apiKey'];
    const encrypted = cryptoUtils.encryptFields(original, fields);
    const decrypted = cryptoUtils.decryptFields(encrypted, fields);
    expect(decrypted.user).toBe(original.user);
    expect(decrypted.pass).toBe(original.pass);
    expect(decrypted.apiKey).toBe(original.apiKey);
    expect(decrypted.role).toBe(original.role);
  });
});

describe('migrateToEncrypted', () => {
  test('encrypts plaintext credentials', () => {
    const plain = { password: 'secret', token: 'abc123' };
    const result = cryptoUtils.migrateToEncrypted(plain, ['password', 'token']);
    expect(result._encrypted).toBe(true);
    expect(cryptoUtils.isEncrypted(result.password)).toBe(true);
  });

  test('returns already-encrypted credentials unchanged', () => {
    const encrypted = cryptoUtils.encryptFields({ password: 'secret' }, ['password']);
    const result = cryptoUtils.migrateToEncrypted(encrypted, ['password']);
    expect(result).toEqual(encrypted);
  });
});

describe('loadOrCreateKey', () => {
  test('returns a buffer', () => {
    const key = cryptoUtils.loadOrCreateKey();
    expect(Buffer.isBuffer(key)).toBe(true);
  });

  test('returns 32-byte key', () => {
    const key = cryptoUtils.loadOrCreateKey();
    expect(key.length).toBe(32);
  });

  test('returns cached key on subsequent calls', () => {
    const key1 = cryptoUtils.loadOrCreateKey();
    const key2 = cryptoUtils.loadOrCreateKey();
    expect(key1).toBe(key2); // same reference (cached)
  });
});

describe('readEncryptedFile', () => {
  const fs = require('fs');
  const os = require('os');
  const path = require('path');

  test('returns null when file does not exist', () => {
    const result = cryptoUtils.readEncryptedFile('/nonexistent/file.json');
    expect(result).toBeNull();
  });

  test('reads and returns plaintext JSON file', () => {
    const tmpFile = path.join(os.tmpdir(), 'dashcaddy-test-plain.json');
    fs.writeFileSync(tmpFile, JSON.stringify({ username: 'admin', password: 'plain' }));
    try {
      const result = cryptoUtils.readEncryptedFile(tmpFile);
      expect(result.username).toBe('admin');
      expect(result.password).toBe('plain');
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });

  test('reads and decrypts encrypted JSON file', () => {
    const tmpFile = path.join(os.tmpdir(), 'dashcaddy-test-encrypted.json');
    const data = { username: 'admin', password: 'secret' };
    cryptoUtils.writeEncryptedFile(tmpFile, data, ['password']);
    try {
      const result = cryptoUtils.readEncryptedFile(tmpFile, ['password']);
      expect(result.username).toBe('admin');
      expect(result.password).toBe('secret');
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });

  test('returns null on JSON parse error', () => {
    const tmpFile = path.join(os.tmpdir(), 'dashcaddy-test-bad.json');
    fs.writeFileSync(tmpFile, 'not json at all {{{');
    try {
      const result = cryptoUtils.readEncryptedFile(tmpFile);
      expect(result).toBeNull();
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });
});

describe('writeEncryptedFile', () => {
  const fs = require('fs');
  const os = require('os');
  const path = require('path');

  test('writes valid JSON to disk', () => {
    const tmpFile = path.join(os.tmpdir(), 'dashcaddy-test-write.json');
    cryptoUtils.writeEncryptedFile(tmpFile, { user: 'test', token: 'abc' }, ['token']);
    try {
      const content = JSON.parse(fs.readFileSync(tmpFile, 'utf8'));
      expect(content._encrypted).toBe(true);
      expect(content.user).toBe('test');
      expect(cryptoUtils.isEncrypted(content.token)).toBe(true);
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });

  test('encrypts specified fields', () => {
    const tmpFile = path.join(os.tmpdir(), 'dashcaddy-test-write2.json');
    cryptoUtils.writeEncryptedFile(tmpFile, { a: 'plain', b: 'secret' }, ['b']);
    try {
      const content = JSON.parse(fs.readFileSync(tmpFile, 'utf8'));
      expect(content.a).toBe('plain');
      expect(content.b).not.toBe('secret');
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });
});
