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

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Build a ref whose .current points to a real (detached) div. */
const makeCloneRef = (): RefObject<HTMLElement> => {
  const clone = document.createElement('div');
  return { current: clone } as unknown as RefObject<HTMLElement>;
};

/**
 * Create a source thumbnail element (with the private data attribute) and
 * append it to the document so querySelectorAll can find it.
 */
const makeSource = (
  rectOverrides: Partial<DOMRect>,
  bgImage: string,
): HTMLElement => {
  const el = document.createElement('div');
  el.setAttribute('data-adl-thumbnail-active', 'true');
  el.style.backgroundImage = bgImage;
  el.getBoundingClientRect = jest.fn(
    () =>
      ({
        top: 0,
        left: 0,
        width: 100,
        height: 100,
        bottom: 100,
        right: 100,
        x: 0,
        y: 0,
        toJSON: jest.fn(),
        ...rectOverrides,
      }) as DOMRect,
  );
  document.body.appendChild(el);
  return el;
};

// ---------------------------------------------------------------------------
// Suite
// ---------------------------------------------------------------------------

describe('useActiveThumbnailRect', () => {
  // Track the most-recently registered RAF callback so tests can trigger it.
  let pendingRaf: FrameRequestCallback | null = null;

  beforeEach(() => {
    pendingRaf = null;
    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
      pendingRaf = cb;
      return 1;
    });
    jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(jest.fn());
    jest
      .spyOn(window, 'getComputedStyle')
      .mockReturnValue({ borderRadius: '4px' } as unknown as CSSStyleDeclaration);
  });

  afterEach(() => {
    jest.restoreAllMocks();
    // Remove any source elements added to the DOM.
    document
      .querySelectorAll('[data-adl-thumbnail-active]')
      .forEach((el) => el.remove());
  });

  /** Run one RAF tick synchronously inside act(). */
  const flush = () =>
    act(() => {
      const cb = pendingRaf;
      pendingRaf = null;
      cb?.(0);
    });

  // -------------------------------------------------------------------------

  it('hides the clone when no active thumbnail element exists in the DOM', () => {
    const cloneRef = makeCloneRef();
    renderHook(() => useActiveThumbnailRect(cloneRef, null));

    flush();

    expect(cloneRef.current!.style.display).toBe('none');
  });

  it('shows the clone and copies position + image from the source element', () => {
    makeSource({ top: 50, left: 120, width: 200, height: 150 }, 'url("thumb.jpg")');
    const cloneRef = makeCloneRef();
    renderHook(() => useActiveThumbnailRect(cloneRef, null));

    flush();

    const clone = cloneRef.current!;
    expect(clone.style.display).toBe('block');
    expect(clone.style.top).toBe('50px');
    expect(clone.style.left).toBe('120px');
    expect(clone.style.width).toBe('200px');
    expect(clone.style.height).toBe('150px');
    // jsdom may return url("thumb.jpg") or url(thumb.jpg)
    expect(clone.style.backgroundImage.replace(/\"/g, '')).toBe('url(thumb.jpg)');
  });

  it('copies the border-radius from getComputedStyle of the source', () => {
    jest
      .spyOn(window, 'getComputedStyle')
      .mockReturnValue({ borderRadius: '12px' } as unknown as CSSStyleDeclaration);
    makeSource({}, 'url("r.jpg")');
    const cloneRef = makeCloneRef();
    renderHook(() => useActiveThumbnailRect(cloneRef, null));

    flush();

    expect(cloneRef.current!.style.borderRadius).toBe('12px');
  });

  it('hides the clone when the source element has no backgroundImage yet', () => {
    makeSource({ top: 10, left: 10, width: 80, height: 80 }, '');
    const cloneRef = makeCloneRef();
    renderHook(() => useActiveThumbnailRect(cloneRef, null));

    flush();

    expect(cloneRef.current!.style.display).toBe('none');
  });

  it('updates the clone on every RAF tick, tracking position changes', () => {
    const source = makeSource({ top: 10, left: 10, width: 100, height: 100 }, 'url("x.jpg")');
    const cloneRef = makeCloneRef();
    renderHook(() => useActiveThumbnailRect(cloneRef, null));

    flush(); // first tick
    expect(cloneRef.current!.style.top).toBe('10px');

    // Simulate the element moving (e.g. carousel sliding).
    (source.getBoundingClientRect as jest.Mock).mockReturnValue({
      top: 200,
      left: 400,
      width: 100,
      height: 100,
      bottom: 300,
      right: 500,
      x: 400,
      y: 200,
      toJSON: jest.fn(),
    });

    flush(); // second tick
    expect(cloneRef.current!.style.top).toBe('200px');
    expect(cloneRef.current!.style.left).toBe('400px');
  });

  describe('when multiple candidates exist (infinite-scroll clones)', () => {
    it('picks the element whose horizontal centre falls inside the viewport', () => {
      // jsdom viewport is 1024×768.  The off-screen clone's centre is at –150px.
      const offScreen = document.createElement('div');
      offScreen.setAttribute('data-adl-thumbnail-active', 'true');
      offScreen.style.backgroundImage = 'url("wrong.jpg")';
      offScreen.getBoundingClientRect = jest.fn(
        () =>
          ({
            left: -200,
            width: 100,
            top: 0,
            height: 100,
            bottom: 100,
            right: -100,
            x: -200,
            y: 0,
            toJSON: jest.fn(),
          }) as DOMRect,
      );
      document.body.appendChild(offScreen);

      // The real visible slide's centre is at 250px — inside the viewport.
      const onScreen = document.createElement('div');
      onScreen.setAttribute('data-adl-thumbnail-active', 'true');
      onScreen.style.backgroundImage = 'url("correct.jpg")';
      onScreen.getBoundingClientRect = jest.fn(
        () =>
          ({
            left: 200,
            width: 100,
            top: 10,
            height: 100,
            bottom: 110,
            right: 300,
            x: 200,
            y: 10,
            toJSON: jest.fn(),
          }) as DOMRect,
      );
      document.body.appendChild(onScreen);

      const cloneRef = makeCloneRef();
      renderHook(() => useActiveThumbnailRect(cloneRef, null));
      flush();

  // jsdom may return url("correct.jpg") or url(correct.jpg)
  expect(cloneRef.current!.style.backgroundImage.replace(/\"/g, '')).toBe('url(correct.jpg)');
  expect(cloneRef.current!.style.left).toBe('200px');
    });
  });

  it('cancels the animation frame when the hook unmounts', () => {
    const cloneRef = makeCloneRef();
    const { unmount } = renderHook(() => useActiveThumbnailRect(cloneRef, null));
    unmount();
    expect(window.cancelAnimationFrame).toHaveBeenCalled();
  });

  it('cancels the previous loop and starts a new one when dep changes', () => {
    const cancelSpy = window.cancelAnimationFrame as jest.Mock;
    const cloneRef = makeCloneRef();

    const { rerender } = renderHook(
      ({ dep }: { dep: unknown }) => useActiveThumbnailRect(cloneRef, dep),
      { initialProps: { dep: 'post-a' } },
    );

    const callsBefore = cancelSpy.mock.calls.length;
    act(() => rerender({ dep: 'post-b' }));

    // The effect cleanup must have fired cancelAnimationFrame.
    expect(cancelSpy.mock.calls.length).toBeGreaterThan(callsBefore);
  });
});
