import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InFlight } from './in-flight.decorator';

describe('InFlight Decorator', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

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

  describe('Basic usage (no options)', () => {
    class TestService {
      callCount = 0;

      @InFlight()
      async fetchData(delay = 100): Promise<string> {
        this.callCount++;
        await new Promise((resolve) => setTimeout(resolve, delay));
        return `result-${this.callCount}`;
      }

      @InFlight()
      async fetchWithError(delay = 100): Promise<string> {
        this.callCount++;
        await new Promise((resolve) => setTimeout(resolve, delay));
        throw new Error('Test error');
      }
    }

    it('should prevent multiple simultaneous calls', async () => {
      const service = new TestService();

      // Make three simultaneous calls
      const promise1 = service.fetchData();
      const promise2 = service.fetchData();
      const promise3 = service.fetchData();

      // Advance timers to complete the async operation
      await vi.runAllTimersAsync();

      // All promises should resolve to the same value
      const [result1, result2, result3] = await Promise.all([
        promise1,
        promise2,
        promise3,
      ]);

      expect(result1).toBe('result-1');
      expect(result2).toBe('result-1');
      expect(result3).toBe('result-1');
      expect(service.callCount).toBe(1);
    });

    it('should allow subsequent calls after completion', async () => {
      const service = new TestService();

      // First call
      const promise1 = service.fetchData();
      await vi.runAllTimersAsync();
      const result1 = await promise1;
      expect(result1).toBe('result-1');

      // Second call after first completes
      const promise2 = service.fetchData();
      await vi.runAllTimersAsync();
      const result2 = await promise2;
      expect(result2).toBe('result-2');

      expect(service.callCount).toBe(2);
    });

    it('should maintain separate state per instance', async () => {
      const service1 = new TestService();
      const service2 = new TestService();

      // Make simultaneous calls on different instances
      const promise1 = service1.fetchData();
      const promise2 = service2.fetchData();

      await vi.runAllTimersAsync();

      const [result1, result2] = await Promise.all([promise1, promise2]);

      // Each instance should have made its own call
      expect(result1).toBe('result-1');
      expect(result2).toBe('result-1');
      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(1);
    });
  });

  describe('With key generator', () => {
    class UserService {
      callCounts = new Map<string, number>();

      @InFlight({
        keyGenerator: (userId: string) => userId,
      })
      async fetchUser(
        userId: string,
        delay = 100,
      ): Promise<{ id: string; name: string }> {
        const count = (this.callCounts.get(userId) || 0) + 1;
        this.callCounts.set(userId, count);

        await new Promise((resolve) => setTimeout(resolve, delay));
        return { id: userId, name: `User ${userId} - Call ${count}` };
      }

      @InFlight({
        keyGenerator: undefined, // Uses JSON.stringify
      })
      async fetchWithDefaultKey(
        param1: string,
        param2: number,
      ): Promise<string> {
        const key = `${param1}-${param2}`;
        const count = (this.callCounts.get(key) || 0) + 1;
        this.callCounts.set(key, count);

        await new Promise((resolve) => setTimeout(resolve, 100));
        return `Result ${count}`;
      }
    }

    it('should deduplicate calls with same key', async () => {
      const service = new UserService();

      // Multiple calls with same userId
      const promise1 = service.fetchUser('user1');
      const promise2 = service.fetchUser('user1');
      const promise3 = service.fetchUser('user1');

      await vi.runAllTimersAsync();

      const [result1, result2, result3] = await Promise.all([
        promise1,
        promise2,
        promise3,
      ]);

      expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
      expect(result2).toEqual(result1);
      expect(result3).toEqual(result1);
      expect(service.callCounts.get('user1')).toBe(1);
    });

    it('should allow simultaneous calls with different keys', async () => {
      const service = new UserService();

      // Calls with different userIds
      const promise1 = service.fetchUser('user1');
      const promise2 = service.fetchUser('user2');
      const promise3 = service.fetchUser('user1'); // Duplicate of first

      await vi.runAllTimersAsync();

      const [result1, result2, result3] = await Promise.all([
        promise1,
        promise2,
        promise3,
      ]);

      expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
      expect(result2).toEqual({ id: 'user2', name: 'User user2 - Call 1' });
      expect(result3).toEqual(result1); // Same as first call

      expect(service.callCounts.get('user1')).toBe(1);
      expect(service.callCounts.get('user2')).toBe(1);
    });

    it('should use JSON.stringify as default key generator when keyGenerator is undefined', async () => {
      const service = new UserService();

      // Multiple calls with same arguments
      const promise1 = service.fetchWithDefaultKey('test', 123);
      const promise2 = service.fetchWithDefaultKey('test', 123);

      // Different arguments
      const promise3 = service.fetchWithDefaultKey('test', 456);

      await vi.runAllTimersAsync();

      const [result1, result2, result3] = await Promise.all([
        promise1,
        promise2,
        promise3,
      ]);

      expect(result1).toBe('Result 1');
      expect(result2).toBe('Result 1'); // Same as first
      expect(result3).toBe('Result 1'); // Different key, separate call

      expect(service.callCounts.get('test-123')).toBe(1);
      expect(service.callCounts.get('test-456')).toBe(1);
    });
  });
});
