/* @vitest-environment jsdom */

import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { usePullToRefresh } from "../use-pull-to-refresh";

function Harness({
  disabled = false,
  refreshing,
  onRefresh,
}: {
  disabled?: boolean;
  refreshing?: boolean;
  onRefresh: () => Promise<void> | void;
}) {
  const pull = usePullToRefresh(() => onRefresh(), {
    disabled,
    refreshing,
  });
  return (
    <div
      aria-label="Pull container"
      data-distance={pull.pullDistance}
      data-pulling={pull.isPulling}
      {...pull.touchHandlers}
    />
  );
}

describe("usePullToRefresh", () => {
  let container: HTMLDivElement;
  let root: Root;

  beforeEach(() => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
      .IS_REACT_ACT_ENVIRONMENT = true;
    container = document.createElement("div");
    document.body.append(container);
    root = createRoot(container);
  });

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

  it("triggers once after pulling beyond the threshold", () => {
    const onRefresh = vi.fn();
    act(() => root.render(<Harness onRefresh={onRefresh} />));
    const target = getTarget(container);

    pull(target, 180);

    expect(onRefresh).toHaveBeenCalledOnce();
  });

  it("does not trigger below the threshold or while disabled", () => {
    const onRefresh = vi.fn();
    act(() => root.render(<Harness onRefresh={onRefresh} />));
    pull(getTarget(container), 80);
    expect(onRefresh).not.toHaveBeenCalled();

    act(() =>
      root.render(<Harness disabled onRefresh={onRefresh} />),
    );
    pull(getTarget(container), 180);
    expect(onRefresh).not.toHaveBeenCalled();
  });

  it("blocks another gesture while refresh is externally controlled", () => {
    const onRefresh = vi.fn();
    act(() =>
      root.render(<Harness refreshing onRefresh={onRefresh} />),
    );

    pull(getTarget(container), 180);

    expect(onRefresh).not.toHaveBeenCalled();
  });
});

function getTarget(container: HTMLElement): HTMLDivElement {
  const target = container.querySelector<HTMLDivElement>(
    '[aria-label="Pull container"]',
  );
  if (!target) throw new Error("Pull container was not rendered");
  return target;
}

function pull(target: HTMLDivElement, distance: number): void {
  act(() => {
    target.dispatchEvent(createTouchEvent("touchstart", 0));
    target.dispatchEvent(createTouchEvent("touchmove", distance));
    target.dispatchEvent(createTouchEvent("touchend", distance, true));
  });
}

function createTouchEvent(
  type: string,
  clientY: number,
  ended = false,
): Event {
  const event = new Event(type, { bubbles: true, cancelable: true });
  Object.defineProperty(event, "touches", {
    value: ended ? [] : [{ identifier: 1, clientY }],
  });
  return event;
}
