import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Debounce } from './debounce.decorator';

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

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

  describe('Basic debouncing', () => {
    class TestService {
      callCount = 0;
      lastArgs: any[] = [];

      @Debounce({ wait: 300 })
      debouncedMethod(value: string): void {
        this.callCount++;
        this.lastArgs = [value];
      }

      @Debounce({ wait: 100 })
      debouncedWithMultipleArgs(a: number, b: string): void {
        this.callCount++;
        this.lastArgs = [a, b];
      }
    }

    it('should debounce method calls', () => {
      const service = new TestService();

      // Call method multiple times rapidly
      service.debouncedMethod('first');
      service.debouncedMethod('second');
      service.debouncedMethod('third');

      // Should not have been called yet
      expect(service.callCount).toBe(0);

      // Fast forward past debounce delay
      vi.advanceTimersByTime(300);

      // Should have been called once with the last arguments
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['third']);
    });

    it('should handle multiple arguments correctly', () => {
      const service = new TestService();

      service.debouncedWithMultipleArgs(1, 'a');
      service.debouncedWithMultipleArgs(2, 'b');
      service.debouncedWithMultipleArgs(3, 'c');

      expect(service.callCount).toBe(0);

      vi.advanceTimersByTime(100);

      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([3, 'c']);
    });

    it('should reset timer on subsequent calls', () => {
      const service = new TestService();

      service.debouncedMethod('first');

      // Advance time but not enough to trigger
      vi.advanceTimersByTime(200);
      expect(service.callCount).toBe(0);

      // Call again, should reset the timer
      service.debouncedMethod('second');

      // Advance by full delay from second call
      vi.advanceTimersByTime(300);
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['second']);
    });
  });

  describe('Leading edge execution', () => {
    class TestService {
      callCount = 0;
      lastArgs: any[] = [];

      @Debounce({ wait: 300, leading: true, trailing: false })
      leadingMethod(value: string): void {
        this.callCount++;
        this.lastArgs = [value];
      }

      @Debounce({ wait: 300, leading: true, trailing: true })
      bothEdgesMethod(value: string): void {
        this.callCount++;
        this.lastArgs = [value];
      }
    }

    it('should execute on leading edge when configured', () => {
      const service = new TestService();

      service.leadingMethod('first');

      // Should execute immediately on leading edge
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['first']);

      // Additional calls within debounce period should not execute
      service.leadingMethod('second');
      service.leadingMethod('third');

      vi.advanceTimersByTime(300);

      // Still should only have been called once (trailing: false)
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['first']);
    });

    it('should execute on both edges when configured', () => {
      const service = new TestService();

      service.bothEdgesMethod('first');

      // Should execute immediately on leading edge
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['first']);

      // Additional calls within debounce period
      service.bothEdgesMethod('second');
      service.bothEdgesMethod('third');

      vi.advanceTimersByTime(300);

      // Should have been called twice (leading + trailing)
      expect(service.callCount).toBe(2);
      expect(service.lastArgs).toEqual(['third']);
    });
  });

  describe('MaxWait option', () => {
    class TestService {
      callCount = 0;
      lastArgs: any[] = [];

      @Debounce({ wait: 1000, maxWait: 2000 })
      maxWaitMethod(value: string): void {
        this.callCount++;
        this.lastArgs = [value];
      }
    }

    it('should respect maxWait limit', () => {
      const service = new TestService();

      // Start calling repeatedly
      service.maxWaitMethod('first');
      expect(service.callCount).toBe(0);

      // Keep calling every 900ms (less than wait time)
      for (let i = 0; i < 5; i++) {
        vi.advanceTimersByTime(900);
        service.maxWaitMethod(`call-${i}`);
        if (i < 2) {
          expect(service.callCount).toBe(0); // Should not execute yet
        }
      }

      // After maxWait time (2000ms), should execute despite continuous calls
      vi.advanceTimersByTime(200); // Total: ~2000ms
      expect(service.callCount).toBeGreaterThan(0);
    });
  });

  describe('Multiple instances', () => {
    class TestService {
      callCount = 0;

      @Debounce({ wait: 300 })
      debouncedMethod(): void {
        this.callCount++;
      }
    }

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

      // Call methods on both instances
      service1.debouncedMethod();
      service2.debouncedMethod();

      // Both should be independent
      expect(service1.callCount).toBe(0);
      expect(service2.callCount).toBe(0);

      vi.advanceTimersByTime(300);

      // Both should have been called once
      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(1);
    });

    it('should not interfere between instances', () => {
      const service1 = new TestService();
      const service2 = new TestService();

      service1.debouncedMethod();

      // Advance partially
      vi.advanceTimersByTime(150);

      service2.debouncedMethod();

      // Advance remaining time for service1
      vi.advanceTimersByTime(150);

      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(0);

      // Advance remaining time for service2
      vi.advanceTimersByTime(150);

      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(1);
    });
  });

  describe('Default options', () => {
    class TestService {
      callCount = 0;

      @Debounce()
      defaultOptionsMethod(): void {
        this.callCount++;
      }

      @Debounce({})
      emptyOptionsMethod(): void {
        this.callCount++;
      }
    }

    it('should use default options when none provided', () => {
      const service = new TestService();

      service.defaultOptionsMethod();

      // Default wait is 0, trailing is true
      expect(service.callCount).toBe(0);

      vi.advanceTimersByTime(0);

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

    it('should use default options when empty options provided', () => {
      const service = new TestService();

      service.emptyOptionsMethod();

      expect(service.callCount).toBe(0);

      vi.advanceTimersByTime(0);

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