// Mock dependencies before requiring the module
jest.mock('../src/security/keychain-manager', () => ({
  available: false,
  store: jest.fn().mockResolvedValue(false),
  retrieve: jest.fn().mockResolvedValue(null),
  delete: jest.fn().mockResolvedValue(true),
}));

jest.mock('../src/security/crypto-utils', () => ({
  encrypt: jest.fn(data => `enc:tag:${Buffer.from(String(data)).toString('base64')}`),
  decrypt: jest.fn(data => {
    const parts = data.split(':');
    return Buffer.from(parts[2], 'base64').toString('utf8');
  }),
  isEncrypted: jest.fn(data => typeof data === 'string' && data.startsWith('enc:')),
  loadOrCreateKey: jest.fn(() => Buffer.alloc(32, 'k')),
  rotateKey: jest.fn(() => ({ oldKey: Buffer.alloc(32, 'k'), newKey: Buffer.alloc(32, 'n') })),
}));

jest.mock('proper-lockfile', () => ({
  lock: jest.fn().mockResolvedValue(jest.fn().mockResolvedValue()),
  unlock: jest.fn().mockResolvedValue(),
  check: jest.fn().mockResolvedValue(false),
}));

jest.mock('fs', () => ({
  existsSync: jest.fn().mockReturnValue(true),
  readFileSync: jest.fn().mockReturnValue('{}'),
  writeFileSync: jest.fn(),
  mkdirSync: jest.fn(),
}));

describe('CredentialManager', () => {
  let credentialManager;
  let fs, lockfile, keychainManager, cryptoUtils;

  beforeEach(() => {
    jest.resetModules();

    // Re-get mocked modules
    fs = require('fs');
    lockfile = require('proper-lockfile');
    keychainManager = require('../src/security/keychain-manager');
    cryptoUtils = require('../src/security/crypto-utils');

    // Reset mock implementations
    fs.existsSync.mockReturnValue(true);
    fs.readFileSync.mockReturnValue('{}');
    fs.writeFileSync.mockImplementation(() => {});
    lockfile.lock.mockResolvedValue(jest.fn().mockResolvedValue());
    keychainManager.available = false;

    credentialManager = require('../src/managers/credential-manager');
    credentialManager.cache.clear();
  });

  describe('store', () => {
    it('stores value in encrypted file when keychain unavailable', async () => {
      const result = await credentialManager.store('test.key', 'secret-value');
      expect(result).toBe(true);
      expect(cryptoUtils.encrypt).toHaveBeenCalledWith('secret-value');
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('stores value in keychain when available', async () => {
      keychainManager.available = true;
      // Need to get a fresh instance that sees available=true
      jest.resetModules();
      fs = require('fs');
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue('{}');
      fs.writeFileSync.mockImplementation(() => {});
      lockfile = require('proper-lockfile');
      lockfile.lock.mockResolvedValue(jest.fn().mockResolvedValue());
      keychainManager = require('../src/security/keychain-manager');
      keychainManager.available = true;
      keychainManager.store.mockResolvedValue(true);
      credentialManager = require('../src/managers/credential-manager');

      const result = await credentialManager.store('test.key', 'value');
      expect(result).toBe(true);
      expect(keychainManager.store).toHaveBeenCalledWith('test.key', 'value');
    });

    it('falls back to file if keychain store fails', async () => {
      keychainManager.available = true;
      jest.resetModules();
      fs = require('fs');
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue('{}');
      fs.writeFileSync.mockImplementation(() => {});
      lockfile = require('proper-lockfile');
      lockfile.lock.mockResolvedValue(jest.fn().mockResolvedValue());
      keychainManager = require('../src/security/keychain-manager');
      keychainManager.available = true;
      keychainManager.store.mockResolvedValue(false);
      cryptoUtils = require('../src/security/crypto-utils');
      credentialManager = require('../src/managers/credential-manager');

      const result = await credentialManager.store('test.key', 'value');
      expect(result).toBe(true);
      expect(cryptoUtils.encrypt).toHaveBeenCalled();
    });

    it('rejects empty key', async () => {
      const result = await credentialManager.store('', 'value');
      expect(result).toBe(false);
    });

    it('rejects empty value', async () => {
      const result = await credentialManager.store('key', '');
      expect(result).toBe(false);
    });

    it('updates cache after storing', async () => {
      await credentialManager.store('test.key', 'cached-value');
      expect(credentialManager.cache.has('test.key')).toBe(true);
      expect(credentialManager.cache.get('test.key').value).toBe('cached-value');
    });
  });

  describe('retrieve', () => {
    it('returns cached value within TTL', async () => {
      credentialManager.cache.set('cached.key', {
        value: 'cached-val',
        exp: Date.now() + 60000
      });
      const result = await credentialManager.retrieve('cached.key');
      expect(result).toBe('cached-val');
    });

    it('does not return expired cache entry', async () => {
      credentialManager.cache.set('expired.key', {
        value: 'old-val',
        exp: Date.now() - 1000
      });
      // Set up file to return data
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'expired.key': { value: 'enc:tag:' + Buffer.from('file-val').toString('base64') }
      }));
      const result = await credentialManager.retrieve('expired.key');
      expect(result).toBe('file-val');
    });

    it('retrieves from encrypted file as fallback', async () => {
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'file.key': { value: 'enc:tag:' + Buffer.from('secret').toString('base64') }
      }));
      const result = await credentialManager.retrieve('file.key');
      expect(result).toBe('secret');
    });

    it('returns null when key not found', async () => {
      fs.readFileSync.mockReturnValue('{}');
      const result = await credentialManager.retrieve('missing.key');
      expect(result).toBeNull();
    });

    it('returns null on error', async () => {
      fs.existsSync.mockReturnValue(false);
      fs.readFileSync.mockImplementation(() => { throw new Error('fail'); });
      const result = await credentialManager.retrieve('broken.key');
      expect(result).toBeNull();
    });
  });

  describe('delete', () => {
    it('removes from cache, keychain, and file', async () => {
      credentialManager.cache.set('del.key', { value: 'x', exp: Date.now() + 60000 });
      fs.readFileSync.mockReturnValue(JSON.stringify({ 'del.key': { value: 'x' } }));

      const result = await credentialManager.delete('del.key');
      expect(result).toBe(true);
      expect(credentialManager.cache.has('del.key')).toBe(false);
    });

    it('returns false on error', async () => {
      lockfile.lock.mockRejectedValue(new Error('lock fail'));
      const result = await credentialManager.delete('fail.key');
      expect(result).toBe(false);
    });
  });

  describe('list', () => {
    it('returns all keys from credentials file', async () => {
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'key1': { value: 'a' },
        'key2': { value: 'b' }
      }));
      const keys = await credentialManager.list();
      expect(keys).toEqual(['key1', 'key2']);
    });

    it('returns empty array on error', async () => {
      fs.existsSync.mockReturnValue(false);
      const keys = await credentialManager.list();
      expect(keys).toEqual([]);
    });
  });

  describe('getMetadata', () => {
    it('returns metadata for a credential', async () => {
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'test.key': { value: 'x', metadata: { provider: 'cloudflare' } }
      }));
      const meta = await credentialManager.getMetadata('test.key');
      expect(meta).toEqual({ provider: 'cloudflare' });
    });

    it('returns null when key not found', async () => {
      fs.readFileSync.mockReturnValue('{}');
      const meta = await credentialManager.getMetadata('missing');
      expect(meta).toBeNull();
    });
  });

  describe('_lockedUpdate', () => {
    it('acquires lock, reads, applies update, writes, releases', async () => {
      const releaseFn = jest.fn().mockResolvedValue();
      lockfile.lock.mockResolvedValue(releaseFn);
      fs.readFileSync.mockReturnValue(JSON.stringify({ a: 1 }));

      await credentialManager._lockedUpdate(creds => {
        creds.b = 2;
        return creds;
      });

      expect(lockfile.lock).toHaveBeenCalled();
      expect(fs.writeFileSync).toHaveBeenCalled();
      const writtenData = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
      expect(writtenData).toEqual({ a: 1, b: 2 });
      expect(releaseFn).toHaveBeenCalled();
    });

    it('throws on ELOCKED error', async () => {
      const error = new Error('locked');
      error.code = 'ELOCKED';
      lockfile.lock.mockRejectedValue(error);

      await expect(credentialManager._lockedUpdate(() => ({}))).rejects.toThrow('locked by another process');
    });

    it('releases lock even on error', async () => {
      const releaseFn = jest.fn().mockResolvedValue();
      lockfile.lock.mockResolvedValue(releaseFn);
      fs.readFileSync.mockReturnValue('{}');

      await expect(
        credentialManager._lockedUpdate(() => { throw new Error('update error'); })
      ).rejects.toThrow('update error');

      expect(releaseFn).toHaveBeenCalled();
    });
  });

  describe('rotateEncryptionKey', () => {
    it('decrypts all credentials then re-encrypts with new key', async () => {
      const releaseFn = jest.fn().mockResolvedValue();
      lockfile.lock.mockResolvedValue(releaseFn);
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'key1': { value: 'enc:tag:' + Buffer.from('secret1').toString('base64'), metadata: {} }
      }));

      const result = await credentialManager.rotateEncryptionKey();
      expect(result).toBe(true);
      expect(cryptoUtils.rotateKey).toHaveBeenCalled();
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('clears cache after rotation', async () => {
      const releaseFn = jest.fn().mockResolvedValue();
      lockfile.lock.mockResolvedValue(releaseFn);
      credentialManager.cache.set('x', { value: 'y', exp: Date.now() + 60000 });
      // Must have non-empty credentials so code path reaches cache.clear()
      fs.readFileSync.mockReturnValue(JSON.stringify({
        'key1': { value: 'enc:tag:' + Buffer.from('val').toString('base64'), metadata: {} }
      }));

      await credentialManager.rotateEncryptionKey();
      expect(credentialManager.cache.size).toBe(0);
    });

    it('returns false on error', async () => {
      lockfile.lock.mockRejectedValue(new Error('nope'));
      const result = await credentialManager.rotateEncryptionKey();
      expect(result).toBe(false);
    });
  });

  describe('exportBackup / importBackup', () => {
    it('exportBackup returns encrypted JSON string', async () => {
      fs.readFileSync.mockReturnValue(JSON.stringify({ key1: { value: 'x' } }));
      const backup = await credentialManager.exportBackup();
      expect(cryptoUtils.encrypt).toHaveBeenCalled();
      expect(typeof backup).toBe('string');
    });

    it('importBackup decrypts and replaces credentials', async () => {
      const backupData = JSON.stringify({
        version: '1.0',
        exportedAt: new Date().toISOString(),
        credentials: { imported: { value: 'y' } }
      });
      const encrypted = `enc:tag:${Buffer.from(backupData).toString('base64')}`;

      const releaseFn = jest.fn().mockResolvedValue();
      lockfile.lock.mockResolvedValue(releaseFn);
      fs.readFileSync.mockReturnValue('{}');

      const result = await credentialManager.importBackup(encrypted);
      expect(result).toBe(true);
    });

    it('importBackup rejects unsupported backup version', async () => {
      const backupData = JSON.stringify({ version: '2.0', credentials: {} });
      const encrypted = `enc:tag:${Buffer.from(backupData).toString('base64')}`;

      const result = await credentialManager.importBackup(encrypted);
      expect(result).toBe(false);
    });

    it('importBackup returns false on error', async () => {
      cryptoUtils.decrypt.mockImplementationOnce(() => { throw new Error('bad'); });
      const result = await credentialManager.importBackup('bad-data');
      expect(result).toBe(false);
    });
  });

  describe('cache TTL', () => {
    it('cache entries expire after TTL', async () => {
      credentialManager.cache.set('ttl.key', {
        value: 'val',
        exp: Date.now() - 1 // Already expired
      });
      fs.readFileSync.mockReturnValue('{}');
      const result = await credentialManager.retrieve('ttl.key');
      expect(result).toBeNull();
      expect(credentialManager.cache.has('ttl.key')).toBe(false);
    });

    it('new store refreshes cache TTL', async () => {
      await credentialManager.store('fresh.key', 'val');
      const cached = credentialManager.cache.get('fresh.key');
      expect(cached.exp).toBeGreaterThan(Date.now());
    });
  });
});
