import { ExchangeRateService, isRateLimitError } from '../../src/libs/exchange-rate/service';

describe('isRateLimitError', () => {
  it('should return true for error message containing "rate limit"', () => {
    const error = new Error('CoinGecko rate limit exceeded');
    expect(isRateLimitError(error)).toBe(true);
  });

  it('should return true for error message containing "Rate Limit" (case insensitive)', () => {
    const error = new Error('Rate Limit Exceeded');
    expect(isRateLimitError(error)).toBe(true);
  });

  it('should return true for error message containing "429"', () => {
    const error = new Error('Request failed with status code 429');
    expect(isRateLimitError(error)).toBe(true);
  });

  it('should return false for other error messages', () => {
    const error = new Error('Network timeout');
    expect(isRateLimitError(error)).toBe(false);
  });

  it('should return false for empty error message', () => {
    const error = new Error('');
    expect(isRateLimitError(error)).toBe(false);
  });

  it('should return false for error without message', () => {
    const error = new Error();
    expect(isRateLimitError(error)).toBe(false);
  });
});

describe('ExchangeRateService', () => {
  let service: ExchangeRateService;

  beforeEach(() => {
    service = new ExchangeRateService();
  });

  describe('CoinGecko Rate Limit State', () => {
    it('should initialize with null rateLimitedUntil', () => {
      expect((service as any).coingeckoRateLimitedUntil).toBeNull();
    });

    it('should initialize with zero retryCount', () => {
      expect((service as any).coingeckoRetryCount).toBe(0);
    });

    it('should have correct RATE_LIMIT_INITIAL_DELAY_MS constant (10 minutes)', () => {
      expect((service as any).RATE_LIMIT_INITIAL_DELAY_MS).toBe(10 * 60 * 1000);
    });

    it('should have correct RATE_LIMIT_MAX_RETRIES constant (5)', () => {
      expect((service as any).RATE_LIMIT_MAX_RETRIES).toBe(5);
    });

    it('should have correct RATE_LIMIT_BACKOFF_MULTIPLIER constant (2)', () => {
      expect((service as any).RATE_LIMIT_BACKOFF_MULTIPLIER).toBe(2);
    });
  });

  describe('shouldSkipCoinGecko', () => {
    it('should return false when not rate limited', () => {
      expect((service as any).shouldSkipCoinGecko()).toBe(false);
    });

    it('should return true when rate limited and time not expired', () => {
      const futureTime = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes in future
      (service as any).coingeckoRateLimitedUntil = futureTime;

      expect((service as any).shouldSkipCoinGecko()).toBe(true);
    });

    it('should return false and clear state when rate limit time expired', () => {
      const pastTime = new Date(Date.now() - 1000); // 1 second ago
      (service as any).coingeckoRateLimitedUntil = pastTime;

      expect((service as any).shouldSkipCoinGecko()).toBe(false);
      expect((service as any).coingeckoRateLimitedUntil).toBeNull();
    });

    it('should return true when max retries exceeded (provider disabled)', () => {
      (service as any).coingeckoRetryCount = 5; // Max retries reached
      (service as any).coingeckoRateLimitedUntil = null;

      expect((service as any).shouldSkipCoinGecko()).toBe(true);
    });
  });

  describe('handleCoinGeckoRateLimit', () => {
    it('should set rateLimitedUntil to 10 minutes from now on first rate limit', () => {
      const beforeTime = Date.now();
      (service as any).handleCoinGeckoRateLimit();
      const afterTime = Date.now();

      expect((service as any).coingeckoRetryCount).toBe(1);
      expect((service as any).coingeckoRateLimitedUntil).not.toBeNull();

      const limitedUntil = (service as any).coingeckoRateLimitedUntil.getTime();
      const expectedDelay = 10 * 60 * 1000; // 10 minutes
      expect(limitedUntil).toBeGreaterThanOrEqual(beforeTime + expectedDelay);
      expect(limitedUntil).toBeLessThanOrEqual(afterTime + expectedDelay);
    });

    it('should use exponential backoff for subsequent rate limits', () => {
      // First rate limit: 10 min
      (service as any).handleCoinGeckoRateLimit();
      expect((service as any).coingeckoRetryCount).toBe(1);

      // Second rate limit: 20 min
      const beforeSecond = Date.now();
      (service as any).handleCoinGeckoRateLimit();
      expect((service as any).coingeckoRetryCount).toBe(2);
      const secondDelay = (service as any).coingeckoRateLimitedUntil.getTime() - beforeSecond;
      expect(secondDelay).toBeGreaterThanOrEqual(20 * 60 * 1000 - 100); // Allow 100ms tolerance

      // Third rate limit: 40 min
      const beforeThird = Date.now();
      (service as any).handleCoinGeckoRateLimit();
      expect((service as any).coingeckoRetryCount).toBe(3);
      const thirdDelay = (service as any).coingeckoRateLimitedUntil.getTime() - beforeThird;
      expect(thirdDelay).toBeGreaterThanOrEqual(40 * 60 * 1000 - 100);
    });

    it('should disable provider after max retries', () => {
      // Exhaust all retries
      for (let i = 0; i < 5; i++) {
        (service as any).handleCoinGeckoRateLimit();
      }

      expect((service as any).coingeckoRetryCount).toBe(5);
      // Provider should now be disabled (shouldSkipCoinGecko returns true)
      expect((service as any).shouldSkipCoinGecko()).toBe(true);
    });

    it('should not increment retry count beyond max', () => {
      // Set to max
      (service as any).coingeckoRetryCount = 5;

      // Try to handle another rate limit
      (service as any).handleCoinGeckoRateLimit();

      // Should not exceed max
      expect((service as any).coingeckoRetryCount).toBe(5);
    });
  });

  describe('resetCoinGeckoRateLimitState', () => {
    it('should reset all rate limit state on success', () => {
      // Set up rate limited state
      (service as any).coingeckoRetryCount = 3;
      (service as any).coingeckoRateLimitedUntil = new Date(Date.now() + 60000);

      // Reset state
      (service as any).resetCoinGeckoRateLimitState();

      expect((service as any).coingeckoRetryCount).toBe(0);
      expect((service as any).coingeckoRateLimitedUntil).toBeNull();
    });
  });

  describe('isCoinGeckoProvider', () => {
    it('should return true for coingecko type provider', () => {
      const provider = { type: 'coingecko', name: 'CoinGecko' };
      expect((service as any).isCoinGeckoProvider(provider)).toBe(true);
    });

    it('should return false for token-data type provider', () => {
      const provider = { type: 'token-data', name: 'TokenData' };
      expect((service as any).isCoinGeckoProvider(provider)).toBe(false);
    });

    it('should return false for coinmarketcap type provider', () => {
      const provider = { type: 'coinmarketcap', name: 'CoinMarketCap' };
      expect((service as any).isCoinGeckoProvider(provider)).toBe(false);
    });

    it('should return false for provider without type', () => {
      const provider = { name: 'Unknown' };
      expect((service as any).isCoinGeckoProvider(provider)).toBe(false);
    });
  });

  describe('validateRate', () => {
    it('should validate positive finite rate', () => {
      const validation = (service as any).validateRate('ABT', '0.15', Date.now());

      expect(validation.degraded).toBe(false);
    });

    it('should throw error for negative rate', () => {
      expect(() => {
        (service as any).validateRate('ABT', '-0.15', Date.now());
      }).toThrow('Invalid rate value');
    });

    it('should throw error for zero rate', () => {
      expect(() => {
        (service as any).validateRate('ABT', '0', Date.now());
      }).toThrow('Invalid rate value');
    });

    it('should mark as degraded for old timestamp', () => {
      const oldTimestamp = Date.now() - 6 * 60 * 1000; // 6 minutes ago
      const validation = (service as any).validateRate('ABT', '0.15', oldTimestamp);

      expect(validation.degraded).toBe(true);
      expect(validation.degraded_reason).toContain('old');
    });

    it('should mark as degraded for large deviation', () => {
      const symbol = 'TEST';
      const now = Date.now();

      // Build history with consistent rate
      const history = ['1.0', '1.0', '1.0'];
      (service as any).rateHistory.set(symbol, history);

      // Try rate with >5% deviation
      const validation = (service as any).validateRate(symbol, '1.10', now);

      expect(validation.degraded).toBe(true);
      expect(validation.degraded_reason).toContain('deviates');
    });

    it('should not mark as degraded for small deviation', () => {
      const symbol = 'TEST';
      const now = Date.now();

      // Build history with consistent rate
      const history = ['1.0', '1.0', '1.0'];
      (service as any).rateHistory.set(symbol, history);

      // Try rate with <5% deviation
      const validation = (service as any).validateRate(symbol, '1.03', now);

      expect(validation.degraded).toBe(false);
    });

    it('should update rate history', () => {
      const symbol = 'NEW';
      const now = Date.now();

      (service as any).validateRate(symbol, '0.15', now);

      const history = (service as any).rateHistory.get(symbol);
      expect(history).toBeDefined();
      expect(history.length).toBe(1);
      expect(history[0]).toBe('0.15');
    });

    it('should limit history size to 10 entries', () => {
      const symbol = 'LIMIT';
      const now = Date.now();

      // Add 12 rates
      for (let i = 0; i < 12; i++) {
        (service as any).validateRate(symbol, '0.15', now);
      }

      const history = (service as any).rateHistory.get(symbol);
      expect(history.length).toBe(10);
    });
  });

  describe('clearCache', () => {
    it('should clear specific symbol cache', () => {
      (service as any).cache.set('ABT', {
        data: { rate: '0.15' },
        timestamp: Date.now(),
      });
      (service as any).cache.set('ETH', {
        data: { rate: '2000' },
        timestamp: Date.now(),
      });

      service.clearCache('ABT');

      expect((service as any).cache.has('ABT')).toBe(false);
      expect((service as any).cache.has('ETH')).toBe(true);
    });

    it('should clear all cache when no symbol provided', () => {
      (service as any).cache.set('ABT', {
        data: { rate: '0.15' },
        timestamp: Date.now(),
      });
      (service as any).cache.set('ETH', {
        data: { rate: '2000' },
        timestamp: Date.now(),
      });

      service.clearCache();

      expect((service as any).cache.size).toBe(0);
    });
  });

  describe('Cache behavior', () => {
    it('should cache rate for 30 seconds', () => {
      const cacheKey = 'ABT';
      const cacheEntry = {
        data: {
          rate: '0.15',
          provider_id: 'test',
          provider_name: 'test',
          timestamp_ms: Date.now(),
          degraded: false,
        },
        timestamp: Date.now() - 20000, // 20 seconds ago
      };

      (service as any).cache.set(cacheKey, cacheEntry);

      // Cache should still be valid
      const cached = (service as any).cache.get(cacheKey);
      expect(cached).toBeDefined();
    });

    it('should not use expired cache', () => {
      const cacheKey = 'ABT';
      const cacheEntry = {
        data: {
          rate: '0.15',
          provider_id: 'test',
          provider_name: 'test',
          timestamp_ms: Date.now(),
          degraded: false,
        },
        timestamp: Date.now() - 35000, // 35 seconds ago (expired)
      };

      (service as any).cache.set(cacheKey, cacheEntry);

      // Cache exists but is expired, so getRate should try to fetch new data
      // This would require mocking the database, which we test in integration tests
    });
  });
});
