// Backup Manager Tests
// Validates backup/restore lifecycle for DashCaddy configurations

jest.mock('fs');
jest.mock('child_process');
jest.mock('../src/managers/credential-manager', () => ({
  exportBackup: jest.fn().mockReturnValue({ encrypted: 'cred-data' }),
  importBackup: jest.fn()
}));
jest.mock('../src/managers/resource-monitor', () => ({
  exportStats: jest.fn().mockReturnValue({ stats: [{ cpu: 10 }] }),
  importStats: jest.fn()
}));

const fs = require('fs');
const crypto = require('crypto');
const credentialManager = require('../src/managers/credential-manager');
const resourceMonitor = require('../src/managers/resource-monitor');

// Setup defaults BEFORE requiring singleton (constructor calls loadConfig/loadHistory)
fs.existsSync.mockReturnValue(false);
fs.readFileSync.mockReturnValue('{}');
fs.writeFileSync.mockReturnValue(undefined);
fs.mkdirSync.mockReturnValue(undefined);
fs.unlinkSync.mockReturnValue(undefined);

const backupManager = require('../src/utilities/backup-manager');

beforeEach(() => {
  jest.clearAllMocks();
  jest.useFakeTimers();

  // Restore defaults
  fs.existsSync.mockReturnValue(false);
  fs.readFileSync.mockReturnValue('{}');
  fs.writeFileSync.mockReturnValue(undefined);
  fs.mkdirSync.mockReturnValue(undefined);
  fs.unlinkSync.mockReturnValue(undefined);

  // Reset internal state
  backupManager.history = [];
  backupManager.config = { backups: {}, defaultRetention: { keep: 7 } };
  backupManager.running = false;
  // Clear all scheduled jobs directly (stop() only clears when running=true)
  for (const [, job] of backupManager.scheduledJobs.entries()) {
    clearInterval(job);
  }
  backupManager.scheduledJobs.clear();
});

afterEach(() => {
  backupManager.stop();
  jest.useRealTimers();
});

describe('BackupManager — backup/restore lifecycle', () => {

  describe('constructor and config', () => {
    it('starts with empty config when no config file exists', () => {
      const config = backupManager.getConfig();
      expect(config.backups).toEqual({});
      expect(config.defaultRetention).toEqual({ keep: 7 });
    });

    it('loadConfig returns saved config when file exists', () => {
      const savedConfig = {
        backups: { daily: { enabled: true, schedule: 'daily' } },
        defaultRetention: { keep: 14 }
      };
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify(savedConfig));
      const config = backupManager.loadConfig();
      expect(config.backups.daily).toBeDefined();
      expect(config.defaultRetention.keep).toBe(14);
    });

    it('loadConfig returns defaults on error', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation(() => { throw new Error('read error'); });
      const config = backupManager.loadConfig();
      expect(config.backups).toEqual({});
    });

    it('loadHistory returns empty array when no file', () => {
      fs.existsSync.mockReturnValue(false);
      expect(backupManager.loadHistory()).toEqual([]);
    });

    it('loadHistory loads saved entries', () => {
      const history = [{ id: 'test-1', status: 'success' }];
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify(history));
      expect(backupManager.loadHistory()).toEqual(history);
    });
  });

  describe('start/stop scheduler', () => {
    it('does nothing on double start', () => {
      backupManager.start();
      backupManager.start(); // should not throw
      expect(backupManager.running).toBe(true);
    });

    it('does nothing on stop when not running', () => {
      backupManager.stop(); // should not throw
      expect(backupManager.running).toBe(false);
    });

    it('clears scheduled jobs on stop', () => {
      backupManager.scheduledJobs.set('test', setInterval(() => {}, 10000));
      backupManager.running = true;
      backupManager.stop();
      expect(backupManager.scheduledJobs.size).toBe(0);
      expect(backupManager.running).toBe(false);
    });
  });

  describe('scheduleBackup intervals', () => {
    it('schedules hourly backup', () => {
      backupManager.scheduleBackup('test', { schedule: 'hourly' });
      expect(backupManager.scheduledJobs.has('test')).toBe(true);
    });

    it('schedules daily backup', () => {
      backupManager.scheduleBackup('test', { schedule: 'daily' });
      expect(backupManager.scheduledJobs.has('test')).toBe(true);
    });

    it('schedules weekly backup', () => {
      backupManager.scheduleBackup('test', { schedule: 'weekly' });
      expect(backupManager.scheduledJobs.has('test')).toBe(true);
    });

    it('schedules monthly backup', () => {
      backupManager.scheduleBackup('test', { schedule: 'monthly' });
      expect(backupManager.scheduledJobs.has('test')).toBe(true);
    });

    it('accepts custom interval in minutes', () => {
      backupManager.scheduleBackup('test', { schedule: '30' });
      expect(backupManager.scheduledJobs.has('test')).toBe(true);
    });

    it('rejects invalid schedule', () => {
      backupManager.scheduleBackup('test', { schedule: 'bogus' });
      expect(backupManager.scheduledJobs.has('test')).toBe(false);
    });
  });

  describe('compress/decompress', () => {
    it('round-trips data through gzip', async () => {
      const original = { version: '1.0', data: { services: [{ id: 'plex' }] } };
      const compressed = await backupManager.compressBackup(original);
      expect(Buffer.isBuffer(compressed)).toBe(true);

      const decompressed = await backupManager.decompressBackup(compressed);
      expect(decompressed).toEqual(original);
    });

    it('compressed output is smaller than JSON', async () => {
      const data = { bigArray: Array(100).fill({ id: 'test', name: 'test-service' }) };
      const compressed = await backupManager.compressBackup(data);
      expect(compressed.length).toBeLessThan(JSON.stringify(data).length);
    });
  });

  describe('encrypt/decrypt (AES-256-GCM)', () => {
    const testKey = crypto.randomBytes(32).toString('hex');

    it('round-trips data through encryption', async () => {
      const original = Buffer.from('DashCaddy backup data');
      const encrypted = await backupManager.encryptBackup(original, testKey);
      const decrypted = await backupManager.decryptBackup(encrypted, testKey);
      expect(decrypted.toString()).toBe('DashCaddy backup data');
    });

    it('encrypted format is iv:authTag:ciphertext (base64)', async () => {
      const data = Buffer.from('test');
      const encrypted = await backupManager.encryptBackup(data, testKey);
      const parts = encrypted.toString().split(':');
      expect(parts.length).toBeGreaterThanOrEqual(3);
    });

    it('rejects tampered data (auth tag mismatch)', async () => {
      const data = Buffer.from('test');
      const encrypted = await backupManager.encryptBackup(data, testKey);
      // Corrupt the authTag so the GCM integrity check is guaranteed to fail.
      // The format is iv:authTag:ciphertext (all base64). We flip all bits of
      // the first authTag byte — XOR with 0xFF always changes the value, so
      // this can never be a no-op (unlike replacing a base64 char with a fixed
      // char, which collides ~1/64 of the time when that char already matches).
      const parts = encrypted.toString().split(':');
      const authTagBuf = Buffer.from(parts[1], 'base64');
      authTagBuf[0] ^= 0xFF;
      parts[1] = authTagBuf.toString('base64');
      const tampered = Buffer.from(parts.join(':'));
      await expect(backupManager.decryptBackup(tampered, testKey))
        .rejects.toThrow();
    });

    it('rejects wrong key', async () => {
      const data = Buffer.from('test');
      const encrypted = await backupManager.encryptBackup(data, testKey);
      const wrongKey = crypto.randomBytes(32).toString('hex');
      await expect(backupManager.decryptBackup(encrypted, wrongKey))
        .rejects.toThrow();
    });

    it('rejects invalid format (fewer than 3 parts)', async () => {
      await expect(backupManager.decryptBackup(Buffer.from('onlyonepart'), testKey))
        .rejects.toThrow('Invalid encrypted backup format');
    });
  });

  describe('calculateChecksum', () => {
    it('returns SHA-256 hex digest', () => {
      const data = Buffer.from('test data');
      const checksum = backupManager.calculateChecksum(data);
      expect(checksum).toMatch(/^[a-f0-9]{64}$/);
    });

    it('same data produces same checksum', () => {
      const data = Buffer.from('DashCaddy');
      expect(backupManager.calculateChecksum(data))
        .toBe(backupManager.calculateChecksum(data));
    });

    it('different data produces different checksum', () => {
      expect(backupManager.calculateChecksum(Buffer.from('A')))
        .not.toBe(backupManager.calculateChecksum(Buffer.from('B')));
    });
  });

  describe('saveToLocal', () => {
    it('creates backup directory if missing', async () => {
      fs.existsSync.mockReturnValue(false);
      await backupManager.saveToLocal(Buffer.from('data'), { path: '/custom/backups' }, 'test-123');
      expect(fs.mkdirSync).toHaveBeenCalledWith('/custom/backups', { recursive: true });
    });

    it('writes backup file with correct name', async () => {
      fs.existsSync.mockReturnValue(true);
      const result = await backupManager.saveToLocal(Buffer.from('data'), {}, 'daily-1234');
      expect(fs.writeFileSync).toHaveBeenCalledWith(
        expect.stringContaining('daily-1234.backup'),
        expect.any(Buffer)
      );
      expect(result.type).toBe('local');
      expect(result.size).toBe(4);
    });
  });

  describe('verifyBackup', () => {
    it('passes when checksum matches', async () => {
      const data = Buffer.from('verified');
      const checksum = crypto.createHash('sha256').update(data).digest('hex');
      fs.readFileSync.mockReturnValue(data);

      const result = await backupManager.verifyBackup({ type: 'local', path: '/backup.dat' }, checksum);
      expect(result).toBe(true);
    });

    it('throws on checksum mismatch', async () => {
      fs.readFileSync.mockReturnValue(Buffer.from('tampered'));
      await expect(backupManager.verifyBackup(
        { type: 'local', path: '/backup.dat' },
        'wrong-checksum'
      )).rejects.toThrow('checksum mismatch');
    });
  });

  describe('history management', () => {
    it('addToHistory appends and saves', () => {
      backupManager.addToHistory({ id: 'test-1', status: 'success' });
      expect(backupManager.getHistory()).toHaveLength(1);
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('caps history at 100 entries', () => {
      for (let i = 0; i < 110; i++) {
        backupManager.addToHistory({ id: `test-${i}`, status: 'success' });
      }
      expect(backupManager.history.length).toBe(100);
    });

    it('getHistory returns newest first', () => {
      backupManager.addToHistory({ id: 'old', status: 'success' });
      backupManager.addToHistory({ id: 'new', status: 'success' });
      const history = backupManager.getHistory();
      expect(history[0].id).toBe('new');
      expect(history[1].id).toBe('old');
    });

    it('getHistory respects limit', () => {
      for (let i = 0; i < 10; i++) {
        backupManager.addToHistory({ id: `test-${i}`, status: 'success' });
      }
      expect(backupManager.getHistory(3)).toHaveLength(3);
    });
  });

  describe('updateConfig', () => {
    it('merges new config and saves', () => {
      backupManager.updateConfig({ customSetting: true });
      expect(backupManager.getConfig().customSetting).toBe(true);
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('restarts scheduler on config update', () => {
      backupManager.start();
      expect(backupManager.running).toBe(true);
      backupManager.updateConfig({ backups: {} });
      // Should still be running after restart
      expect(backupManager.running).toBe(true);
    });
  });

  describe('backupServices / backupConfig', () => {
    it('reads services.json when it exists', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }]));
      const result = backupManager.backupServices();
      expect(result).toEqual([{ id: 'plex' }]);
    });

    it('returns null when services.json missing', () => {
      fs.existsSync.mockReturnValue(false);
      expect(backupManager.backupServices()).toBeNull();
    });

    it('returns null on read error', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation(() => { throw new Error('read error'); });
      expect(backupManager.backupServices()).toBeNull();
    });

    it('reads config.json when it exists', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify({ tld: '.sami' }));
      const result = backupManager.backupConfig();
      expect(result).toEqual({ tld: '.sami' });
    });
  });

  describe('cleanupOldBackups', () => {
    it('deletes backups beyond retention limit', async () => {
      // Add 5 successful backups
      for (let i = 0; i < 5; i++) {
        backupManager.history.push({
          id: `daily-${i}`,
          name: 'daily',
          status: 'success',
          timestamp: new Date(2026, 0, i + 1).toISOString(),
          locations: [{ type: 'local', path: `/backups/daily-${i}.backup` }]
        });
      }
      fs.existsSync.mockReturnValue(true);

      await backupManager.cleanupOldBackups('daily', { keep: 2 });

      // Should delete 3 oldest
      expect(fs.unlinkSync).toHaveBeenCalledTimes(3);
      // History should have 2 remaining for 'daily'
      const remaining = backupManager.history.filter(b => b.name === 'daily');
      expect(remaining).toHaveLength(2);
    });

    it('keeps all when under retention limit', async () => {
      backupManager.history.push({
        id: 'daily-1', name: 'daily', status: 'success',
        timestamp: new Date().toISOString(),
        locations: [{ type: 'local', path: '/backups/daily-1.backup' }]
      });

      await backupManager.cleanupOldBackups('daily', { keep: 7 });
      expect(fs.unlinkSync).not.toHaveBeenCalled();
    });
  });

  describe('backupCredentials / backupStats', () => {
    it('returns credential export data', () => {
      const result = backupManager.backupCredentials();
      expect(result).toEqual({ encrypted: 'cred-data' });
      expect(credentialManager.exportBackup).toHaveBeenCalled();
    });

    it('returns null on credential export error', () => {
      credentialManager.exportBackup.mockImplementationOnce(() => { throw new Error('no key'); });
      expect(backupManager.backupCredentials()).toBeNull();
    });

    it('returns stats export data', () => {
      const result = backupManager.backupStats();
      expect(result).toEqual({ stats: [{ cpu: 10 }] });
      expect(resourceMonitor.exportStats).toHaveBeenCalled();
    });

    it('returns null on stats export error', () => {
      resourceMonitor.exportStats.mockImplementationOnce(() => { throw new Error('no stats'); });
      expect(backupManager.backupStats()).toBeNull();
    });
  });

  describe('createBackupData', () => {
    it('includes all sources when "all" specified', async () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation((filePath) => {
        if (typeof filePath === 'string') {
          if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }]);
          if (filePath.includes('config')) return JSON.stringify({ tld: '.sami' });
        }
        return '{}';
      });

      const data = await backupManager.createBackupData(['all']);
      expect(data.version).toBe('1.0');
      expect(data.data.services).toEqual([{ id: 'plex' }]);
      expect(data.data.config).toEqual({ tld: '.sami' });
      expect(data.data.credentials).toEqual({ encrypted: 'cred-data' });
      expect(data.data.stats).toEqual({ stats: [{ cpu: 10 }] });
    });

    it('includes only credentials when specified', async () => {
      const data = await backupManager.createBackupData(['credentials']);
      expect(data.data.credentials).toEqual({ encrypted: 'cred-data' });
      expect(data.data.services).toBeUndefined();
    });

    it('includes only stats when specified', async () => {
      const data = await backupManager.createBackupData(['stats']);
      expect(data.data.stats).toEqual({ stats: [{ cpu: 10 }] });
      expect(data.data.services).toBeUndefined();
    });
  });

  describe('saveToDestination', () => {
    it('routes to saveToLocal for local type', async () => {
      fs.existsSync.mockReturnValue(true);
      const result = await backupManager.saveToDestination(Buffer.from('data'), { type: 'local' }, 'bk-1');
      expect(result.type).toBe('local');
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('throws for unsupported destination type', async () => {
      await expect(backupManager.saveToDestination(Buffer.from('data'), { type: 's3' }, 'bk-1'))
        .rejects.toThrow('Unsupported destination type: s3');
    });
  });

  describe('executeBackup', () => {
    it('runs full backup pipeline and records success in history', async () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation((filePath) => {
        if (typeof filePath === 'string') {
          if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }]);
          if (filePath.includes('config')) return JSON.stringify({ tld: '.sami' });
        }
        return '{}';
      });

      const events = [];
      backupManager.on('backup-start', e => events.push({ type: 'start', ...e }));
      backupManager.on('backup-complete', e => events.push({ type: 'complete', ...e }));

      const result = await backupManager.executeBackup('daily', {
        include: ['services', 'config'],
        destinations: [{ type: 'local' }],
        verify: false
      });

      expect(result.status).toBe('success');
      expect(result.name).toBe('daily');
      expect(result.compressed).toBe(true);
      expect(result.size).toBeGreaterThan(0);
      expect(backupManager.history).toHaveLength(1);
      expect(events).toHaveLength(2);
      expect(events[0].type).toBe('start');
      expect(events[1].type).toBe('complete');

      backupManager.removeAllListeners();
    });

    it('runs encrypted backup pipeline', async () => {
      const key = crypto.randomBytes(32).toString('hex');
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }]));

      const result = await backupManager.executeBackup('encrypted', {
        include: ['services'],
        destinations: [{ type: 'local' }],
        encrypt: true,
        encryptionKey: key,
        verify: false
      });

      expect(result.status).toBe('success');
      expect(result.encrypted).toBe(true);
    });

    it('records failure in history when all destinations fail', async () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }]));
      fs.writeFileSync.mockImplementation((path) => {
        if (typeof path === 'string' && path.includes('.backup')) throw new Error('disk full');
      });

      const events = [];
      backupManager.on('backup-failed', e => events.push(e));

      await expect(backupManager.executeBackup('daily', {
        include: ['services'],
        destinations: [{ type: 'local' }],
        verify: false
      })).rejects.toThrow('Failed to save backup to any destination');

      expect(backupManager.history).toHaveLength(1);
      expect(backupManager.history[0].status).toBe('failed');
      expect(events).toHaveLength(1);

      backupManager.removeAllListeners();
    });

    it('runs cleanup after successful backup with retention', async () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify([{ id: 'plex' }]));

      // Pre-fill history with old backups
      for (let i = 0; i < 5; i++) {
        backupManager.history.push({
          id: `daily-old-${i}`, name: 'daily', status: 'success',
          timestamp: new Date(2026, 0, i + 1).toISOString(),
          locations: [{ type: 'local', path: `/backups/daily-old-${i}.backup` }]
        });
      }

      await backupManager.executeBackup('daily', {
        include: ['services'],
        destinations: [{ type: 'local' }],
        verify: false,
        retention: { keep: 2 }
      });

      // Old backups should be cleaned up (5 old + 1 new = 6 total, keep 2 → delete 4)
      expect(fs.unlinkSync).toHaveBeenCalled();
    });
  });

  describe('restoreBackup', () => {
    it('throws when backup not found in history', async () => {
      await expect(backupManager.restoreBackup('nonexistent'))
        .rejects.toThrow('Backup not found: nonexistent');
    });

    it('throws on unsupported backup version', async () => {
      // Create backup data with wrong version
      const wrongVersionData = { version: '2.0', data: {} };
      const compressed = await backupManager.compressBackup(wrongVersionData);

      backupManager.history.push({
        id: 'test-restore',
        status: 'success',
        encrypted: false,
        locations: [{ type: 'local', path: '/backups/test-restore.backup' }]
      });

      fs.readFileSync.mockReturnValue(compressed);

      await expect(backupManager.restoreBackup('test-restore'))
        .rejects.toThrow('Unsupported backup version: 2.0');
    });

    it('restores services and config from backup', async () => {
      const backupData = {
        version: '1.0',
        data: {
          services: [{ id: 'plex' }, { id: 'radarr' }],
          config: { tld: '.sami' }
        }
      };
      const compressed = await backupManager.compressBackup(backupData);

      backupManager.history.push({
        id: 'test-restore',
        status: 'success',
        encrypted: false,
        locations: [{ type: 'local', path: '/backups/test-restore.backup' }]
      });

      fs.readFileSync.mockReturnValue(compressed);

      const events = [];
      backupManager.on('restore-start', e => events.push({ type: 'start', ...e }));
      backupManager.on('restore-complete', e => events.push({ type: 'complete', ...e }));

      const result = await backupManager.restoreBackup('test-restore');

      expect(result.success).toBe(true);
      expect(result.restored.services).toBe(true);
      expect(result.restored.config).toBe(true);
      expect(fs.writeFileSync).toHaveBeenCalledWith(
        expect.stringContaining('services'),
        expect.stringContaining('plex')
      );
      expect(events).toHaveLength(2);

      backupManager.removeAllListeners();
    });

    it('restores credentials and stats from backup', async () => {
      const backupData = {
        version: '1.0',
        data: {
          credentials: { encrypted: 'cred-data' },
          stats: { stats: [{ cpu: 10 }] }
        }
      };
      const compressed = await backupManager.compressBackup(backupData);

      backupManager.history.push({
        id: 'full-restore',
        status: 'success',
        encrypted: false,
        locations: [{ type: 'local', path: '/backups/full-restore.backup' }]
      });

      fs.readFileSync.mockReturnValue(compressed);

      const result = await backupManager.restoreBackup('full-restore');

      expect(result.restored.credentials).toBe(true);
      expect(result.restored.stats).toBe(true);
      expect(credentialManager.importBackup).toHaveBeenCalledWith({ encrypted: 'cred-data' });
      expect(resourceMonitor.importStats).toHaveBeenCalledWith({ stats: [{ cpu: 10 }] });
    });

    it('restores encrypted backup', async () => {
      const key = crypto.randomBytes(32).toString('hex');
      const backupData = { version: '1.0', data: { services: [{ id: 'plex' }] } };
      const compressed = await backupManager.compressBackup(backupData);
      const encrypted = await backupManager.encryptBackup(compressed, key);

      backupManager.history.push({
        id: 'enc-restore',
        status: 'success',
        encrypted: true,
        locations: [{ type: 'local', path: '/backups/enc-restore.backup' }]
      });

      fs.readFileSync.mockReturnValue(encrypted);

      const result = await backupManager.restoreBackup('enc-restore', { encryptionKey: key });

      expect(result.success).toBe(true);
      expect(result.restored.services).toBe(true);
    });

    it('emits restore-failed on error', async () => {
      backupManager.history.push({
        id: 'fail-restore',
        status: 'success',
        encrypted: false,
        locations: [{ type: 'local', path: '/backups/fail-restore.backup' }]
      });

      fs.readFileSync.mockImplementation(() => { throw new Error('read error'); });

      const events = [];
      backupManager.on('restore-failed', e => events.push(e));

      await expect(backupManager.restoreBackup('fail-restore'))
        .rejects.toThrow();

      expect(events).toHaveLength(1);
      expect(events[0].error).toBeDefined();

      backupManager.removeAllListeners();
    });

    it('skips restore of specific sections when options disable them', async () => {
      const backupData = {
        version: '1.0',
        data: {
          services: [{ id: 'plex' }],
          config: { tld: '.sami' },
          credentials: { encrypted: 'data' },
          stats: { stats: [] }
        }
      };
      const compressed = await backupManager.compressBackup(backupData);

      backupManager.history.push({
        id: 'partial-restore',
        status: 'success',
        encrypted: false,
        locations: [{ type: 'local', path: '/backups/partial.backup' }]
      });

      fs.readFileSync.mockReturnValue(compressed);

      const result = await backupManager.restoreBackup('partial-restore', {
        restoreServices: false,
        restoreConfig: false,
        restoreCredentials: false,
        restoreStats: false
      });

      expect(result.success).toBe(true);
      expect(result.restored.services).toBeUndefined();
      expect(result.restored.config).toBeUndefined();
      expect(result.restored.credentials).toBeUndefined();
      expect(result.restored.stats).toBeUndefined();
    });
  });

  describe('start with configured backups', () => {
    it('schedules enabled backups on start', () => {
      backupManager.config = {
        backups: {
          daily: { enabled: true, schedule: 'daily' },
          disabled: { enabled: false, schedule: 'hourly' }
        },
        defaultRetention: { keep: 7 }
      };

      backupManager.start();

      expect(backupManager.scheduledJobs.has('daily')).toBe(true);
      expect(backupManager.scheduledJobs.has('disabled')).toBe(false);
    });
  });

  describe('persistence error handling', () => {
    it('saveConfig handles write error gracefully', () => {
      fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
      expect(() => backupManager.saveConfig()).not.toThrow();
    });

    it('saveHistory handles write error gracefully', () => {
      fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
      expect(() => backupManager.saveHistory()).not.toThrow();
    });

    it('backupConfig returns null on read error', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation(() => { throw new Error('corrupt'); });
      expect(backupManager.backupConfig()).toBeNull();
    });
  });

  describe('verifyBackup edge cases', () => {
    it('returns true for non-local backup type', async () => {
      const result = await backupManager.verifyBackup({ type: 'remote', path: 'na' }, 'checksum');
      expect(result).toBe(true);
    });
  });

  describe('DashCaddy scenarios', () => {
    it('full backup pipeline: services + config → compress → verify', async () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation((filePath) => {
        if (typeof filePath === 'string') {
          if (filePath.includes('services')) return JSON.stringify([{ id: 'plex' }, { id: 'radarr' }]);
          if (filePath.includes('config')) return JSON.stringify({ tld: '.sami', mode: 'homelab' });
        }
        return '{}';
      });

      const data = await backupManager.createBackupData(['services', 'config']);
      expect(data.version).toBe('1.0');
      expect(data.data.services).toEqual([{ id: 'plex' }, { id: 'radarr' }]);
      expect(data.data.config).toEqual({ tld: '.sami', mode: 'homelab' });

      // Compress and verify round-trip
      const compressed = await backupManager.compressBackup(data);
      const decompressed = await backupManager.decompressBackup(compressed);
      expect(decompressed.data.services).toEqual(data.data.services);
    });

    it('encrypted backup round-trip with real AES-256-GCM', async () => {
      const key = crypto.randomBytes(32).toString('hex');
      const payload = { version: '1.0', data: { services: [{ id: 'jellyfin' }] } };

      const compressed = await backupManager.compressBackup(payload);
      const encrypted = await backupManager.encryptBackup(compressed, key);
      const decrypted = await backupManager.decryptBackup(encrypted, key);
      const restored = await backupManager.decompressBackup(decrypted);

      expect(restored.data.services[0].id).toBe('jellyfin');
    });
  });
});
