import { act, renderHook } from '@testing-library/react';
import { useScrollEnd } from '../useScrollEnd';

describe('useScrollEnd', () => {
  let container: HTMLDivElement;
  let containerRef: { current: HTMLDivElement | null };
  let callback: jest.Mock;

  beforeEach(() => {
    jest.useFakeTimers();
    container = document.createElement('div');
    containerRef = { current: container };
    callback = jest.fn();
  });

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

  describe('native scrollend support', () => {
    beforeEach(() => {
      Object.defineProperty(HTMLElement.prototype, 'onscrollend', {
        configurable: true,
        value: null,
      });
    });

    afterEach(() => {
      // Clean up the property so it doesn't bleed into other suites
      delete (HTMLElement.prototype as any).onscrollend;
    });

    it('attaches a scrollend listener to the container', () => {
      jest.spyOn(container, 'addEventListener');
      renderHook(() => useScrollEnd(containerRef, callback));

      expect(container.addEventListener).toHaveBeenCalledWith('scrollend', expect.any(Function));
    });

    it('calls the callback when scrollend fires', () => {
      renderHook(() => useScrollEnd(containerRef, callback));

      act(() => {
        container.dispatchEvent(new Event('scrollend'));
      });

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

    it('removes the scrollend listener on unmount', () => {
      jest.spyOn(container, 'removeEventListener');
      const { unmount } = renderHook(() => useScrollEnd(containerRef, callback));

      unmount();

      expect(container.removeEventListener).toHaveBeenCalledWith('scrollend', expect.any(Function));
    });

    it('always invokes the latest callback without re-registering the listener', () => {
      const firstCallback = jest.fn();
      const secondCallback = jest.fn();

      const { rerender } = renderHook(({ cb }) => useScrollEnd(containerRef, cb), {
        initialProps: { cb: firstCallback },
      });

      rerender({ cb: secondCallback });

      act(() => {
        container.dispatchEvent(new Event('scrollend'));
      });

      expect(firstCallback).not.toHaveBeenCalled();
      expect(secondCallback).toHaveBeenCalledTimes(1);
    });
  });

  describe('fallback debounce path (no native scrollend)', () => {
    it('attaches a scroll listener when scrollend is not supported', () => {
      jest.spyOn(container, 'addEventListener');
      renderHook(() => useScrollEnd(containerRef, callback));

      expect(container.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), { passive: true });
    });

    it('does not call the callback immediately on scroll', () => {
      renderHook(() => useScrollEnd(containerRef, callback));

      act(() => {
        container.dispatchEvent(new Event('scroll'));
      });

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

    it('calls the callback after the default debounce delay (250 ms)', () => {
      renderHook(() => useScrollEnd(containerRef, callback));

      act(() => {
        container.dispatchEvent(new Event('scroll'));
        jest.advanceTimersByTime(249);
      });
      expect(callback).not.toHaveBeenCalled();

      act(() => {
        jest.advanceTimersByTime(1);
      });
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('resets the debounce timer on rapid successive scrolls', () => {
      renderHook(() => useScrollEnd(containerRef, callback));

      act(() => {
        container.dispatchEvent(new Event('scroll'));
        jest.advanceTimersByTime(200);
        container.dispatchEvent(new Event('scroll')); // resets timer
        jest.advanceTimersByTime(200);
      });
      expect(callback).not.toHaveBeenCalled();

      act(() => {
        jest.advanceTimersByTime(50);
      });
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('respects a custom fallbackDelayMs', () => {
      renderHook(() => useScrollEnd(containerRef, callback, 100));

      act(() => {
        container.dispatchEvent(new Event('scroll'));
        jest.advanceTimersByTime(99);
      });
      expect(callback).not.toHaveBeenCalled();

      act(() => {
        jest.advanceTimersByTime(1);
      });
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('removes the scroll listener and clears the pending timer on unmount', () => {
      jest.spyOn(container, 'removeEventListener');
      const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout');

      const { unmount } = renderHook(() => useScrollEnd(containerRef, callback));

      act(() => {
        container.dispatchEvent(new Event('scroll')); // starts the timer
      });

      unmount();

      expect(container.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
      expect(clearTimeoutSpy).toHaveBeenCalled();
      // Advance past the delay — callback must NOT fire after unmount
      act(() => {
        jest.advanceTimersByTime(500);
      });
      expect(callback).not.toHaveBeenCalled();
    });
  });

  describe('null containerRef', () => {
    it('does nothing when containerRef.current is null', () => {
      containerRef.current = null;
      // Should not throw
      expect(() => renderHook(() => useScrollEnd(containerRef, callback))).not.toThrow();
      expect(callback).not.toHaveBeenCalled();
    });
  });
});
