/* @vitest-environment jsdom */

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

import { BrowserDetector } from "@/utils/browser-detect";
import { PlatformDetector } from "@/utils/platform-detect";

import { useChatKeyboardAvoidance } from "../use-chat-keyboard-avoidance";

vi.mock("@/utils/browser-detect", () => ({
  BrowserDetector: {
    isFacebookInAppBrowser: vi.fn(() => true),
  },
}));

vi.mock("@/utils/platform-detect", () => ({
  PlatformDetector: {
    isAndroid: vi.fn(() => true),
  },
}));

class MockVisualViewport extends EventTarget {
  height = 760;
  offsetTop = 0;
  scale = 1;
  width = 390;
}

class MockVirtualKeyboard extends EventTarget {
  boundingRect = { height: 0 };
}

function Harness({ active }: { active: boolean }) {
  const containerRef = useRef<HTMLDivElement>(null);
  useChatKeyboardAvoidance({ active, containerRef });
  return <div ref={containerRef} data-testid="chat-input" />;
}

describe("useChatKeyboardAvoidance", () => {
  let container: HTMLDivElement;
  let root: Root;
  let visualViewport: MockVisualViewport;
  let virtualKeyboard: MockVirtualKeyboard;
  let innerHeight: number;
  let innerWidth: number;

  beforeEach(() => {
    vi.useFakeTimers();
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
      .IS_REACT_ACT_ENVIRONMENT = true;
    vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
    vi.mocked(PlatformDetector.isAndroid).mockReturnValue(true);
    innerHeight = 800;
    innerWidth = 390;
    visualViewport = new MockVisualViewport();
    virtualKeyboard = new MockVirtualKeyboard();
    Object.defineProperty(window, "innerHeight", {
      configurable: true,
      get: () => innerHeight,
    });
    Object.defineProperty(window, "innerWidth", {
      configurable: true,
      get: () => innerWidth,
    });
    Object.defineProperty(window, "visualViewport", {
      configurable: true,
      value: visualViewport,
    });
    Object.defineProperty(navigator, "virtualKeyboard", {
      configurable: true,
      value: virtualKeyboard,
    });
    vi.stubGlobal(
      "requestAnimationFrame",
      vi.fn((callback: FrameRequestCallback) =>
        window.setTimeout(() => callback(0), 0),
      ),
    );
    vi.stubGlobal(
      "cancelAnimationFrame",
      vi.fn((id: number) => window.clearTimeout(id)),
    );
    container = document.createElement("div");
    document.body.append(container);
    root = createRoot(container);
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
    vi.useRealTimers();
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("uses a one-shot Facebook fallback after 150ms without polling", () => {
    const intervalSpy = vi.spyOn(window, "setInterval");
    renderHarness(root, false);
    renderHarness(root, true);

    act(() => vi.advanceTimersByTime(149));
    expect(getBottomInset(container)).toBe("");

    act(() => vi.advanceTimersByTime(1));
    expect(getBottomInset(container)).toBe("344px");
    expect(intervalSpy).not.toHaveBeenCalled();
  });

  it("does not run any avoidance outside Facebook's in-app browser", () => {
    vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(false);
    renderHarness(root, false);
    renderHarness(root, true);

    visualViewport.height = 460;
    virtualKeyboard.boundingRect.height = 320;
    act(() => {
      visualViewport.dispatchEvent(new Event("resize"));
      virtualKeyboard.dispatchEvent(new Event("geometrychange"));
      vi.advanceTimersByTime(150);
    });

    expect(getBottomInset(container)).toBe("");
  });

  it("does not run avoidance for iOS Facebook", () => {
    vi.mocked(PlatformDetector.isAndroid).mockReturnValue(false);
    renderHarness(root, false);
    renderHarness(root, true);

    visualViewport.height = 460;
    act(() => {
      visualViewport.dispatchEvent(new Event("resize"));
      vi.advanceTimersByTime(150);
    });

    expect(getBottomInset(container)).toBe("");
  });

  it("uses Visual Viewport geometry and clears after keyboard dismissal", () => {
    renderHarness(root, false);
    renderHarness(root, true);

    visualViewport.height = 460;
    act(() => {
      visualViewport.dispatchEvent(new Event("resize"));
      vi.advanceTimersByTime(0);
    });
    expect(getBottomInset(container)).toBe("316px");

    visualViewport.height = 760;
    act(() => {
      visualViewport.dispatchEvent(new Event("resize"));
      vi.advanceTimersByTime(0);
    });
    expect(getBottomInset(container)).toBe("0px");
  });

  it("uses Virtual Keyboard geometry with the target gap", () => {
    renderHarness(root, false);
    renderHarness(root, true);

    virtualKeyboard.boundingRect.height = 320;
    act(() => {
      virtualKeyboard.dispatchEvent(new Event("geometrychange"));
      vi.advanceTimersByTime(0);
    });

    expect(getBottomInset(container)).toBe("336px");
  });

  it("does not add fallback when the layout viewport resized", () => {
    renderHarness(root, false);
    renderHarness(root, true);

    innerHeight = 500;
    visualViewport.height = 500;
    act(() => {
      window.dispatchEvent(new Event("resize"));
      vi.advanceTimersByTime(150);
    });

    expect(getBottomInset(container)).toBe("0px");
  });

  it("allows a delayed real viewport signal to replace the fallback", () => {
    renderHarness(root, false);
    renderHarness(root, true);
    act(() => vi.advanceTimersByTime(150));
    expect(getBottomInset(container)).toBe("344px");

    visualViewport.height = 500;
    act(() => {
      visualViewport.dispatchEvent(new Event("resize"));
      vi.advanceTimersByTime(0);
    });

    expect(getBottomInset(container)).toBe("276px");
  });

  it("removes the local inset when focus leaves", () => {
    renderHarness(root, false);
    renderHarness(root, true);
    act(() => vi.advanceTimersByTime(150));
    expect(getBottomInset(container)).toBe("344px");

    renderHarness(root, false);

    expect(getBottomInset(container)).toBe("");
  });
});

function renderHarness(root: Root, active: boolean): void {
  act(() => root.render(<Harness active={active} />));
}

function getBottomInset(container: HTMLElement): string {
  const input = container.querySelector<HTMLDivElement>(
    '[data-testid="chat-input"]',
  );
  if (!input) throw new Error("Chat input test node was not rendered");
  return input.style.getPropertyValue("--chat-keyboard-bottom-inset");
}
