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 { useVisualViewportInsets } from './useVisualViewport';

/**
 * Fast-follow coverage for `useVisualViewportInsets` (BottomSheet's
 * keyboard-avoidance source of truth). Mirrors the bare createRoot + act
 * harness `useIsMobile.test.tsx` uses (no @testing-library/react in this
 * repo). Covers:
 *  - the no-`visualViewport` fallback (older WebViews) returns
 *    `{ height: window.innerHeight, keyboardInset: 0 }` AND registers no
 *    listener (there's nothing to listen to).
 *  - the keyboard-open math: `keyboardInset = innerHeight - (vv.height +
 *    vv.offsetTop)`, recomputed when the mocked vv fires `resize`.
 *  - both `resize` and `scroll` listeners are added on mount and the SAME
 *    handler is removed for both on unmount (a leaked listener would keep
 *    reacting after the owning component -- e.g. BottomSheet -- is gone).
 */

let container: HTMLDivElement;
let root: Root;
let originalInnerHeight: number;
let originalVisualViewport: VisualViewport | null;

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 `visualViewport`: just enough surface for the hook
 *  (`height`, `offsetTop`, `addEventListener`/`removeEventListener`). */
function makeVisualViewport(height: number, offsetTop: number) {
  const listeners: Record<string, Array<() => void>> = { resize: [], scroll: [] };
  const vv: any = {
    height,
    offsetTop,
    addEventListener: vi.fn((type: string, fn: () => void) => {
      listeners[type].push(fn);
    }),
    removeEventListener: vi.fn((type: string, fn: () => void) => {
      const i = listeners[type].indexOf(fn);
      if (i !== -1) listeners[type].splice(i, 1);
    }),
  };
  return {
    vv,
    fire: (type: 'resize' | 'scroll') => listeners[type].forEach((fn) => fn()),
    listenerCount: (type: 'resize' | 'scroll') => listeners[type].length,
  };
}

let probed: { height: number; keyboardInset: number } | null = null;
const Probe: React.FC = () => {
  probed = useVisualViewportInsets();
  return <span data-height={probed.height} data-inset={probed.keyboardInset} />;
};

beforeEach(() => {
  originalInnerHeight = window.innerHeight;
  originalVisualViewport = window.visualViewport;
  probed = null;
});

afterEach(() => {
  Object.defineProperty(window, 'innerHeight', {
    value: originalInnerHeight,
    configurable: true,
    writable: true,
  });
  Object.defineProperty(window, 'visualViewport', {
    value: originalVisualViewport,
    configurable: true,
  });
});

describe('useVisualViewportInsets', () => {
  test('falls back to innerHeight/0 and registers no listener when visualViewport is undefined', () => {
    Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true, writable: true });
    Object.defineProperty(window, 'visualViewport', { value: undefined, configurable: true });

    render(<Probe />);

    expect(probed).toEqual({ height: 800, keyboardInset: 0 });

    unmount();
  });

  test('computes keyboardInset from visualViewport and recomputes on resize', () => {
    Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true, writable: true });
    const { vv, fire } = makeVisualViewport(800, 0);
    Object.defineProperty(window, 'visualViewport', { value: vv, configurable: true });

    render(<Probe />);

    // No keyboard open: vv fills the layout viewport exactly.
    expect(probed).toEqual({ height: 800, keyboardInset: 0 });

    // Simulate the on-screen keyboard opening: the visual viewport shrinks.
    vv.height = 500;
    vv.offsetTop = 0;
    act(() => {
      fire('resize');
    });

    expect(probed).toEqual({ height: 500, keyboardInset: 300 });

    unmount();
  });

  test('registers resize and scroll listeners on mount and removes both (same handler) on unmount', () => {
    Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true, writable: true });
    const { vv, listenerCount } = makeVisualViewport(800, 0);
    Object.defineProperty(window, 'visualViewport', { value: vv, configurable: true });

    render(<Probe />);

    expect(vv.addEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
    expect(vv.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
    expect(listenerCount('resize')).toBe(1);
    expect(listenerCount('scroll')).toBe(1);

    const resizeHandler = vv.addEventListener.mock.calls.find((c: any[]) => c[0] === 'resize')[1];
    const scrollHandler = vv.addEventListener.mock.calls.find((c: any[]) => c[0] === 'scroll')[1];
    expect(resizeHandler).toBe(scrollHandler);

    unmount();

    expect(vv.removeEventListener).toHaveBeenCalledWith('resize', resizeHandler);
    expect(vv.removeEventListener).toHaveBeenCalledWith('scroll', scrollHandler);
    expect(listenerCount('resize')).toBe(0);
    expect(listenerCount('scroll')).toBe(0);
  });
});
