const { describe, it, expect, jest, beforeEach, afterEach } = require('@jest/globals');
const mockFs = require('mock-fs');
const path = require('path');
const crypto = require('crypto');
const { EventEmitter } = require('events');

// Mock dependencies before requiring the module
jest.mock('../../src/database/db');
jest.mock('../../src/utils/logger');
jest.mock('../../src/services/emailProcessor');
jest.mock('node-cron');
jest.mock('../../src/services/backupManifest');
jest.mock('../../src/services/storage/s3Storage');

const backupService = require('../../src/services/backupService');
const { db } = require('../../src/database/db');
const logger = require('../../src/utils/logger');
const { queueEmail } = require('../../src/services/emailProcessor');
const cron = require('node-cron');
const backupManifest = require('../../src/services/backupManifest');
const S3StorageAdapter = require('../../src/services/storage/s3Storage');

describe('Enhanced Backup Service Tests', () => {
  let mockDb;
  let mockS3Client;
  let mockCronJob;

  beforeEach(() => {
    // Reset all mocks
    jest.clearAllMocks();
    
    // Mock database
    mockDb = {
      select: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      first: jest.fn(),
      insert: jest.fn(),
      update: jest.fn(),
      delete: jest.fn()
    };
    db.mockReturnValue(mockDb);
    
    // Mock cron job
    mockCronJob = {
      stop: jest.fn()
    };
    cron.schedule.mockReturnValue(mockCronJob);
    
    // Mock S3 client
    mockS3Client = {
      testConnection: jest.fn().mockResolvedValue(true),
      upload: jest.fn().mockResolvedValue({ Location: 's3://bucket/key' }),
      uploadStream: jest.fn().mockResolvedValue({ Location: 's3://bucket/key' }),
      download: jest.fn().mockResolvedValue(),
      exists: jest.fn().mockResolvedValue(false),
      delete: jest.fn().mockResolvedValue(),
      list: jest.fn().mockResolvedValue({ Contents: [] })
    };
    S3StorageAdapter.mockImplementation(() => mockS3Client);
    
    // Mock backup manifest
    backupManifest.generateManifest = jest.fn().mockResolvedValue({
      backup: { id: 'test-backup-123' },
      version: '2.0'
    });
    backupManifest.saveManifest = jest.fn().mockResolvedValue('/path/to/manifest.json');
    backupManifest.loadManifest = jest.fn().mockResolvedValue({});
    backupManifest.validateManifest = jest.fn();
    backupManifest.generateSummaryReport = jest.fn().mockReturnValue('Summary report');
    
    // Mock logger
    logger.info = jest.fn();
    logger.error = jest.fn();
    logger.warn = jest.fn();
    logger.debug = jest.fn();
  });

  afterEach(() => {
    mockFs.restore();
  });

  describe('getBackupConfig', () => {
    it('should retrieve and parse backup configuration from database', async () => {
      const mockSettings = [
        { setting_key: 'backup_enabled', setting_value: 'true' },
        { setting_key: 'backup_destination_type', setting_value: '"s3"' },
        { setting_key: 'backup_s3_bucket', setting_value: '"test-bucket"' },
        { setting_key: 'backup_retention_days', setting_value: '30' }
      ];
      
      mockDb.select.mockResolvedValue(mockSettings);
      
      const config = await backupService.getBackupConfig();
      
      expect(config).toEqual({
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_retention_days: 30
      });
      
      expect(db).toHaveBeenCalledWith('app_settings');
      expect(mockDb.where).toHaveBeenCalledWith('setting_type', 'backup');
    });

    it('should handle JSON parse errors gracefully', async () => {
      const mockSettings = [
        { setting_key: 'backup_enabled', setting_value: 'invalid-json' }
      ];
      
      mockDb.select.mockResolvedValue(mockSettings);
      
      const config = await backupService.getBackupConfig();
      
      expect(config).toEqual({
        backup_enabled: 'invalid-json'
      });
    });

    it('should return null on database error', async () => {
      mockDb.select.mockRejectedValue(new Error('Database error'));
      
      const config = await backupService.getBackupConfig();
      
      expect(config).toBeNull();
      expect(logger.error).toHaveBeenCalled();
    });
  });

  describe('S3 Backup Functionality', () => {
    beforeEach(() => {
      // Mock file system
      mockFs({
        '/storage/events/active/event1': {
          'photo1.jpg': Buffer.from('photo1 content'),
          'photo2.jpg': Buffer.from('photo2 content')
        },
        '/storage/events/archived/event2.zip': Buffer.from('archived content'),
        '/storage/thumbnails': {
          'thumb1.jpg': Buffer.from('thumb1 content')
        },
        '/storage/uploads': {
          'logo.png': Buffer.from('logo content')
        }
      });
      
      process.env.STORAGE_PATH = '/storage';
    });

    it('should perform S3 backup with correct configuration', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_region: 'us-east-1',
        backup_s3_endpoint: 'https://s3.amazonaws.com',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret',
        backup_include_archived: true,
        backup_max_file_size_mb: 100
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.where.mockReturnThis();
      mockDb.first.mockResolvedValue(null);
      mockDb.insert.mockResolvedValue([1]);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      jest.spyOn(backupService, 'getDatabaseBackupInfo').mockResolvedValue({
        type: 'sqlite',
        backupFile: null,
        hasChanged: true
      });
      
      await backupService.runBackup();
      
      expect(S3StorageAdapter).toHaveBeenCalledWith({
        bucket: 'test-bucket',
        region: 'us-east-1',
        endpoint: 'https://s3.amazonaws.com',
        accessKeyId: 'test-key',
        secretAccessKey: 'test-secret',
        forcePathStyle: false,
        sslEnabled: true,
        maxRetries: 3,
        retryDelay: 1000
      });
      
      expect(mockS3Client.testConnection).toHaveBeenCalled();
      expect(mockS3Client.upload).toHaveBeenCalled();
    });

    it('should handle S3 upload failures gracefully', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      mockS3Client.testConnection.mockRejectedValue(new Error('Connection failed'));
      
      await backupService.runBackup();
      
      expect(logger.error).toHaveBeenCalledWith('S3 backup failed:', expect.any(Error));
      expect(mockDb.update).toHaveBeenCalledWith(expect.objectContaining({
        status: 'failed',
        error_message: expect.stringContaining('Connection failed')
      }));
    });

    it('should skip unchanged files in incremental backup', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret',
        backup_incremental: true
      };
      
      // Mock existing file state
      mockDb.first.mockImplementation((query) => {
        if (query === undefined) {
          return Promise.resolve({
            file_path: 'events/active/event1/photo1.jpg',
            checksum: crypto.createHash('sha256').update('photo1 content').digest('hex')
          });
        }
        return Promise.resolve(null);
      });
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      await backupService.runBackup();
      
      // Should skip unchanged file
      const uploadCalls = mockS3Client.upload.mock.calls;
      const photo1Uploaded = uploadCalls.some(call => 
        call[1].includes('photo1.jpg')
      );
      expect(photo1Uploaded).toBe(false);
    });

    it('should include database backup when configured', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret',
        backup_include_database: true
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      jest.spyOn(backupService, 'getDatabaseBackupInfo').mockResolvedValue({
        type: 'sqlite',
        backupFile: '/backup/db-backup.sql',
        size: 1024000,
        checksum: 'abc123',
        hasChanged: false
      });
      
      // Mock database backup file
      mockFs({
        '/storage/events/active': {},
        '/backup/db-backup.sql': Buffer.from('database backup content')
      });
      
      await backupService.runBackup();
      
      // Verify database backup was uploaded
      const uploadCalls = mockS3Client.upload.mock.calls;
      const dbBackupUploaded = uploadCalls.some(call => 
        call[1].includes('database/db-backup.sql')
      );
      expect(dbBackupUploaded).toBe(true);
    });

    it('should validate required S3 configuration', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket'
        // Missing access key and secret key
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      await backupService.runBackup();
      
      expect(logger.error).toHaveBeenCalledWith(
        'S3 backup failed:',
        expect.objectContaining({
          message: expect.stringContaining('S3 backup configuration incomplete')
        })
      );
    });
  });

  describe('Manifest Generation', () => {
    it('should generate and save manifest for successful backup', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 'local',
        backup_destination_path: '/backup',
        backup_manifest_format: 'json'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      mockFs({
        '/storage/events/active/event1': {
          'photo1.jpg': Buffer.from('photo1 content')
        },
        '/backup': {}
      });
      
      await backupService.runBackup();
      
      expect(backupManifest.generateManifest).toHaveBeenCalledWith(
        expect.objectContaining({
          backupType: 'full',
          backupPath: '/backup',
          format: 'json'
        })
      );
      
      expect(backupManifest.saveManifest).toHaveBeenCalled();
    });

    it('should generate incremental manifest when parent exists', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 'local',
        backup_destination_path: '/backup'
      };
      
      const lastBackup = {
        id: 1,
        manifest_path: '/backup/manifests/previous.json',
        manifest_id: 'previous-backup-123'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([2]);
      mockDb.first.mockImplementation(() => Promise.resolve(lastBackup));
      mockDb.orderBy.mockReturnThis();
      mockDb.where.mockReturnThis();
      mockDb.whereNot = jest.fn().mockReturnThis();
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      mockFs({
        '/storage/events/active': {},
        '/backup': {}
      });
      
      await backupService.runBackup();
      
      expect(backupManifest.loadManifest).toHaveBeenCalledWith('/backup/manifests/previous.json');
      expect(backupManifest.generateIncrementalManifest).toHaveBeenCalled();
    });

    it('should upload manifest to S3 for S3 backups', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret',
        backup_manifest_format: 'yaml'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      const manifest = {
        backup: { id: 'backup-123' },
        version: '2.0'
      };
      backupManifest.generateManifest.mockResolvedValue(manifest);
      
      mockFs({
        '/storage/events/active': {},
        '/storage/temp': {}
      });
      
      await backupService.runBackup();
      
      // Verify manifest was uploaded to S3
      const uploadCalls = mockS3Client.upload.mock.calls;
      const manifestUploaded = uploadCalls.some(call => 
        call[1].includes('manifests/backup-manifest-backup-123.yaml')
      );
      expect(manifestUploaded).toBe(true);
    });
  });

  describe('Backward Compatibility', () => {
    it('should support local backup destination', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 'local',
        backup_destination_path: '/backup/local'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      mockFs({
        '/storage/events/active/event1': {
          'photo1.jpg': Buffer.from('photo1 content')
        },
        '/backup/local': {}
      });
      
      await backupService.runBackup();
      
      // Verify files were copied to local destination
      const fs = require('fs');
      const destPath = '/backup/local/events/active/event1/photo1.jpg';
      expect(fs.existsSync(destPath)).toBe(true);
    });

    it('should support rsync backup destination', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 'rsync',
        backup_rsync_host: 'backup.example.com',
        backup_rsync_user: 'backup',
        backup_rsync_path: '/remote/backup'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      // Mock exec for rsync
      const { exec } = require('child_process');
      const mockExec = jest.fn((cmd, callback) => {
        callback(null, { stdout: 'Number of files transferred: 1\nTotal file size: 1024 bytes' });
      });
      exec.mockImplementation(mockExec);
      
      mockFs({
        '/storage/events/active': {}
      });
      
      await backupService.runBackup();
      
      expect(mockExec).toHaveBeenCalledWith(
        expect.stringContaining('rsync'),
        expect.any(Function)
      );
    });
  });

  describe('Error Handling and Recovery', () => {
    it('should handle file read errors gracefully', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_s3_access_key: 'test-key',
        backup_s3_secret_key: 'test-secret'
      };
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.first.mockResolvedValue(null);
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      // Mock file that throws error on read
      const fs = require('fs');
      const originalCreateReadStream = fs.createReadStream;
      fs.createReadStream = jest.fn((path) => {
        if (path.includes('error.jpg')) {
          const stream = new EventEmitter();
          process.nextTick(() => stream.emit('error', new Error('File read error')));
          return stream;
        }
        return originalCreateReadStream(path);
      });
      
      mockFs({
        '/storage/events/active': {
          'error.jpg': Buffer.from('content'),
          'good.jpg': Buffer.from('content')
        }
      });
      
      await backupService.runBackup();
      
      // Should continue with other files despite error
      expect(logger.error).toHaveBeenCalledWith(
        expect.stringContaining('Failed to backup file'),
        expect.any(Error)
      );
      
      fs.createReadStream = originalCreateReadStream;
    });

    it('should send failure email on backup error', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 's3',
        backup_s3_bucket: 'test-bucket',
        backup_email_on_failure: true
      };
      
      const admins = [
        { email: 'admin1@example.com', is_active: true },
        { email: 'admin2@example.com', is_active: true }
      ];
      
      mockDb.select.mockResolvedValue([]);
      mockDb.insert.mockResolvedValue([1]);
      mockDb.where.mockReturnThis();
      
      jest.spyOn(backupService, 'getBackupConfig')
        .mockResolvedValueOnce(config)
        .mockResolvedValueOnce(config);
      
      // Force an error
      jest.spyOn(backupService, 'getFilesToBackup').mockRejectedValue(new Error('Storage error'));
      
      // Mock admin users query
      db.mockImplementation((table) => {
        if (table === 'admin_users') {
          return {
            where: jest.fn().mockResolvedValue(admins)
          };
        }
        return mockDb;
      });
      
      await backupService.runBackup();
      
      expect(queueEmail).toHaveBeenCalledTimes(2);
      expect(queueEmail).toHaveBeenCalledWith(
        null,
        'admin1@example.com',
        'backup_failed',
        expect.objectContaining({
          error_message: 'Storage error'
        })
      );
    });

    it('should handle concurrent backup attempts', async () => {
      const config = {
        backup_enabled: true,
        backup_destination_type: 'local',
        backup_destination_path: '/backup'
      };
      
      jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
      
      mockFs({
        '/storage/events/active': {},
        '/backup': {}
      });
      
      // Start two backups concurrently
      const backup1 = backupService.runBackup();
      const backup2 = backupService.runBackup();
      
      await Promise.all([backup1, backup2]);
      
      // Second backup should be skipped
      expect(logger.warn).toHaveBeenCalledWith('Backup already running, skipping');
    });
  });

  describe('Service Lifecycle', () => {
    it('should start backup service with cron schedule', async () => {
      const config = {
        backup_enabled: true,
        backup_schedule: '0 3 * * *' // 3 AM daily
      };
      
      mockDb.select.mockResolvedValue(
        Object.entries(config).map(([key, value]) => ({
          setting_key: key,
          setting_value: value.toString()
        }))
      );
      
      await backupService.startBackupService();
      
      expect(cron.schedule).toHaveBeenCalledWith('0 3 * * *', expect.any(Function));
      expect(logger.info).toHaveBeenCalledWith('Backup service started with schedule: 0 3 * * *');
    });

    it('should stop existing job when restarting service', async () => {
      const config = {
        backup_enabled: true,
        backup_schedule: '0 2 * * *'
      };
      
      mockDb.select.mockResolvedValue(
        Object.entries(config).map(([key, value]) => ({
          setting_key: key,
          setting_value: value.toString()
        }))
      );
      
      // Start service twice
      await backupService.startBackupService();
      await backupService.startBackupService();
      
      expect(mockCronJob.stop).toHaveBeenCalled();
    });

    it('should not start service when backup is disabled', async () => {
      const config = {
        backup_enabled: false
      };
      
      mockDb.select.mockResolvedValue([
        { setting_key: 'backup_enabled', setting_value: 'false' }
      ]);
      
      await backupService.startBackupService();
      
      expect(cron.schedule).not.toHaveBeenCalled();
      expect(logger.info).toHaveBeenCalledWith('Backup service is disabled');
    });
  });

  describe('Backup Status and History', () => {
    it('should return backup status with recent runs', async () => {
      const recentRuns = [
        {
          id: 1,
          started_at: new Date(),
          completed_at: new Date(),
          status: 'completed',
          files_backed_up: 100,
          total_size_bytes: 1024000,
          manifest_path: '/backup/manifest.json'
        }
      ];
      
      mockDb.limit.mockResolvedValue(recentRuns);
      
      backupManifest.validateManifest.mockImplementation(() => true);
      
      const status = await backupService.getBackupStatus();
      
      expect(status).toEqual({
        isRunning: false,
        isHealthy: true,
        lastRun: expect.objectContaining({
          ...recentRuns[0],
          manifestValid: true
        }),
        recentRuns: recentRuns,
        nextScheduledRun: expect.any(String)
      });
    });

    it('should clean up old backup runs', async () => {
      mockDb.delete.mockResolvedValue(5);
      
      await backupService.cleanupOldBackupRuns(30);
      
      expect(mockDb.where).toHaveBeenCalledWith('started_at', '<', expect.any(Date));
      expect(mockDb.delete).toHaveBeenCalled();
      expect(logger.info).toHaveBeenCalledWith('Cleaned up 5 old backup runs');
    });
  });

  describe('getBackupManifest', () => {
    it('should retrieve manifest from local filesystem', async () => {
      const backupRun = {
        id: 1,
        manifest_path: '/backup/manifests/backup-123.json'
      };
      
      mockDb.first.mockResolvedValue(backupRun);
      
      const manifest = { backup: { id: 'backup-123' } };
      backupManifest.loadManifest.mockResolvedValue(manifest);
      backupManifest.generateSummaryReport.mockReturnValue('Summary');
      
      const result = await backupService.getBackupManifest(1);
      
      expect(result).toEqual({
        manifest: manifest,
        summary: 'Summary'
      });
    });

    it('should retrieve manifest from S3', async () => {
      const backupRun = {
        id: 1,
        manifest_path: 's3://test-bucket/backups/manifests/backup-123.json'
      };
      
      mockDb.first.mockResolvedValue(backupRun);
      mockDb.select.mockResolvedValue([
        { setting_key: 'backup_s3_access_key', setting_value: '"test-key"' },
        { setting_key: 'backup_s3_secret_key', setting_value: '"test-secret"' }
      ]);
      
      const manifest = { backup: { id: 'backup-123' } };
      backupManifest.loadManifest.mockResolvedValue(manifest);
      
      await backupService.getBackupManifest(1);
      
      expect(S3StorageAdapter).toHaveBeenCalled();
      expect(mockS3Client.download).toHaveBeenCalledWith(
        'backups/manifests/backup-123.json',
        expect.any(String)
      );
    });
  });
});