import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import { useIsMobile } from './useIsMobile';

/**
 * Mobile-A2 (review item 5): useIsMobile is the single source of truth every
 * mobile-only branch (bottom tab bar, collapsed topbar, bottom sheets) reads
 * from, so it needs direct coverage of: (a) it reflects matchMedia's
 * `matches`, (b) the `change` listener is registered AND cleaned up on
 * unmount (a leaked listener would keep re-rendering an unmounted tree /
 * leak the component instance), and (c) the legacy Safari<14
 * addListener/removeListener fallback is used when addEventListener isn't
 * available.
 *
 * DOM-harness pattern mirrors PageContext.pure-updaters.test.tsx /
 * useKeyboardShortcuts.test.tsx: bare createRoot + act, no @testing-library
 * (not a project dependency).
 */

let container: HTMLDivElement;
let root: Root;
let originalMatchMedia: typeof window.matchMedia;

function render(ui: React.ReactElement) {
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
    root.render(ui);
  });
}

function unmount() {
  act(() => {
    root.unmount();
  });
  container.remove();
}

/** A minimal fake MediaQueryList. `mode` picks which listener API it
 *  exposes, so tests can force the legacy fallback path. */
function makeMql(matches: boolean, mode: 'modern' | 'legacy' = 'modern') {
  const listeners: Array<() => void> = [];
  const mql: any = {
    matches,
    media: '(max-width: 768px)',
  };
  if (mode === 'modern') {
    mql.addEventListener = vi.fn((_type: string, fn: () => void) => listeners.push(fn));
    mql.removeEventListener = vi.fn((_type: string, fn: () => void) => {
      const i = listeners.indexOf(fn);
      if (i !== -1) listeners.splice(i, 1);
    });
  } else {
    mql.addListener = vi.fn((fn: () => void) => listeners.push(fn));
    mql.removeListener = vi.fn((fn: () => void) => {
      const i = listeners.indexOf(fn);
      if (i !== -1) listeners.splice(i, 1);
    });
  }
  return { mql, fire: () => listeners.forEach((fn) => fn()), listenerCount: () => listeners.length };
}

let probedValue: boolean | null = null;
const Probe: React.FC = () => {
  probedValue = useIsMobile();
  return <span data-value={String(probedValue)} />;
};

beforeEach(() => {
  originalMatchMedia = window.matchMedia;
  probedValue = null;
});

afterEach(() => {
  window.matchMedia = originalMatchMedia;
});

describe('useIsMobile', () => {
  test('returns true when the mobile media query matches', () => {
    const { mql } = makeMql(true);
    window.matchMedia = vi.fn(() => mql) as any;

    render(<Probe />);
    expect(probedValue).toBe(true);
    unmount();
  });

  test('returns false when the mobile media query does not match', () => {
    const { mql } = makeMql(false);
    window.matchMedia = vi.fn(() => mql) as any;

    render(<Probe />);
    expect(probedValue).toBe(false);
    unmount();
  });

  test('registers the change listener via addEventListener and updates on change', () => {
    const { mql, fire, listenerCount } = makeMql(false);
    window.matchMedia = vi.fn(() => mql) as any;

    render(<Probe />);
    expect(probedValue).toBe(false);
    expect(mql.addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
    expect(listenerCount()).toBe(1);

    // Flip the query result and fire the registered 'change' listener --
    // the hook must re-read mql.matches, not just toggle blindly.
    mql.matches = true;
    act(() => {
      fire();
    });
    expect(probedValue).toBe(true);

    unmount();
  });

  test('cleans up the addEventListener listener on unmount', () => {
    const { mql, listenerCount } = makeMql(true);
    window.matchMedia = vi.fn(() => mql) as any;

    render(<Probe />);
    expect(listenerCount()).toBe(1);

    unmount();

    expect(mql.removeEventListener).toHaveBeenCalledWith('change', expect.any(Function));
    expect(listenerCount()).toBe(0);
  });

  test('falls back to legacy addListener/removeListener when addEventListener is unavailable', () => {
    const { mql, fire, listenerCount } = makeMql(false, 'legacy');
    window.matchMedia = vi.fn(() => mql) as any;

    render(<Probe />);
    expect(probedValue).toBe(false);
    expect(mql.addListener).toHaveBeenCalledWith(expect.any(Function));
    expect(listenerCount()).toBe(1);

    mql.matches = true;
    act(() => {
      fire();
    });
    expect(probedValue).toBe(true);

    unmount();

    expect(mql.removeListener).toHaveBeenCalledWith(expect.any(Function));
    expect(listenerCount()).toBe(0);
  });
});
