import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ref, type ShallowRef } from 'vue';
import { useFloatingItem } from './use-floating-item.composable';

describe('useFloatingItem', () => {
  let trigger: ShallowRef<HTMLElement | null>;
  let floating: ShallowRef<HTMLElement | null>;
  let triggerEl: HTMLElement;
  let floatingEl: HTMLElement;
  let childButton: HTMLButtonElement;

  beforeEach(() => {
    triggerEl = document.createElement('button');
    floatingEl = document.createElement('div');
    childButton = document.createElement('button');
    childButton.textContent = 'item';
    floatingEl.appendChild(childButton);

    // start hidden
    floatingEl.classList.add('mc-sidebar__floating-item--hidden');

    // attach to DOM so focus and events behave normally
    document.body.appendChild(triggerEl);
    document.body.appendChild(floatingEl);

    trigger = ref(triggerEl);
    floating = ref(floatingEl);
  });

  afterEach(() => {
    vi.restoreAllMocks();
    document.body.innerHTML = '';
  });

  it('showFloatingItem sets position, removes hidden class, sets flag and adds mousedown listener', () => {
    const { showFloatingItem, floatingItemIsDisplayed } = useFloatingItem(
      trigger,
      floating,
    );

    // ensure getBoundingClientRect returns a known top
    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 42,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    const addSpy = vi.spyOn(document, 'addEventListener');

    showFloatingItem();
    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(false);
    expect(floatingItemIsDisplayed.value).toBe(true);
    expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
  });

  it('hideFloatingItem adds hidden class, clears flag and removes mousedown listener', () => {
    const { showFloatingItem, hideFloatingItem, floatingItemIsDisplayed } =
      useFloatingItem(trigger, floating);

    // make visible first
    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 1,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);
    const removeSpy = vi.spyOn(document, 'removeEventListener');

    showFloatingItem();
    // sanity
    expect(floatingItemIsDisplayed.value).toBe(true);
    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(false);

    hideFloatingItem();

    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(true);
    expect(floatingItemIsDisplayed.value).toBe(false);
    expect(removeSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
  });

  it('hideFloatingItem does not hide when relatedTarget is inside floating or trigger if allowItemHover true', () => {
    const { showFloatingItem, hideFloatingItem, floatingItemIsDisplayed } =
      useFloatingItem(trigger, floating, { allowItemHover: true });

    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 1,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    showFloatingItem();
    expect(floatingItemIsDisplayed.value).toBe(true);

    // simulate relatedTarget inside floating
    const fakeEvent = {
      relatedTarget: childButton,
    } as unknown as FocusEvent;

    hideFloatingItem(fakeEvent);
    expect(floatingItemIsDisplayed.value).toBe(true);
    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(false);
  });

  it('clicking outside after showFloatingItem hides the floating item', () => {
    const result = useFloatingItem(trigger, floating);
    const { showFloatingItem, floatingItemIsDisplayed } = result;

    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 5,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    showFloatingItem();
    expect(floatingItemIsDisplayed.value).toBe(true);

    // Dispatch mousedown on an outside element
    const outside = document.createElement('div');
    document.body.appendChild(outside);

    const event = new MouseEvent('mousedown', { bubbles: true });
    // dispatch on outside so event.target is outside
    outside.dispatchEvent(event);

    // after event loop, listener should have executed synchronously
    expect(floatingItemIsDisplayed.value).toBe(false);
    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(true);
  });

  it('onTriggerKeydown opens on ArrowDown/Enter/Space and focuses first listbox item', () => {
    const { onTriggerKeydown, floatingItemIsDisplayed } = useFloatingItem(
      trigger,
      floating,
    );

    // ensure getBoundingClientRect returns value so showFloatingItem proceeds
    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 2,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    const keyEvent = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    const preventSpy = vi.spyOn(keyEvent, 'preventDefault');

    const focusSpy = vi.spyOn(childButton, 'focus');

    onTriggerKeydown(keyEvent);
    expect(preventSpy).toHaveBeenCalled();
    expect(floatingItemIsDisplayed.value).toBe(true);
    expect(focusSpy).toHaveBeenCalled();
  });

  it('onTriggerKeydown with Tab hides the floating item', () => {
    const { onTriggerKeydown, showFloatingItem, floatingItemIsDisplayed } =
      useFloatingItem(trigger, floating);

    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 3,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    showFloatingItem();
    expect(floatingItemIsDisplayed.value).toBe(true);

    const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' });
    onTriggerKeydown(tabEvent);

    expect(floatingItemIsDisplayed.value).toBe(false);
    expect(
      floatingEl.classList.contains('mc-sidebar__floating-item--hidden'),
    ).toBe(true);
  });

  it('onListboxKeydown Escape hides and focuses trigger', () => {
    const { onListboxKeydown, showFloatingItem, floatingItemIsDisplayed } =
      useFloatingItem(trigger, floating);

    vi.spyOn(triggerEl, 'getBoundingClientRect').mockReturnValue({
      top: 6,
      left: 0,
      bottom: 0,
      right: 0,
      width: 0,
      height: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    } as unknown as DOMRect);

    const triggerFocusSpy = vi.spyOn(triggerEl, 'focus');

    showFloatingItem();
    expect(floatingItemIsDisplayed.value).toBe(true);

    const escEvent = new KeyboardEvent('keydown', { key: 'Escape' });
    const preventSpy = vi.spyOn(escEvent, 'preventDefault');

    onListboxKeydown(escEvent);

    expect(preventSpy).toHaveBeenCalled();
    expect(floatingItemIsDisplayed.value).toBe(false);
    expect(triggerFocusSpy).toHaveBeenCalled();
  });
});
