import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
  waitFor,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { QuestionSnapList } from "./question-snap-list";
import {
  computeQuestionLift,
  updateQuestionKeyboardHeight,
} from "./question-viewport-coordinator";

vi.mock("@/translations/provider", () => ({
  useI18n: () => ({ dictionary: {} }),
}));

describe("QuestionSnapList keyboard interaction", () => {
  afterEach(() => {
    cleanup();
    document.body.classList.remove("question-keyboard-open");
    document.documentElement.style.removeProperty("--question-lift-y");
  });

  it("computes a stable occlusion lift without overshoot", () => {
    expect(
      computeQuestionLift({
        baseTop: 300,
        baseBottom: 600,
        visibleTop: 100,
        visibleBottom: 500,
      }),
    ).toBe(150);
    expect(
      computeQuestionLift({
        baseTop: 300,
        baseBottom: 600,
        visibleTop: 100,
        visibleBottom: 800,
      }),
    ).toBe(0);
  });

  it("does not treat pointer contact as input focus", () => {
    render(
      <QuestionSnapList>
        <input aria-label="Question one" type="text" />
      </QuestionSnapList>,
    );

    fireEvent.pointerDown(
      screen.getByRole("textbox", { name: "Question one" }),
    );

    expect(document.body).not.toHaveClass("question-keyboard-open");
  });

  it("activates only on real focus and resets before changing questions", async () => {
    render(
      <QuestionSnapList>
        <input aria-label="Question one" type="text" />
        <input aria-label="Question two" type="text" />
      </QuestionSnapList>,
    );

    const firstInput = screen.getByRole("textbox", { name: "Question one" });
    const secondInput = document.querySelector<HTMLInputElement>(
      'input[aria-label="Question two"]',
    );
    const content = firstInput.closest<HTMLElement>(".question-snap-content");
    vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({
      top: 300,
      bottom: 600,
      left: 0,
      right: 300,
      width: 300,
      height: 300,
      x: 0,
      y: 300,
      toJSON: () => ({}),
    });

    firstInput.focus();
    updateQuestionKeyboardHeight(300);
    await waitFor(() => {
      expect(document.body).toHaveClass("question-keyboard-open");
    });

    fireEvent.wheel(screen.getByRole("region", { name: "Questions" }), {
      deltaY: 100,
    });

    await waitFor(() => {
      expect(document.body).not.toHaveClass("question-keyboard-open");
      expect(firstInput).not.toHaveFocus();
      expect(secondInput).not.toHaveFocus();
      expect(
        document.documentElement.style.getPropertyValue("--question-lift-y"),
      ).toBe("0px");
    });
  });

  it("does not keep question keyboard state for a dialog input", async () => {
    render(
      <>
        <QuestionSnapList>
          <input aria-label="Question input" type="text" />
        </QuestionSnapList>
        <div role="dialog">
          <input aria-label="Dialog search" type="text" />
        </div>
      </>,
    );

    const questionInput = screen.getByRole("textbox", {
      name: "Question input",
    });
    const content = questionInput.closest<HTMLElement>(
      ".question-snap-content",
    );
    vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({
      top: 300,
      bottom: 600,
      left: 0,
      right: 300,
      width: 300,
      height: 300,
      x: 0,
      y: 300,
      toJSON: () => ({}),
    });

    questionInput.focus();
    updateQuestionKeyboardHeight(300);
    await waitFor(() => {
      expect(document.body).toHaveClass("question-keyboard-open");
    });

    screen.getByRole("textbox", { name: "Dialog search" }).focus();

    await waitFor(() => {
      expect(document.body).not.toHaveClass("question-keyboard-open");
    });
  });

  it("raises again when Flutter reopens the keyboard without another focusin", async () => {
    render(
      <QuestionSnapList>
        <input aria-label="Persistent input" type="text" />
      </QuestionSnapList>,
    );

    const input = screen.getByRole("textbox", { name: "Persistent input" });
    const content = input.closest<HTMLElement>(".question-snap-content");
    vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({
      top: 300,
      bottom: 600,
      left: 0,
      right: 300,
      width: 300,
      height: 300,
      x: 0,
      y: 300,
      toJSON: () => ({}),
    });

    input.focus();
    updateQuestionKeyboardHeight(300);
    await waitFor(() =>
      expect(document.body).toHaveClass("question-keyboard-open"),
    );

    updateQuestionKeyboardHeight(0);
    await waitFor(() => {
      expect(document.body).not.toHaveClass("question-keyboard-open");
      expect(input).toHaveFocus();
    });

    updateQuestionKeyboardHeight(300);
    await waitFor(() => {
      expect(document.body).toHaveClass("question-keyboard-open");
      expect(input).toHaveFocus();
    });
  });

  describe("touch isolation and first-tap focus reliability", () => {
    it("does not step question or blur active input when touch has jitter on input", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <label htmlFor="name-input">Full Name</label>
            <input id="name-input" aria-label="Full Name" type="text" />
          </div>
          <div>
            <input aria-label="Second Question" type="text" />
          </div>
        </QuestionSnapList>,
      );

      const input = screen.getByRole("textbox", { name: "Full Name" });
      input.focus();
      expect(input).toHaveFocus();

      // Finger touches input with 12px jitter/drift (typical coarse touch contact area shift)
      fireEvent.touchStart(input, {
        touches: [{ clientY: 300 }],
        target: input,
      });
      fireEvent.touchMove(input, {
        touches: [{ clientY: 288 }],
        target: input,
      });
      fireEvent.touchEnd(input, {
        changedTouches: [{ clientY: 288 }],
        target: input,
      });

      // Input should remain focused, and active question must remain index 0
      expect(input).toHaveFocus();
      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);
    });

    it("does not engage drag or blur input when touched while snap animation is settling", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <textarea aria-label="Bio description" />
          </div>
          <div>
            <textarea aria-label="Second Bio" />
          </div>
        </QuestionSnapList>,
      );

      const textarea = screen.getByRole("textbox", {
        name: "Bio description",
      });
      textarea.focus();

      // Simulate touching during animation or right as question is rendered
      fireEvent.touchStart(textarea, {
        touches: [{ clientY: 400 }],
        target: textarea,
      });
      fireEvent.touchEnd(textarea, {
        changedTouches: [{ clientY: 390 }],
        target: textarea,
      });

      expect(textarea).toHaveFocus();
      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);
    });

    it("does not step question on option button/label with micro-jitter (clean tap)", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <button type="button">Select Option</button>
          </div>
          <div>
            <button type="button">Next Step</button>
          </div>
        </QuestionSnapList>,
      );

      const button = screen.getByRole("button", { name: "Select Option" });

      // Small jitter (delta 8px < INTERACTIVE_DRAG_SLOP 16px)
      fireEvent.touchStart(button, {
        touches: [{ clientX: 100, clientY: 250 }],
        target: button,
      });
      fireEvent.touchMove(button, {
        touches: [{ clientX: 100, clientY: 242 }],
        target: button,
      });
      fireEvent.touchEnd(button, {
        changedTouches: [{ clientX: 100, clientY: 242 }],
        target: button,
      });

      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);
    });

    it("allows intentional vertical drag starting on a label/button to swipe questions (soft ownership)", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <label htmlFor="opt-1">Option 1</label>
            <input id="opt-1" type="radio" name="opt" />
          </div>
          <div>
            <label htmlFor="opt-2">Option 2</label>
            <input id="opt-2" type="radio" name="opt" />
          </div>
        </QuestionSnapList>,
      );

      const label = screen.getByText("Option 1");

      // Intentional drag (startY: 300, endY: 200 -> deltaY = 100 > slop)
      fireEvent.touchStart(label, {
        touches: [{ clientX: 100, clientY: 300 }],
        target: label,
      });
      fireEvent.touchMove(label, {
        touches: [{ clientX: 100, clientY: 200 }],
        target: label,
      });
      fireEvent.touchEnd(label, {
        changedTouches: [{ clientX: 100, clientY: 200 }],
        target: label,
      });

      expect(onActiveIndexChange).toHaveBeenCalledWith(1);
    });

    it("suppresses trailing click on option after vertical drag swipe", () => {
      const onClick = vi.fn();
      render(
        <QuestionSnapList>
          <div>
            <button type="button" onClick={onClick}>
              Option Button
            </button>
          </div>
          <div>Slide 2</div>
        </QuestionSnapList>,
      );

      const button = screen.getByRole("button", { name: "Option Button" });
      const region = screen.getByRole("region", { name: "Questions" });

      // Swipe drag starting on button
      fireEvent.touchStart(button, {
        touches: [{ clientX: 100, clientY: 300 }],
        target: button,
      });
      fireEvent.touchMove(button, {
        touches: [{ clientX: 100, clientY: 200 }],
        target: button,
      });
      fireEvent.touchEnd(button, {
        changedTouches: [{ clientX: 100, clientY: 200 }],
        target: button,
      });

      // Synthetic click dispatched after drag
      fireEvent.click(button);

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

    it("allows intentional vertical swipe on non-interactive question background", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div data-testid="slide-1">Slide 1</div>
          <div data-testid="slide-2">Slide 2</div>
        </QuestionSnapList>,
      );

      const region = screen.getByRole("region", { name: "Questions" });

      // Fast upward flick on background area
      fireEvent.touchStart(region, {
        touches: [{ clientX: 100, clientY: 400 }],
        target: region,
      });
      fireEvent.touchEnd(region, {
        changedTouches: [{ clientX: 100, clientY: 320 }],
        target: region,
      });

      expect(onActiveIndexChange).toHaveBeenCalledWith(1);
    });

    it("does not swipe or blur when touching and dragging inside a text input", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <input aria-label="First Name" type="text" />
          </div>
          <div>
            <input aria-label="Last Name" type="text" />
          </div>
        </QuestionSnapList>,
      );

      const input = screen.getByRole("textbox", { name: "First Name" });
      input.focus();

      // Large drag on input (e.g. user selecting text or scrolling within text input)
      fireEvent.touchStart(input, {
        touches: [{ clientX: 100, clientY: 400 }],
        target: input,
      });
      fireEvent.touchMove(input, {
        touches: [{ clientX: 100, clientY: 200 }],
        target: input,
      });
      fireEvent.touchEnd(input, {
        changedTouches: [{ clientX: 100, clientY: 200 }],
        target: input,
      });

      expect(input).toHaveFocus();
      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);
    });

    it("does not swipe on hard-ignored controls like range sliders", () => {
      const onActiveIndexChange = vi.fn();
      render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <input type="range" aria-label="Volume slider" />
          </div>
          <div>Slide 2</div>
        </QuestionSnapList>,
      );

      const slider = screen.getByRole("slider", { name: "Volume slider" });

      fireEvent.touchStart(slider, {
        touches: [{ clientX: 100, clientY: 300 }],
        target: slider,
      });
      fireEvent.touchMove(slider, {
        touches: [{ clientX: 100, clientY: 200 }],
        target: slider,
      });
      fireEvent.touchEnd(slider, {
        changedTouches: [{ clientX: 100, clientY: 200 }],
        target: slider,
      });

      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);
    });
  });

  describe("Periodic scroll hint idle cycle", () => {
    it("follows 2s idle -> 2s visible -> 2s hidden -> repeat cycle and resets on user interaction", () => {
      vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });

      const { container } = render(
        <QuestionSnapList
          firstQuestionHint={<span data-testid="scroll-hint">Scroll icon</span>}
        >
          <div>Question 1</div>
          <div>Question 2</div>
        </QuestionSnapList>,
      );

      const hintWrapper = container.querySelector(".motion-safe\\:animate-bounce");
      expect(hintWrapper).not.toBeNull();

      // Initially hidden (waiting 2s)
      expect(hintWrapper).toHaveClass("opacity-0");
      expect(hintWrapper).not.toHaveClass("opacity-100");

      // Advance 1s: still hidden
      act(() => {
        vi.advanceTimersByTime(1000);
      });
      expect(hintWrapper).toHaveClass("opacity-0");

      // Advance another 1s (total 2s idle): now visible
      act(() => {
        vi.advanceTimersByTime(1000);
      });
      expect(hintWrapper).toHaveClass("opacity-100");

      // Advance 2s while visible (total 4s): becomes hidden
      act(() => {
        vi.advanceTimersByTime(2000);
      });
      expect(hintWrapper).toHaveClass("opacity-0");

      // Advance 2s while hidden (total 6s): becomes visible again (cycle repeat)
      act(() => {
        vi.advanceTimersByTime(2000);
      });
      expect(hintWrapper).toHaveClass("opacity-100");

      // User interacts (touchstart): immediately hides and restarts 2s idle timer
      const region = screen.getByRole("region", { name: "Questions" });
      act(() => {
        fireEvent.touchStart(region);
      });
      expect(hintWrapper).toHaveClass("opacity-0");

      // Advance 1.5s after touch: still hidden
      act(() => {
        vi.advanceTimersByTime(1500);
      });
      expect(hintWrapper).toHaveClass("opacity-0");

      // Advance another 500ms (total 2s after touch): becomes visible again
      act(() => {
        vi.advanceTimersByTime(500);
      });
      expect(hintWrapper).toHaveClass("opacity-100");

      vi.useRealTimers();
    });
  });

  describe("Overflowing inline-options vs non-overflowing/sheet questions", () => {
    it("allows inner scrolling for overflowing radio questions before snapping to next question", () => {
      const onActiveIndexChange = vi.fn();
      const { container } = render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <label>
              <input type="radio" name="opt" value="1" /> Option 1
            </label>
            <label>
              <input type="radio" name="opt" value="2" /> Option 2
            </label>
            <label>
              <input type="radio" name="opt" value="3" /> Option 3
            </label>
            <label>
              <input type="radio" name="opt" value="4" /> Option 4
            </label>
          </div>
          <div>Question 2</div>
        </QuestionSnapList>,
      );

      const contentEl = container.querySelector<HTMLElement>(".question-snap-content");
      expect(contentEl).not.toBeNull();

      if (contentEl) {
        // Mock overflowing height: scrollHeight 600px > clientHeight 400px (maxScroll = 200px)
        Object.defineProperty(contentEl, "scrollHeight", { value: 600, configurable: true });
        Object.defineProperty(contentEl, "clientHeight", { value: 400, configurable: true });
        contentEl.scrollTop = 0;
      }

      const region = screen.getByRole("region", { name: "Questions" });

      // Drag up while at top (scrollTop = 0 < maxScroll = 200): should NOT flip question
      fireEvent.touchStart(region, {
        touches: [{ clientX: 100, clientY: 300 }],
      });
      fireEvent.touchMove(region, {
        touches: [{ clientX: 100, clientY: 200 }],
      });
      fireEvent.touchEnd(region, {
        changedTouches: [{ clientX: 100, clientY: 200 }],
      });

      expect(onActiveIndexChange).not.toHaveBeenCalledWith(1);

      // Now simulate user having scrolled to bottom of options (scrollTop = 200)
      if (contentEl) {
        contentEl.scrollTop = 200;
      }

      // Drag up at the bottom: now it SHOULD snap to next question
      fireEvent.touchStart(region, {
        touches: [{ clientX: 100, clientY: 300 }],
      });
      fireEvent.touchMove(region, {
        touches: [{ clientX: 100, clientY: 150 }],
      });
      fireEvent.touchEnd(region, {
        changedTouches: [{ clientX: 100, clientY: 150 }],
      });

      expect(onActiveIndexChange).toHaveBeenCalledWith(1);
    });

    it("immediately snaps for non-overflowing questions (e.g. dropdown or short question)", () => {
      const onActiveIndexChange = vi.fn();
      const { container } = render(
        <QuestionSnapList onActiveIndexChange={onActiveIndexChange}>
          <div>
            <button>Dropdown Trigger</button>
          </div>
          <div>Question 2</div>
        </QuestionSnapList>,
      );

      const contentEl = container.querySelector<HTMLElement>(".question-snap-content");
      if (contentEl) {
        // Fits in screen: scrollHeight 300 <= clientHeight 400
        Object.defineProperty(contentEl, "scrollHeight", { value: 300, configurable: true });
        Object.defineProperty(contentEl, "clientHeight", { value: 400, configurable: true });
      }

      const region = screen.getByRole("region", { name: "Questions" });

      fireEvent.touchStart(region, {
        touches: [{ clientX: 100, clientY: 300 }],
      });
      fireEvent.touchMove(region, {
        touches: [{ clientX: 100, clientY: 150 }],
      });
      fireEvent.touchEnd(region, {
        changedTouches: [{ clientX: 100, clientY: 150 }],
      });

      expect(onActiveIndexChange).toHaveBeenCalledWith(1);
    });
  });
});
