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

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

const makeContainerRef = (el: HTMLElement) => ({ current: el as HTMLElement | null });

// Execute rAF callbacks synchronously so focus side-effects in the hook are
// visible immediately without needing fake timers.
beforeEach(() => {
  jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
    cb(0);
    return 0;
  });
  jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {});
});

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

// ---------------------------------------------------------------------------
// Suites
// ---------------------------------------------------------------------------

describe('useDialogFocusTrap – inactive', () => {
  it('does not attach a keydown listener on document', () => {
    const container = document.createElement('div');
    document.body.appendChild(container);

    const spy = jest.spyOn(document, 'addEventListener');
    renderHook(() =>
      useDialogFocusTrap({ active: false, containerRef: makeContainerRef(container) }),
    );

    expect(spy).not.toHaveBeenCalledWith('keydown', expect.any(Function));

    document.body.removeChild(container);
  });

  it('does not change focus when inactive', () => {
    const outsideBtn = document.createElement('button');
    document.body.appendChild(outsideBtn);
    outsideBtn.focus();

    const container = document.createElement('div');
    document.body.appendChild(container);

    renderHook(() =>
      useDialogFocusTrap({ active: false, containerRef: makeContainerRef(container) }),
    );

    expect(document.activeElement).toBe(outsideBtn);

    document.body.removeChild(outsideBtn);
    document.body.removeChild(container);
  });
});

describe('useDialogFocusTrap – active', () => {
  let container: HTMLDivElement;

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('tabindex', '-1');
    document.body.appendChild(container);
  });

  afterEach(() => {
    // Guard: element may have been removed by a test already
    if (document.body.contains(container)) {
      document.body.removeChild(container);
    }
  });

  // ── Initial focus ──────────────────────────────────────────────────────────

  it('focuses the first focusable element inside the container on activation', () => {
    const btn = document.createElement('button');
    container.appendChild(btn);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    expect(document.activeElement).toBe(btn);
  });

  it('focuses the container itself when it contains no focusable elements', () => {
    const focusSpy = jest.spyOn(container, 'focus');

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    expect(focusSpy).toHaveBeenCalled();
  });

  it('skips elements with aria-hidden="true"', () => {
    const btn1 = document.createElement('button');
    btn1.setAttribute('aria-hidden', 'true');
    const btn2 = document.createElement('button');
    container.appendChild(btn1);
    container.appendChild(btn2);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    // btn1 is hidden from AT, so btn2 should receive focus first
    expect(document.activeElement).toBe(btn2);
  });

  // ── Tab key trap ───────────────────────────────────────────────────────────

  it('wraps Tab forward: last → first focusable element', () => {
    const btn1 = document.createElement('button');
    const btn2 = document.createElement('button');
    container.appendChild(btn1);
    container.appendChild(btn2);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    act(() => { btn2.focus(); });
    expect(document.activeElement).toBe(btn2);

    const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
    const preventDefaultSpy = jest.spyOn(tabEvent, 'preventDefault');

    act(() => { document.dispatchEvent(tabEvent); });

    expect(preventDefaultSpy).toHaveBeenCalled();
    expect(document.activeElement).toBe(btn1);
  });

  it('wraps Shift+Tab backward: first → last focusable element', () => {
    const btn1 = document.createElement('button');
    const btn2 = document.createElement('button');
    container.appendChild(btn1);
    container.appendChild(btn2);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    act(() => { btn1.focus(); });
    expect(document.activeElement).toBe(btn1);

    const shiftTabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true });
    const preventDefaultSpy = jest.spyOn(shiftTabEvent, 'preventDefault');

    act(() => { document.dispatchEvent(shiftTabEvent); });

    expect(preventDefaultSpy).toHaveBeenCalled();
    expect(document.activeElement).toBe(btn2);
  });

  it('ignores non-Tab keydown events', () => {
    const btn1 = document.createElement('button');
    const btn2 = document.createElement('button');
    container.appendChild(btn1);
    container.appendChild(btn2);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    act(() => { btn1.focus(); });

    const escEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
    const preventDefaultSpy = jest.spyOn(escEvent, 'preventDefault');
    act(() => { document.dispatchEvent(escEvent); });

    // Focus should stay on btn1 — Escape is not handled by the trap
    expect(document.activeElement).toBe(btn1);
    expect(preventDefaultSpy).not.toHaveBeenCalled();
  });

  it('Tab with no focusable elements focuses the container root', () => {
    const focusSpy = jest.spyOn(container, 'focus');
    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
    const preventDefaultSpy = jest.spyOn(tabEvent, 'preventDefault');
    act(() => { document.dispatchEvent(tabEvent); });

    expect(preventDefaultSpy).toHaveBeenCalled();
    expect(focusSpy).toHaveBeenCalled();
  });

  // ── focusin outside redirect ───────────────────────────────────────────────

  it('redirects focus back inside when an element outside the dialog gains focus', () => {
    const btn = document.createElement('button');
    container.appendChild(btn);

    const outsideBtn = document.createElement('button');
    document.body.appendChild(outsideBtn);

    renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    // Cause focus to leave the dialog — jsdom fires "focusin" on document
    act(() => { outsideBtn.focus(); });

    expect(document.activeElement).toBe(btn);

    document.body.removeChild(outsideBtn);
  });

  // ── Focus restoration on cleanup ──────────────────────────────────────────

  it('restores focus to the previously focused element when the hook cleans up', () => {
    const previousBtn = document.createElement('button');
    document.body.appendChild(previousBtn);
    previousBtn.focus();

    const btn = document.createElement('button');
    container.appendChild(btn);

    const { unmount } = renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    // Trap should have shifted focus to btn
    expect(document.activeElement).toBe(btn);

    act(() => { unmount(); });

    expect(document.activeElement).toBe(previousBtn);

    document.body.removeChild(previousBtn);
  });

  it('does not throw if the previously focused element was removed from the DOM before cleanup', () => {
    const btn = document.createElement('button');
    container.appendChild(btn);

    const { unmount } = renderHook(() =>
      useDialogFocusTrap({ active: true, containerRef: makeContainerRef(container) }),
    );

    // No element was focused beforehand → previouslyFocused is body/null,
    // which is always in the document, so unmount must never throw.
    expect(() => { act(() => { unmount(); }); }).not.toThrow();
  });
});
