/**
 * Unit tests for clock synchronization and skew handling
 */

import { ClockSyncManager, createDefaultClockSyncManager } from '../clock-sync';

// Mock fetch for testing
global.fetch = jest.fn();

describe('Clock Sync Manager', () => {
  let clockSync: ClockSyncManager;
  
  beforeEach(() => {
    clockSync = createDefaultClockSyncManager();
    jest.clearAllMocks();
  });
  
  describe('Server Time Sync', () => {
    it('should sync with server time successfully', async () => {
      const mockServerTime = 1704067200000; // 2024-01-01 00:00:00
      const mockClientTime = 1704067195000; // 2024-01-01 00:00:00 - 5s
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      // Mock Date.now to return consistent client time
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
      
      await clockSync.syncWithServer('https://api.example.com');
      
      expect(clockSync.getServerOffset()).toBe(5000); // 5 second offset
      expect(clockSync.getLastSyncTime()).toBe(mockClientTime);
    });
    
    it('should handle server sync failure gracefully', async () => {
      (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
      
      const initialOffset = clockSync.getServerOffset();
      
      await clockSync.syncWithServer('https://api.example.com');
      
      // Offset should remain unchanged on failure
      expect(clockSync.getServerOffset()).toBe(initialOffset);
    });
    
    it('should handle invalid server time response', async () => {
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue('0')
        }
      });
      
      const initialOffset = clockSync.getServerOffset();
      
      await clockSync.syncWithServer('https://api.example.com');
      
      // Offset should remain unchanged on invalid response
      expect(clockSync.getServerOffset()).toBe(initialOffset);
    });
    
    it('should handle HTTP error responses', async () => {
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500
      });
      
      const initialOffset = clockSync.getServerOffset();
      
      await clockSync.syncWithServer('https://api.example.com');
      
      // Offset should remain unchanged on HTTP error
      expect(clockSync.getServerOffset()).toBe(initialOffset);
    });
  });
  
  describe('Clock Skew Detection', () => {
    it('should detect excessive clock skew', async () => {
      const mockServerTime = 1704067200000; // 2024-01-01 00:00:00
      const mockClientTime = 1704067200000 - (35 * 1000); // 35 seconds behind
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
      
      await clockSync.syncWithServer('https://api.example.com');
      
      expect(clockSync.isClockSkewExcessive()).toBe(true);
    });
    
    it('should not detect excessive clock skew within tolerance', async () => {
      const mockServerTime = 1704067200000; // 2024-01-01 00:00:00
      const mockClientTime = 1704067200000 - (25 * 1000); // 25 seconds behind
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
      
      await clockSync.syncWithServer('https://api.example.com');
      
      expect(clockSync.isClockSkewExcessive()).toBe(false);
    });
  });
  
  describe('JWT Timestamp Validation', () => {
    beforeEach(() => {
      // Set up clock sync with known offset
      const mockServerTime = 1704067200000; // 2024-01-01 00:00:00
      const mockClientTime = 1704067195000; // 5 seconds behind
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
    });
    
    it('should validate JWT within acceptable time window', async () => {
      await clockSync.syncWithServer('https://api.example.com');
      
      const jwt = {
        iat: Math.floor((1704067200000 - 1000) / 1000), // 1 second ago
        exp: Math.floor((1704067200000 + 3600000) / 1000) // 1 hour from now
      };
      
      expect(clockSync.validateJwtTimestamp(jwt)).toBe(true);
    });
    
    it('should reject JWT with excessive clock skew', async () => {
      const mockServerTime = 1704067200000; // 2024-01-01 00:00:00
      const mockClientTime = 1704067200000; // Same as server time
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      // Mock Date.now to return consistent client time
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
      
      await clockSync.syncWithServer('https://api.example.com');
      
      const jwt = {
        iat: Math.floor((1704067200000 - 45000) / 1000), // 45 seconds ago (exceeds 30s tolerance)
        exp: Math.floor((1704067200000 + 3600000) / 1000) // 1 hour from now
      };
      
      expect(clockSync.validateJwtTimestamp(jwt)).toBe(false);
    });
    
    it('should reject expired JWT', async () => {
      await clockSync.syncWithServer('https://api.example.com');
      
      const jwt = {
        iat: Math.floor((1704067200000 - 3600000) / 1000), // 1 hour ago
        exp: Math.floor((1704067200000 - 35000) / 1000) // 35 seconds ago (expired, exceeds tolerance)
      };
      
      expect(clockSync.validateJwtTimestamp(jwt)).toBe(false);
    });
    
    it('should reject JWT that is too old', async () => {
      await clockSync.syncWithServer('https://api.example.com');
      
      const jwt = {
        iat: Math.floor((1704067200000 - 7200000) / 1000), // 2 hours ago (exceeds max age)
        exp: Math.floor((1704067200000 + 3600000) / 1000) // 1 hour from now
      };
      
      expect(clockSync.validateJwtTimestamp(jwt)).toBe(false);
    });
    
    it('should handle JWT with clock skew tolerance', async () => {
      await clockSync.syncWithServer('https://api.example.com');
      
      const jwt = {
        iat: Math.floor((1704067200000 - 25000) / 1000), // 25 seconds ago (within tolerance)
        exp: Math.floor((1704067200000 + 3600000) / 1000) // 1 hour from now
      };
      
      expect(clockSync.validateJwtTimestamp(jwt)).toBe(true);
    });
  });
  
  describe('Periodic Sync', () => {
    it('should start and stop periodic sync', () => {
      const syncSpy = jest.spyOn(clockSync, 'syncWithServer');
      
      clockSync.startPeriodicSync('https://api.example.com');
      expect(syncSpy).not.toHaveBeenCalled(); // Not called immediately
      
      clockSync.stopPeriodicSync();
      // Should not throw error
    });
    
    it('should detect when sync is needed', () => {
      // Initially needs sync (no previous sync)
      expect(clockSync.needsSync()).toBe(true);
      
      // Mock a recent sync
      jest.spyOn(Date, 'now').mockReturnValue(1000);
      clockSync.syncWithServer('https://api.example.com');
      
      // Should not need sync immediately after
      expect(clockSync.needsSync()).toBe(false);
    });
  });
  
  describe('Time Calculations', () => {
    it('should calculate server time correctly', async () => {
      const mockServerTime = 1704067200000;
      const mockClientTime = 1704067195000;
      
      (fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        headers: {
          get: jest.fn().mockReturnValue(mockServerTime.toString())
        }
      });
      
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime);
      
      await clockSync.syncWithServer('https://api.example.com');
      
      // Mock current time to be 1 second later
      jest.spyOn(Date, 'now').mockReturnValue(mockClientTime + 1000);
      
      const serverTime = clockSync.getServerTime();
      expect(serverTime).toBe(mockServerTime + 1000);
    });
    
    it('should return client time correctly', () => {
      const mockTime = 1704067200000;
      jest.spyOn(Date, 'now').mockReturnValue(mockTime);
      
      expect(clockSync.getClientTime()).toBe(mockTime);
    });
  });
  
  describe('Configuration', () => {
    it('should use default configuration', () => {
      const config = clockSync.getConfig();
      
      expect(config.maxClockSkewSeconds).toBe(30);
      expect(config.skewCheckIntervalMs).toBe(300000);
      expect(config.jwtClockSkewTolerance).toBe(30);
      expect(config.jwtMaxAge).toBe(3600000);
    });
    
    it('should use custom configuration', () => {
      const customClockSync = new ClockSyncManager({
        maxClockSkewSeconds: 60,
        skewCheckIntervalMs: 600000
      });
      
      const config = customClockSync.getConfig();
      
      expect(config.maxClockSkewSeconds).toBe(60);
      expect(config.skewCheckIntervalMs).toBe(600000);
    });
  });
});
