/* eslint-disable max-len */
import { RefObject } from 'react';
import { act, renderHook } from '@testing-library/react';
import useWidgetObserved from '../useWidgetObserved';
import { useStore } from '../../services/store';

jest.mock('../../services/store', () => ({
  useStore: jest.fn(),
}));

describe('useWidgetObserved', () => {
  let mockObserve: jest.Mock;
  let mockDisconnect: jest.Mock;
  let intersectionCallback: IntersectionObserverCallback;
  let mockTriggerEvent: jest.Mock;
  let widgetRef: RefObject<Element | null>;

  beforeEach(() => {
    jest.useFakeTimers();
    jest.clearAllMocks();

    mockObserve = jest.fn();
    mockDisconnect = jest.fn();
    mockTriggerEvent = jest.fn();
    widgetRef = { current: document.createElement('div') };

    global.IntersectionObserver = jest.fn((callback) => {
      intersectionCallback = callback;
      return {
        observe: mockObserve,
        disconnect: mockDisconnect,
      } as unknown as IntersectionObserver;
    }) as unknown as typeof IntersectionObserver;

    (useStore as jest.Mock).mockReturnValue({
      data: {
        id: 'widget-default',
        settings: { type: 'carousel' },
      },
      triggerEvent: mockTriggerEvent,
    });
  });

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

  it('creates an observer with a full-visibility threshold', () => {
    renderHook(() => useWidgetObserved(widgetRef));

    expect(global.IntersectionObserver).toHaveBeenCalledWith(
      expect.any(Function),
      { threshold: 1.0 },
    );
    expect(mockObserve).toHaveBeenCalledWith(widgetRef.current);
  });

  it('fires widgetObserved after one second of continuous visibility', () => {
    (useStore as jest.Mock).mockReturnValue({
      data: {
        id: 'widget-visible',
        settings: { type: 'wall' },
      },
      triggerEvent: mockTriggerEvent,
    });

    renderHook(() => useWidgetObserved(widgetRef));

    act(() => {
      intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver);
      jest.advanceTimersByTime(999);
    });

    expect(mockTriggerEvent).not.toHaveBeenCalled();

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

    expect(mockTriggerEvent).toHaveBeenCalledWith('widgetObserved', { layout: 'wall' }, 'widget-visible');
  });

  it('cancels the timer when the widget leaves the viewport before one second', () => {
    (useStore as jest.Mock).mockReturnValue({
      data: {
        id: 'widget-cancelled',
        settings: { type: 'mosaic' },
      },
      triggerEvent: mockTriggerEvent,
    });

    renderHook(() => useWidgetObserved(widgetRef));

    act(() => {
      intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver);
      jest.advanceTimersByTime(500);
      intersectionCallback([{ isIntersecting: false } as IntersectionObserverEntry], {} as IntersectionObserver);
      jest.advanceTimersByTime(500);
    });

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

  it('fires only once for the same widget id across remounts', () => {
    (useStore as jest.Mock).mockReturnValue({
      data: {
        id: 'widget-once',
        settings: { type: 'carousel' },
      },
      triggerEvent: mockTriggerEvent,
    });

    const { unmount } = renderHook(() => useWidgetObserved(widgetRef));

    act(() => {
      intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver);
      jest.advanceTimersByTime(1000);
    });

    unmount();

    renderHook(() => useWidgetObserved(widgetRef));

    expect(mockTriggerEvent).toHaveBeenCalledTimes(1);
    expect(mockObserve).toHaveBeenCalledTimes(1);
  });

  it('disconnects the observer and clears a pending timer on unmount', () => {
    const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
    const { unmount } = renderHook(() => useWidgetObserved(widgetRef));

    act(() => {
      intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver);
    });

    unmount();

    expect(mockDisconnect).toHaveBeenCalled();
    expect(clearTimeoutSpy).toHaveBeenCalled();

    clearTimeoutSpy.mockRestore();
  });
});
