import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { RegionInfo, RegionSettings } from '@livekit/protocol';
import { RegionUrlProvider } from './RegionUrlProvider';
import { ConnectionError, ConnectionErrorReason } from './errors';

// Use fake timers for testing auto-refetch
vi.useFakeTimers();

// Test helpers
function createMockRegionSettings(regions: Array<{ region: string; url: string }>): RegionSettings {
  return {
    regions: regions.map((r) => ({
      region: r.region,
      url: r.url,
    })) as RegionInfo[],
  } as RegionSettings;
}

function createMockResponse(
  status: number,
  data?: any,
  headers?: Record<string, string>,
): Response {
  const defaultHeaders = new Headers(headers || {});
  return {
    ok: status >= 200 && status < 300,
    status,
    statusText: status === 401 ? 'Unauthorized' : status === 500 ? 'Internal Server Error' : 'OK',
    headers: defaultHeaders,
    json: vi.fn().mockResolvedValue(data),
  } as unknown as Response;
}

describe('RegionUrlProvider', () => {
  let fetchMock: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    // Reset the static cache before each test
    // @ts-ignore - accessing private static field for testing
    RegionUrlProvider.cache = new Map();
    // @ts-ignore - accessing private static field for testing
    if (RegionUrlProvider.settingsTimeout) {
      // @ts-ignore
      clearTimeout(RegionUrlProvider.settingsTimeout);
    }

    // Mock fetch
    fetchMock = vi.fn();
    vi.stubGlobal('fetch', fetchMock);
  });

  afterEach(() => {
    vi.clearAllTimers();
    vi.restoreAllMocks();
  });

  describe('constructor and basic methods', () => {
    it('constructs with valid URL and token', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'test-token');
      expect(provider).toBeDefined();
      expect(provider.getServerUrl().toString()).toBe('wss://test.livekit.cloud/');
    });

    it('updates token correctly', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'initial-token');
      provider.updateToken('new-token');
      // Token is private, but we can verify it's used in subsequent calls
      expect(provider).toBeDefined();
    });

    it('returns server URL', () => {
      const url = 'wss://example.livekit.cloud';
      const provider = new RegionUrlProvider(url, 'token');
      expect(provider.getServerUrl().toString()).toBe('wss://example.livekit.cloud/');
    });

    it('resets attempted regions', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      // Get first region
      const region1 = await provider.getNextBestRegionUrl();
      expect(region1).toBe('wss://us-west.livekit.cloud');

      // Reset and verify we can get the first region again
      provider.resetAttempts();
      const region2 = await provider.getNextBestRegionUrl();
      expect(region2).toBe('wss://us-west.livekit.cloud');
    });
  });

  describe('isCloud', () => {
    it('returns true for .livekit.cloud domains', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      expect(provider.isCloud()).toBe(true);
    });

    it('returns true for .livekit.run domains', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.run', 'token');
      expect(provider.isCloud()).toBe(true);
    });

    it('returns false for non-cloud domains', () => {
      const provider = new RegionUrlProvider('wss://self-hosted.example.com', 'token');
      expect(provider.isCloud()).toBe(false);
    });

    it('returns false for localhost', () => {
      const provider = new RegionUrlProvider('ws://localhost:7880', 'token');
      expect(provider.isCloud()).toBe(false);
    });
  });

  describe('fetchRegionSettings', () => {
    it('fetches successfully with valid token and response', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const result = await provider.fetchRegionSettings();

      expect(fetchMock).toHaveBeenCalledWith('https://test.livekit.cloud/settings/regions', {
        headers: { authorization: 'Bearer token' },
        signal: undefined,
      });
      expect(result.regionSettings).toEqual(mockSettings);
      expect(result.maxAgeInMs).toBe(3600000);
    });

    it('converts wss to https for settings URL', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(200, createMockRegionSettings([])));

      await provider.fetchRegionSettings();

      expect(fetchMock).toHaveBeenCalledWith(
        expect.stringContaining('https://test.livekit.cloud'),
        expect.anything(),
      );
    });

    it('converts ws to http for settings URL', async () => {
      const provider = new RegionUrlProvider('ws://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(200, createMockRegionSettings([])));

      await provider.fetchRegionSettings();

      expect(fetchMock).toHaveBeenCalledWith(
        expect.stringContaining('http://test.livekit.cloud'),
        expect.anything(),
      );
    });

    it('respects abort signal', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const abortController = new AbortController();
      fetchMock.mockResolvedValue(createMockResponse(200, createMockRegionSettings([])));

      await provider.fetchRegionSettings(abortController.signal);

      expect(fetchMock).toHaveBeenCalledWith(expect.anything(), {
        headers: { authorization: 'Bearer token' },
        signal: abortController.signal,
      });
    });

    it('throws ConnectionError with NotAllowed for 401 response', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(401));

      const error = await provider.fetchRegionSettings().catch((e) => e);
      expect(error).toBeInstanceOf(ConnectionError);
      expect(error).toMatchObject({
        reason: ConnectionErrorReason.NotAllowed,
        status: 401,
      });
    });

    it('throws ConnectionError with InternalError for 500 response', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(500));

      const error = await provider.fetchRegionSettings().catch((e) => e);
      expect(error).toBeInstanceOf(ConnectionError);
      expect(error.reason).toBe(ConnectionErrorReason.InternalError);
    });

    it('extracts max-age from Cache-Control header', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(
        createMockResponse(200, createMockRegionSettings([]), {
          'Cache-Control': 'max-age=7200',
        }),
      );

      const result = await provider.fetchRegionSettings();
      expect(result.maxAgeInMs).toBe(7200000);
    });

    it('uses default max-age when Cache-Control is missing', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(200, createMockRegionSettings([])));

      const result = await provider.fetchRegionSettings();
      expect(result.maxAgeInMs).toBe(5000); // DEFAULT_MAX_AGE_MS
    });

    it('uses default max-age when max-age is not in Cache-Control', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(
        createMockResponse(200, createMockRegionSettings([]), {
          'Cache-Control': 'public, no-cache',
        }),
      );

      const result = await provider.fetchRegionSettings();
      expect(result.maxAgeInMs).toBe(5000);
    });

    it('sets updatedAtInMs to current time', async () => {
      const now = Date.now();
      vi.setSystemTime(now);

      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      fetchMock.mockResolvedValue(createMockResponse(200, createMockRegionSettings([])));

      const result = await provider.fetchRegionSettings();
      expect(result.updatedAtInMs).toBe(now);
    });
  });

  describe('getNextBestRegionUrl', () => {
    it('throws error for non-cloud domains', async () => {
      const provider = new RegionUrlProvider('wss://self-hosted.example.com', 'token');

      await expect(provider.getNextBestRegionUrl()).rejects.toThrow(
        'region availability is only supported for LiveKit Cloud domains',
      );
    });

    it('returns first region on initial call', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const region = await provider.getNextBestRegionUrl();
      expect(region).toBe('wss://us-west.livekit.cloud');
    });

    it('returns subsequent regions on repeated calls', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
        { region: 'eu-central', url: 'wss://eu-central.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const region1 = await provider.getNextBestRegionUrl();
      const region2 = await provider.getNextBestRegionUrl();
      const region3 = await provider.getNextBestRegionUrl();

      expect(region1).toBe('wss://us-west.livekit.cloud');
      expect(region2).toBe('wss://us-east.livekit.cloud');
      expect(region3).toBe('wss://eu-central.livekit.cloud');
    });

    it('returns null when all regions exhausted', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      await provider.getNextBestRegionUrl();
      const region = await provider.getNextBestRegionUrl();

      expect(region).toBeNull();
    });

    it('uses cached settings when available and fresh', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      // First call should fetch
      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Create new provider with same host
      const provider2 = new RegionUrlProvider('wss://test.livekit.cloud', 'token');

      // Second call should use cache
      await provider2.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1); // Still 1, no new fetch
    });

    it('fetches new settings when cache is expired', async () => {
      const now = Date.now();
      vi.setSystemTime(now);

      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=1' }), // 1 second TTL
      );

      // First call
      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Advance time beyond TTL
      vi.setSystemTime(now + 2000);

      // Create new provider and call again
      const provider2 = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      await provider2.getNextBestRegionUrl();

      expect(fetchMock).toHaveBeenCalledTimes(2); // Should fetch again
    });

    it('fetches new settings when cache is empty', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalled();
    });

    it('filters out already attempted regions', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const region1 = await provider.getNextBestRegionUrl();
      const region2 = await provider.getNextBestRegionUrl();

      expect(region1).toBe('wss://us-west.livekit.cloud');
      expect(region2).toBe('wss://us-east.livekit.cloud');
      expect(region1).not.toBe(region2);
    });

    it('works correctly after resetAttempts', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      // Exhaust regions
      await provider.getNextBestRegionUrl();
      let region = await provider.getNextBestRegionUrl();
      expect(region).toBeNull();

      // Reset and try again
      provider.resetAttempts();
      region = await provider.getNextBestRegionUrl();
      expect(region).toBe('wss://us-west.livekit.cloud');
    });

    it('respects abort signal', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const abortController = new AbortController();
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(createMockResponse(200, mockSettings));

      await provider.getNextBestRegionUrl(abortController.signal);

      expect(fetchMock).toHaveBeenCalledWith(expect.anything(), {
        headers: expect.anything(),
        signal: abortController.signal,
      });
    });
  });

  describe('caching behavior', () => {
    it('cache is shared across multiple instances with same hostname', async () => {
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const provider1 = new RegionUrlProvider('wss://test.livekit.cloud', 'token1');
      await provider1.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Different token, same hostname - should use cache
      const provider2 = new RegionUrlProvider('wss://test.livekit.cloud', 'token2');
      await provider2.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1); // Still 1
    });

    it('cache is separate for different hostnames', async () => {
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const provider1 = new RegionUrlProvider('wss://test1.livekit.cloud', 'token');
      await provider1.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Different hostname - should fetch again
      const provider2 = new RegionUrlProvider('wss://test2.livekit.cloud', 'token');
      await provider2.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it('cache keys by hostname without port or protocol', async () => {
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const provider1 = new RegionUrlProvider('wss://test.livekit.cloud:443', 'token');
      await provider1.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Same hostname, different protocol/port - should use cache
      const provider2 = new RegionUrlProvider('https://test.livekit.cloud', 'token');
      await provider2.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });
  });

  describe('auto-refetch mechanism', () => {
    it('schedules auto-refetch with correct max-age duration', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=10' }),
      );

      await provider.getNextBestRegionUrl();

      // Verify timeout is set (we can't easily check the exact duration without accessing internals)
      expect(vi.getTimerCount()).toBeGreaterThan(0);
    });

    it('auto-refetch updates cache on success', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const initialSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);
      const updatedSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      fetchMock
        .mockResolvedValueOnce(
          createMockResponse(200, initialSettings, { 'Cache-Control': 'max-age=100' }),
        )
        .mockResolvedValue(
          createMockResponse(200, updatedSettings, { 'Cache-Control': 'max-age=100' }),
        );

      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Advance time to trigger auto-refetch
      await vi.runOnlyPendingTimersAsync();

      // Verify the refetch happened
      expect(fetchMock).toHaveBeenCalledTimes(2);

      // Verify cache was updated
      // @ts-ignore - accessing private cache for testing
      const cached = RegionUrlProvider.cache.get('test.livekit.cloud');
      expect(cached?.regionSettings).toEqual(updatedSettings);
    });

    it('auto-refetch handles errors gracefully', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock
        .mockResolvedValueOnce(
          createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
        )
        .mockRejectedValueOnce(new Error('Fetch failed'))
        .mockResolvedValue(
          createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
        );

      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Advance time to trigger auto-refetch (which will fail)
      await vi.runOnlyPendingTimersAsync();

      // Verify fetch was attempted and failed
      expect(fetchMock).toHaveBeenCalledTimes(2);

      // Advance time again to trigger retry after error
      await vi.runOnlyPendingTimersAsync();

      // Verify it retried and succeeded
      expect(fetchMock).toHaveBeenCalledTimes(3);
    });

    it('clears previous timeout when updating cache', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=10' }),
      );

      await provider.getNextBestRegionUrl();
      const firstTimerCount = vi.getTimerCount();

      // Update cache again (should clear previous timeout)
      provider.setServerReportedRegions({
        regionSettings: mockSettings,
        updatedAtInMs: Date.now(),
        maxAgeInMs: 5000,
      });
      const secondTimerCount = vi.getTimerCount();

      // Should still have timers but not accumulating
      expect(secondTimerCount).toBeGreaterThan(0);
      expect(secondTimerCount).toBeLessThanOrEqual(firstTimerCount + 1);
    });
  });

  describe('setServerReportedRegions', () => {
    it('stores region settings in cache', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      provider.setServerReportedRegions({
        regionSettings: mockSettings,
        updatedAtInMs: Date.now(),
        maxAgeInMs: 3600000,
      });

      // Should use cached settings without fetching
      const region = await provider.getNextBestRegionUrl();
      expect(region).toBe('wss://us-west.livekit.cloud');
      expect(fetchMock).not.toHaveBeenCalled();
    });

    it('stores settings with correct fields', () => {
      const now = Date.now();
      vi.setSystemTime(now);

      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      provider.setServerReportedRegions({
        regionSettings: mockSettings,
        updatedAtInMs: now,
        maxAgeInMs: 7200000,
      });

      // @ts-ignore - accessing private cache for testing
      const cached = RegionUrlProvider.cache.get('test.livekit.cloud');
      expect(cached?.regionSettings).toEqual(mockSettings);
      expect(cached?.updatedAtInMs).toBe(now);
      expect(cached?.maxAgeInMs).toBe(7200000);
    });

    it('updates existing cache entry', () => {
      const now = Date.now();
      vi.setSystemTime(now);

      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const initialSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);
      const updatedSettings = createMockRegionSettings([
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      provider.setServerReportedRegions({
        regionSettings: initialSettings,
        updatedAtInMs: now,
        maxAgeInMs: 5000,
      });

      provider.setServerReportedRegions({
        regionSettings: updatedSettings,
        updatedAtInMs: now + 1000,
        maxAgeInMs: 10000,
      });

      // @ts-ignore - accessing private cache for testing
      const cached = RegionUrlProvider.cache.get('test.livekit.cloud');
      expect(cached?.regionSettings).toEqual(updatedSettings);
      expect(cached?.updatedAtInMs).toBe(now + 1000);
      expect(cached?.maxAgeInMs).toBe(10000);
    });

    it('triggers auto-refetch timeout', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      const initialTimerCount = vi.getTimerCount();
      provider.setServerReportedRegions({
        regionSettings: mockSettings,
        updatedAtInMs: Date.now(),
        maxAgeInMs: 5000,
      });
      const finalTimerCount = vi.getTimerCount();

      expect(finalTimerCount).toBeGreaterThan(initialTimerCount);
    });
  });

  describe('edge cases', () => {
    it('handles empty regions list', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const region = await provider.getNextBestRegionUrl();
      expect(region).toBeNull();
    });

    it('handles malformed region settings response', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');

      fetchMock.mockResolvedValue(
        createMockResponse(200, { invalid: 'data' }, { 'Cache-Control': 'max-age=3600' }),
      );

      // Should not throw during fetch, but may have issues when accessing regions
      const result = await provider.fetchRegionSettings();
      expect(result).toBeDefined();
    });

    it('handles network timeout', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');

      fetchMock.mockRejectedValue(new Error('Network timeout'));

      await expect(provider.fetchRegionSettings()).rejects.toThrow('Network timeout');
    });

    it('wraps fetch throw as ConnectionError with ServerUnreachable reason', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');

      // Simulate fetch itself throwing an error (common in network failures)
      fetchMock.mockRejectedValue(new TypeError('Failed to fetch'));

      // Should throw a ConnectionError that can be handled
      const error = await provider.fetchRegionSettings().catch((e) => e);

      expect(error).toBeInstanceOf(ConnectionError);
      expect(error.reason).toBe(ConnectionErrorReason.ServerUnreachable);
      expect(error.status).toBe(undefined);
      expect(error.message).toContain('Failed to fetch');
    });

    it('handles concurrent getNextBestRegionUrl calls', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-east', url: 'wss://us-east.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      // Make concurrent calls
      const [region1, region2] = await Promise.all([
        provider.getNextBestRegionUrl(),
        provider.getNextBestRegionUrl(),
      ]);

      // Both should return regions (may be same or different depending on timing)
      expect(region1).toBeTruthy();
      expect(region2).toBeTruthy();
    });

    it('preserves cache when one instance fails to fetch', async () => {
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock
        .mockResolvedValueOnce(
          createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
        )
        .mockResolvedValueOnce(createMockResponse(500));

      // First provider populates cache
      const provider1 = new RegionUrlProvider('wss://test.livekit.cloud', 'token1');
      await provider1.getNextBestRegionUrl();

      // Advance time to expire cache
      vi.setSystemTime(Date.now() + 4000000);

      // Second provider tries to fetch but fails
      const provider2 = new RegionUrlProvider('wss://test.livekit.cloud', 'token2');
      await expect(provider2.getNextBestRegionUrl()).rejects.toThrow();

      // Cache should still be accessible by a third instance if still valid
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it('handles token update correctly', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'initial-token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=1' }),
      );

      // Initial fetch with initial token
      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledWith(expect.anything(), {
        headers: { authorization: 'Bearer initial-token' },
        signal: undefined,
      });

      // Update token
      provider.updateToken('new-token');

      // Expire cache and fetch again
      vi.setSystemTime(Date.now() + 2000);
      const provider2 = new RegionUrlProvider('wss://test.livekit.cloud', 'new-token');
      await provider2.getNextBestRegionUrl();

      // Should use new token
      expect(fetchMock).toHaveBeenLastCalledWith(expect.anything(), {
        headers: { authorization: 'Bearer new-token' },
        signal: undefined,
      });
    });

    it('filters regions with same URL correctly', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west-1', url: 'wss://us-west.livekit.cloud' },
        { region: 'us-west-2', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=3600' }),
      );

      const region1 = await provider.getNextBestRegionUrl();
      const region2 = await provider.getNextBestRegionUrl();

      // First region should be returned, second should be null since it has the same URL
      expect(region1).toBe('wss://us-west.livekit.cloud');
      expect(region2).toBeNull(); // Filtered out because same URL was already attempted
    });
  });

  describe('connection tracking and auto-refetch cleanup', () => {
    beforeEach(() => {
      // Reset connection tracking maps
      // @ts-ignore - accessing private static field for testing
      RegionUrlProvider.connectionTrackers = new Map();
    });

    it('stops auto-refetch 30s after last connection disconnects', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
      );

      // Initial fetch to start auto-refetch
      await provider.getNextBestRegionUrl();
      expect(fetchMock).toHaveBeenCalledTimes(1);

      const hostname = provider.getServerUrl().hostname;

      // Simulate connection
      provider.notifyConnected();

      // Verify auto-refetch is running
      const timersBeforeDisconnect = vi.getTimerCount();
      expect(timersBeforeDisconnect).toBeGreaterThan(0);

      // Simulate disconnect
      provider.notifyDisconnected();

      // Should schedule cleanup timeout (30s)
      // Advance time by 29s - refetch should still be running
      vi.advanceTimersByTime(29000);
      expect(fetchMock).toHaveBeenCalledTimes(1); // No additional fetches

      // Advance by 1 more second (total 30s) - cleanup should trigger
      await vi.advanceTimersByTimeAsync(1000);

      // Auto-refetch should be stopped, so no new timers for refetch
      // @ts-ignore - accessing private static field for testing
      const refetchTimeout = RegionUrlProvider.settingsTimeouts.get(hostname);
      expect(refetchTimeout).toBeUndefined();
    });

    it('cancels cleanup when reconnecting before 30s delay', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
      );

      await provider.getNextBestRegionUrl();
      const hostname = provider.getServerUrl().hostname;

      // Connect and disconnect
      provider.notifyConnected();
      provider.notifyDisconnected();

      // Advance time by 15s (less than 30s)
      vi.advanceTimersByTime(15000);

      // Reconnect before cleanup triggers
      provider.notifyConnected();

      // @ts-ignore - accessing private static field for testing
      const tracker = RegionUrlProvider.connectionTrackers.get(hostname);
      expect(tracker?.cleanupTimeout).toBeUndefined(); // Cleanup should be cancelled

      // Advance past the original 30s mark
      vi.advanceTimersByTime(20000);

      // Auto-refetch should still be running
      // @ts-ignore - accessing private static field for testing
      const refetchTimeout = RegionUrlProvider.settingsTimeouts.get(hostname);
      expect(refetchTimeout).toBeDefined();
    });

    it('tracks multiple connections correctly', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
      );

      await provider.getNextBestRegionUrl();
      const hostname = provider.getServerUrl().hostname;

      // Simulate 3 connections
      provider.notifyConnected();
      provider.notifyConnected();
      provider.notifyConnected();

      // @ts-ignore - accessing private static field for testing
      const tracker = RegionUrlProvider.connectionTrackers.get(hostname);
      expect(tracker?.connectionCount).toBe(3);

      // Disconnect first connection
      provider.notifyDisconnected();

      // @ts-ignore - accessing private static field for testing
      expect(tracker?.connectionCount).toBe(2);

      // Should NOT schedule cleanup yet (still have active connections)
      expect(tracker?.cleanupTimeout).toBeUndefined();

      // Disconnect second connection
      provider.notifyDisconnected();
      // @ts-ignore - accessing private static field for testing
      expect(tracker?.connectionCount).toBe(1);

      // Disconnect last connection
      provider.notifyDisconnected();
      // @ts-ignore - accessing private static field for testing
      expect(tracker?.connectionCount).toBe(0);

      // NOW cleanup should be scheduled
      expect(tracker?.cleanupTimeout).toBeDefined();
    });

    it('handles disconnect without prior connect gracefully', () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const hostname = provider.getServerUrl().hostname;

      // Disconnect without connect should not throw
      expect(() => {
        provider.notifyDisconnected();
      }).not.toThrow();

      // Should not create a tracker
      // @ts-ignore - accessing private static field for testing
      const tracker = RegionUrlProvider.connectionTrackers.get(hostname);
      expect(tracker).toBeUndefined();
    });

    it('clears cleanup timeout when scheduling new cleanup', async () => {
      const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
      const mockSettings = createMockRegionSettings([
        { region: 'us-west', url: 'wss://us-west.livekit.cloud' },
      ]);

      fetchMock.mockResolvedValue(
        createMockResponse(200, mockSettings, { 'Cache-Control': 'max-age=100' }),
      );

      await provider.getNextBestRegionUrl();
      const hostname = provider.getServerUrl().hostname;

      // Connect and disconnect (schedules cleanup)
      provider.notifyConnected();
      provider.notifyDisconnected();

      // @ts-ignore - accessing private static field for testing
      const tracker = RegionUrlProvider.connectionTrackers.get(hostname);
      const firstCleanupTimeout = tracker?.cleanupTimeout;
      expect(firstCleanupTimeout).toBeDefined();

      // Reconnect and disconnect again (should cancel first and schedule new)
      provider.notifyConnected();
      provider.notifyDisconnected();

      const secondCleanupTimeout = tracker?.cleanupTimeout;
      expect(secondCleanupTimeout).toBeDefined();
      expect(secondCleanupTimeout).not.toBe(firstCleanupTimeout);
    });
  });
});
