import { OAuthReconnectionTracker } from './OAuthReconnectionTracker';

describe('OAuthReconnectTracker', () => {
  let tracker: OAuthReconnectionTracker;
  const userId = 'user123';
  const serverName = 'test-server';
  const anotherServer = 'another-server';

  beforeEach(() => {
    tracker = new OAuthReconnectionTracker();
  });

  describe('setFailed', () => {
    it('should record a failed reconnection attempt', () => {
      tracker.setFailed(userId, serverName);
      expect(tracker.isFailed(userId, serverName)).toBe(true);
    });

    it('should track multiple servers for the same user', () => {
      tracker.setFailed(userId, serverName);
      tracker.setFailed(userId, anotherServer);

      expect(tracker.isFailed(userId, serverName)).toBe(true);
      expect(tracker.isFailed(userId, anotherServer)).toBe(true);
    });

    it('should track different users independently', () => {
      const anotherUserId = 'user456';

      tracker.setFailed(userId, serverName);
      tracker.setFailed(anotherUserId, serverName);

      expect(tracker.isFailed(userId, serverName)).toBe(true);
      expect(tracker.isFailed(anotherUserId, serverName)).toBe(true);
    });
  });

  describe('isFailed', () => {
    it('should return false when no failed attempt is recorded', () => {
      expect(tracker.isFailed(userId, serverName)).toBe(false);
    });

    it('should return true after a failed attempt is recorded', () => {
      tracker.setFailed(userId, serverName);
      expect(tracker.isFailed(userId, serverName)).toBe(true);
    });

    it('should return false for a different server even after another server failed', () => {
      tracker.setFailed(userId, serverName);
      expect(tracker.isFailed(userId, anotherServer)).toBe(false);
    });
  });

  describe('removeFailed', () => {
    it('should clear a failed reconnect record', () => {
      tracker.setFailed(userId, serverName);
      expect(tracker.isFailed(userId, serverName)).toBe(true);

      tracker.removeFailed(userId, serverName);
      expect(tracker.isFailed(userId, serverName)).toBe(false);
    });

    it('should only clear the specific server for the user', () => {
      tracker.setFailed(userId, serverName);
      tracker.setFailed(userId, anotherServer);

      tracker.removeFailed(userId, serverName);

      expect(tracker.isFailed(userId, serverName)).toBe(false);
      expect(tracker.isFailed(userId, anotherServer)).toBe(true);
    });

    it('should handle clearing non-existent records gracefully', () => {
      expect(() => tracker.removeFailed(userId, serverName)).not.toThrow();
    });
  });

  describe('setActive', () => {
    it('should mark a server as reconnecting', () => {
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should track multiple reconnecting servers', () => {
      tracker.setActive(userId, serverName);
      tracker.setActive(userId, anotherServer);

      expect(tracker.isActive(userId, serverName)).toBe(true);
      expect(tracker.isActive(userId, anotherServer)).toBe(true);
    });
  });

  describe('isActive', () => {
    it('should return false when server is not reconnecting', () => {
      expect(tracker.isActive(userId, serverName)).toBe(false);
    });

    it('should return true when server is marked as reconnecting', () => {
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should handle non-existent user gracefully', () => {
      expect(tracker.isActive('non-existent-user', serverName)).toBe(false);
    });
  });

  describe('removeActive', () => {
    it('should clear reconnecting state for a server', () => {
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      tracker.removeActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(false);
    });

    it('should only clear specific server state', () => {
      tracker.setActive(userId, serverName);
      tracker.setActive(userId, anotherServer);

      tracker.removeActive(userId, serverName);

      expect(tracker.isActive(userId, serverName)).toBe(false);
      expect(tracker.isActive(userId, anotherServer)).toBe(true);
    });

    it('should handle clearing non-existent state gracefully', () => {
      expect(() => tracker.removeActive(userId, serverName)).not.toThrow();
    });
  });

  describe('cleanup behavior', () => {
    it('should clean up empty user sets for failed reconnects', () => {
      tracker.setFailed(userId, serverName);
      tracker.removeFailed(userId, serverName);

      // Record and clear another user to ensure internal cleanup
      const anotherUserId = 'user456';
      tracker.setFailed(anotherUserId, serverName);

      // Original user should still be able to reconnect
      expect(tracker.isFailed(userId, serverName)).toBe(false);
    });

    it('should clean up empty user sets for active reconnections', () => {
      tracker.setActive(userId, serverName);
      tracker.removeActive(userId, serverName);

      // Mark another user to ensure internal cleanup
      const anotherUserId = 'user456';
      tracker.setActive(anotherUserId, serverName);

      // Original user should not be reconnecting
      expect(tracker.isActive(userId, serverName)).toBe(false);
    });
  });

  describe('combined state management', () => {
    it('should handle both failed and reconnecting states independently', () => {
      // Mark as reconnecting
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);
      expect(tracker.isFailed(userId, serverName)).toBe(false);

      // Record failed attempt
      tracker.setFailed(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);
      expect(tracker.isFailed(userId, serverName)).toBe(true);

      // Clear reconnecting state
      tracker.removeActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(false);
      expect(tracker.isFailed(userId, serverName)).toBe(true);

      // Clear failed state
      tracker.removeFailed(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(false);
      expect(tracker.isFailed(userId, serverName)).toBe(false);
    });
  });

  describe('timeout behavior', () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

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

    it('should track timestamp when setting active state', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Verify timestamp was recorded (implementation detail tested via timeout behavior)
      jest.advanceTimersByTime(1000); // 1 second
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should handle timeout checking with isStillReconnecting', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(true);

      // Advance time by 2 minutes 59 seconds - should still be reconnecting
      jest.advanceTimersByTime(2 * 60 * 1000 + 59 * 1000);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(true);

      // Advance time by 2 more seconds (total 3 minutes 1 second) - should not be still reconnecting
      jest.advanceTimersByTime(2000);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);

      // But isActive should still return true (simple check)
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should handle multiple servers with different timeout periods', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      // Set server1 as active
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance 3 minutes
      jest.advanceTimersByTime(3 * 60 * 1000);

      // Set server2 as active
      tracker.setActive(userId, anotherServer);
      expect(tracker.isActive(userId, anotherServer)).toBe(true);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance 2 more minutes + 1ms (server1 at 5 min + 1ms, server2 at 2 min + 1ms)
      jest.advanceTimersByTime(2 * 60 * 1000 + 1);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false); // server1 timed out
      expect(tracker.isStillReconnecting(userId, anotherServer)).toBe(true); // server2 still active

      // Advance 3 more minutes (server2 at 5 min + 1ms)
      jest.advanceTimersByTime(3 * 60 * 1000);
      expect(tracker.isStillReconnecting(userId, anotherServer)).toBe(false); // server2 timed out
    });

    it('should clear timestamp when removing active state', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      tracker.removeActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(false);

      // Set active again and verify new timestamp is used
      jest.advanceTimersByTime(3 * 60 * 1000);
      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance 4 more minutes from new timestamp - should still be active
      jest.advanceTimersByTime(4 * 60 * 1000);
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should properly cleanup after timeout occurs', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      tracker.setActive(userId, anotherServer);
      expect(tracker.isActive(userId, serverName)).toBe(true);
      expect(tracker.isActive(userId, anotherServer)).toBe(true);

      // Advance past timeout
      jest.advanceTimersByTime(6 * 60 * 1000);

      // Both should still be in active set but not "still reconnecting"
      expect(tracker.isActive(userId, serverName)).toBe(true);
      expect(tracker.isActive(userId, anotherServer)).toBe(true);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);
      expect(tracker.isStillReconnecting(userId, anotherServer)).toBe(false);

      // Cleanup both
      expect(tracker.cleanupIfTimedOut(userId, serverName)).toBe(true);
      expect(tracker.cleanupIfTimedOut(userId, anotherServer)).toBe(true);

      // Now they should be removed from active set
      expect(tracker.isActive(userId, serverName)).toBe(false);
      expect(tracker.isActive(userId, anotherServer)).toBe(false);
    });

    it('should handle timeout check for non-existent entries gracefully', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      // Check non-existent entry
      expect(tracker.isActive('non-existent', 'non-existent')).toBe(false);
      expect(tracker.isStillReconnecting('non-existent', 'non-existent')).toBe(false);

      // Set and then manually remove
      tracker.setActive(userId, serverName);
      tracker.removeActive(userId, serverName);

      // Advance time and check - should not throw
      jest.advanceTimersByTime(6 * 60 * 1000);
      expect(tracker.isActive(userId, serverName)).toBe(false);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);
    });
  });

  describe('isStillReconnecting', () => {
    it('should return true for active entries within timeout', () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(true);

      // Still within timeout
      jest.advanceTimersByTime(3 * 60 * 1000);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(true);

      jest.useRealTimers();
    });

    it('should return false for timed out entries', () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);

      // Advance past timeout
      jest.advanceTimersByTime(6 * 60 * 1000);

      // Should not be still reconnecting
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);

      // But isActive should still return true (simple check)
      expect(tracker.isActive(userId, serverName)).toBe(true);

      jest.useRealTimers();
    });

    it('should return false for non-existent entries', () => {
      expect(tracker.isStillReconnecting('non-existent', 'non-existent')).toBe(false);
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);
    });
  });

  describe('cleanupIfTimedOut', () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

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

    it('should cleanup timed out entries and return true', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance past timeout
      jest.advanceTimersByTime(6 * 60 * 1000);

      // Cleanup should return true and remove the entry
      const wasCleanedUp = tracker.cleanupIfTimedOut(userId, serverName);
      expect(wasCleanedUp).toBe(true);
      expect(tracker.isActive(userId, serverName)).toBe(false);
    });

    it('should not cleanup active entries and return false', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);

      // Within timeout period
      jest.advanceTimersByTime(3 * 60 * 1000);

      const wasCleanedUp = tracker.cleanupIfTimedOut(userId, serverName);
      expect(wasCleanedUp).toBe(false);
      expect(tracker.isActive(userId, serverName)).toBe(true);
    });

    it('should return false for non-existent entries', () => {
      const wasCleanedUp = tracker.cleanupIfTimedOut('non-existent', 'non-existent');
      expect(wasCleanedUp).toBe(false);
    });
  });

  describe('timestamp tracking edge cases', () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

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

    it('should update timestamp when setting active on already active server', () => {
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance 3 minutes
      jest.advanceTimersByTime(3 * 60 * 1000);
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Set active again - should reset timestamp
      tracker.setActive(userId, serverName);

      // Advance 4 more minutes from reset (total 7 minutes from start)
      jest.advanceTimersByTime(4 * 60 * 1000);
      // Should still be active since timestamp was reset at 3 minutes
      expect(tracker.isActive(userId, serverName)).toBe(true);

      // Advance 2 more minutes (6 minutes from reset)
      jest.advanceTimersByTime(2 * 60 * 1000);
      // Should not be still reconnecting (timed out)
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);
    });

    it('should handle same server for different users independently', () => {
      const anotherUserId = 'user456';
      const now = Date.now();
      jest.setSystemTime(now);

      tracker.setActive(userId, serverName);

      // Advance 3 minutes
      jest.advanceTimersByTime(3 * 60 * 1000);

      tracker.setActive(anotherUserId, serverName);

      // Advance 3 more minutes
      jest.advanceTimersByTime(3 * 60 * 1000);

      // First user's connection should be timed out
      expect(tracker.isStillReconnecting(userId, serverName)).toBe(false);
      // Second user's connection should still be reconnecting
      expect(tracker.isStillReconnecting(anotherUserId, serverName)).toBe(true);
    });
  });
});
