import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import useDebounceCallback from './useDebounceCallback';

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

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

  describe('default options', () => {
    it('should not invoke callback immediately', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        result.current();
      });

      expect(callback).toHaveBeenCalledTimes(0);
    });

    it('should invoke callback after delay period', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        result.current();
      });

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should reset delay on subsequent calls', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        result.current(); // First call at t=0
      });

      act(() => {
        vi.advanceTimersByTime(500); // t=500
      });

      // Should not be called yet
      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        result.current(); // Second call at t=500, resets the timer
      });

      act(() => {
        vi.advanceTimersByTime(500); // t=1000
      });

      // Should still not be called (only 500ms since last call at t=500)
      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        vi.advanceTimersByTime(500); // t=1500
      });

      // Now it should be called (1000ms since last call at t=500)
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should only invoke once after period of inactivity', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      act(() => {
        result.current();
        result.current();
        result.current();
        result.current();
      });

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(1);
    });
  });

  describe('arguments', () => {
    it('should pass arguments to callback correctly', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      act(() => {
        result.current('arg1', 'arg2', 123);
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledWith('arg1', 'arg2', 123);
    });

    it('should use latest arguments when debounce fires', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      act(() => {
        result.current('first');
        result.current('second');
        result.current('third');
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledWith('third');
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should use the expected arguments across multiple debounce periods', () => {
      const callback = vi.fn((arg: string) => arg);
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        result.current('first');
        result.current('second');
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(1);
      expect(callback).toHaveBeenCalledWith('second');
      expect(callback).toHaveReturnedWith('second');

      act(() => {
        result.current('third');
        result.current('fourth');
        result.current('fifth');
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(2);
      expect(callback).toHaveBeenCalledWith('fifth');
      expect(callback).toHaveReturnedWith('fifth');

      act(() => {
        result.current('sixth');
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(3);
      expect(callback).toHaveBeenCalledWith('sixth');
      expect(callback).toHaveReturnedWith('sixth');
    });
  });

  describe('options', () => {
    it('should respect leading: true option', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { leading: true })
      );

      act(() => {
        result.current();
      });

      // Should invoke immediately with leading: true
      expect(callback).toHaveBeenCalledTimes(1);

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      // Should not invoke again (no trailing edge with single call)
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should respect trailing: false option', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { trailing: false })
      );

      act(() => {
        result.current();
      });

      expect(callback).not.toHaveBeenCalled();

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      // Should not invoke (trailing is disabled)
      expect(callback).not.toHaveBeenCalled();
    });

    it('should respect leading: true and trailing: true', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { leading: true, trailing: true })
      );

      act(() => {
        result.current('first'); // Leading edge
        result.current('second'); // Trailing edge
      });

      expect(callback).toHaveBeenCalledTimes(1);
      expect(callback).toHaveBeenLastCalledWith('first');

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).toHaveBeenCalledTimes(2);
      expect(callback).toHaveBeenLastCalledWith('second');
    });

    it('should respect leading: true and trailing: false', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { leading: true, trailing: false })
      );

      act(() => {
        result.current('first'); // Leading edge only
        result.current('second'); // Should be ignored
      });

      expect(callback).toHaveBeenCalledTimes(1);
      expect(callback).toHaveBeenLastCalledWith('first');

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      // Should still be 1, no trailing edge
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should respect maxWait option', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { maxWait: 1500 })
      );

      act(() => {
        result.current('first');
      });

      expect(callback).toHaveBeenCalledTimes(0);

      act(() => {
        vi.advanceTimersByTime(500);
      });

      act(() => {
        result.current('second');
      });

      expect(callback).toHaveBeenCalledTimes(0);

      // Initial 1000ms debounce delay
      act(() => {
        vi.advanceTimersByTime(500);
      });

      expect(callback).toHaveBeenCalledTimes(0);

      // 1500ms since first call, but before second 1000ms debounce delay
      act(() => {
        vi.advanceTimersByTime(500);
      });

      // Should invoke at maxWait (1500ms total since first call)
      expect(callback).toHaveBeenCalledTimes(1);
      expect(callback).toHaveBeenLastCalledWith('second');
    });
  });

  describe('control functions', () => {
    it('should cancel pending invocation', () => {
      const callback = vi.fn();
      const { result } = renderHook(() => useDebounceCallback(callback, 1000));

      act(() => {
        result.current();
      });

      act(() => {
        result.current.cancel();
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      expect(callback).not.toHaveBeenCalled();
    });

    it('should flush pending invocation immediately', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { trailing: true })
      );

      act(() => {
        result.current('test');
      });

      expect(callback).not.toHaveBeenCalled();

      act(() => {
        result.current.flush();
      });

      expect(callback).toHaveBeenCalledTimes(1);
      expect(callback).toHaveBeenLastCalledWith('test');
    });

    it('should return result when flushing', () => {
      const callback = vi.fn((x: number) => x * 2);
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { trailing: true })
      );

      act(() => {
        result.current(5);
      });

      let flushResult: number | undefined;
      act(() => {
        flushResult = result.current.flush();
      });

      expect(flushResult).toBe(10);
      expect(callback).toHaveBeenCalledWith(5);
    });

    it('should handle cancel after flush', () => {
      const callback = vi.fn();
      const { result } = renderHook(() =>
        useDebounceCallback(callback, 1000, { trailing: true })
      );

      act(() => {
        result.current();
      });

      act(() => {
        result.current.flush();
      });

      act(() => {
        result.current.cancel();
      });

      act(() => {
        vi.advanceTimersByTime(1000);
      });

      // Should only be called once from flush
      expect(callback).toHaveBeenCalledTimes(1);
    });
  });

  it('should handle undefined delay time', () => {
    const callback = vi.fn();
    const { result } = renderHook(() =>
      useDebounceCallback(callback, undefined, { trailing: true })
    );

    act(() => {
      result.current();
    });

    act(() => {
      vi.advanceTimersByTime(500); // default delay
    });

    expect(callback).toHaveBeenCalled();
  });

  it('should handle delay of 0', () => {
    const callback = vi.fn();
    const { result } = renderHook(() =>
      useDebounceCallback(callback, 0, { trailing: true })
    );

    act(() => {
      result.current();
    });

    act(() => {
      vi.advanceTimersByTime(0);
    });

    expect(callback).toHaveBeenCalled();
  });

  it('should update debounced function when callback changes', () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    const { result, rerender } = renderHook(
      ({ cb }) => useDebounceCallback(cb, 1000, { trailing: true }),
      { initialProps: { cb: callback1 } }
    );

    act(() => {
      result.current();
    });

    // Update the callback
    rerender({ cb: callback2 });

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // Should call the new callback
    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).toHaveBeenCalledTimes(1);
  });

  it('should cancel pending invocation when callback changes', () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    const { result, rerender } = renderHook(
      ({ cb }) => useDebounceCallback(cb, 1000, { leading: true }),
      { initialProps: { cb: callback1 } }
    );

    act(() => {
      result.current('first');
    });

    // callback1 should be called immediately with leading: true
    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback1).toHaveBeenLastCalledWith('first');

    act(() => {
      vi.advanceTimersByTime(500);
    });

    // Update the callback (should cancel any pending invocation)
    rerender({ cb: callback2 });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    // callback1 should still be at 1, callback2 not called
    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback2).not.toHaveBeenCalled();

    // New call with updated callback
    act(() => {
      result.current('second');
    });

    // callback2 should be called immediately with leading: true
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenLastCalledWith('second');
  });

  it('should cancel pending invocation when delay changes', () => {
    const callback = vi.fn();

    const { result, rerender } = renderHook(
      ({ delay }) => useDebounceCallback(callback, delay, { trailing: true }),
      { initialProps: { delay: 1000 } }
    );

    act(() => {
      result.current('first');
    });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    // Update the delay (should cancel pending invocation)
    rerender({ delay: 2000 });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    // Should not be called because the debounce was canceled
    expect(callback).not.toHaveBeenCalled();

    // New call with updated delay
    act(() => {
      result.current('second');
    });

    act(() => {
      vi.advanceTimersByTime(2000);
    });

    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenLastCalledWith('second');
  });

  it('should maintain separate debounce state for different hook instances', () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    const { result: result1 } = renderHook(() =>
      useDebounceCallback(callback1, 1000, { trailing: true })
    );
    const { result: result2 } = renderHook(() =>
      useDebounceCallback(callback2, 1000, { trailing: true })
    );

    act(() => {
      result1.current('first');
      result2.current('second');
    });

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback1).toHaveBeenLastCalledWith('first');
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenLastCalledWith('second');
  });

  it('should handle complex object arguments', () => {
    const callback = vi.fn();
    const { result } = renderHook(() =>
      useDebounceCallback(callback, 1000, { trailing: true })
    );

    const complexArg = { id: 1, data: { nested: 'value' }, array: [1, 2, 3] };

    act(() => {
      result.current(complexArg);
    });

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(callback).toHaveBeenLastCalledWith(complexArg);
  });

  it('should work with typed callbacks', () => {
    type UserData = { id: number; name: string };
    const callback = vi.fn((_userData: UserData, _extra: string) => {});

    const { result } = renderHook(() =>
      useDebounceCallback(callback, 1000, { trailing: true })
    );

    const userData: UserData = { id: 1, name: 'Test User' };

    act(() => {
      result.current(userData, 'extra-param');
    });

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(callback).toHaveBeenLastCalledWith(userData, 'extra-param');
  });

  it('should handle rapid successive calls correctly', () => {
    const callback = vi.fn();
    const { result } = renderHook(() =>
      useDebounceCallback(callback, 1000, { trailing: true })
    );

    // Simulate rapid typing
    act(() => {
      for (let i = 0; i < 10; i++) {
        result.current(`value-${i}`);
        vi.advanceTimersByTime(50);
      }
    });

    // Should not have been called yet
    expect(callback).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // Should only be called once with the last value
    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenLastCalledWith('value-9');
  });

  it('should cleanup on unmount', () => {
    const callback = vi.fn();
    const { result, unmount } = renderHook(() =>
      useDebounceCallback(callback, 1000)
    );

    act(() => {
      result.current();
    });

    unmount();

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // Should not be called after unmount
    expect(callback).not.toHaveBeenCalled();
  });

  it('should handle multiple flushes', () => {
    const callback = vi.fn((x: number) => x * 2);
    const { result } = renderHook(() =>
      useDebounceCallback(callback, 1000, { trailing: true })
    );

    act(() => {
      result.current(5);
    });

    let firstFlush: number | undefined;
    act(() => {
      firstFlush = result.current.flush();
    });

    expect(firstFlush).toBe(10);
    expect(callback).toHaveBeenCalledTimes(1);

    act(() => {
      result.current(10);
    });

    let secondFlush: number | undefined;
    act(() => {
      secondFlush = result.current.flush();
    });

    expect(secondFlush).toBe(20);
    expect(callback).toHaveBeenCalledTimes(2);
  });

  it('should NOT cancel pending invocation when callback changes', () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    const { result, rerender } = renderHook(
      ({ cb }) => useDebounceCallback(cb, 1000),
      { initialProps: { cb: callback1 } }
    );

    act(() => {
      result.current('test-value');
    });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).not.toHaveBeenCalled();

    // Change the callback
    rerender({ cb: callback2 });

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenLastCalledWith('test-value');
  });

  it('should cancel pending invocation when any options change', () => {
    const callback = vi.fn();

    const { result, rerender } = renderHook(
      ({ opts }) => useDebounceCallback(callback, 1000, opts),
      { initialProps: { opts: { leading: false, trailing: true } } }
    );

    act(() => {
      result.current('first-call');
    });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(callback).not.toHaveBeenCalled();

    // Change the options
    rerender({ opts: { leading: true, trailing: false } });

    expect(callback).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(callback).not.toHaveBeenCalled();

    act(() => {
      result.current('second-call');
    });

    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenLastCalledWith('second-call');

    act(() => {
      result.current('third-call');
    });

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('should cancel pending invocation when unmounting', () => {
    const callback = vi.fn();

    const { result, unmount } = renderHook(() =>
      useDebounceCallback(callback, 1000)
    );

    act(() => {
      result.current('test-value');
    });

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(callback).not.toHaveBeenCalled();

    // Unmount the hook
    unmount();

    expect(callback).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(600);
    });

    expect(callback).not.toHaveBeenCalled();
  });
});
