/* @vitest-environment jsdom */

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

const mocks = vi.hoisted(() => ({
  dispatch: vi.fn(),
}));

vi.mock("@/stores/chat/chat-context", () => ({
  useChatDispatch: () => mocks.dispatch,
}));
vi.mock("@/providers/character-provider", () => ({
  useActiveCharacter: () => ({ id: "maya-tan" }),
  useActiveCharacterRoutes: () => ({
    tip: "/characters/maya/tip",
  }),
}));
vi.mock("../../hooks/use-chat-keyboard-avoidance", () => ({
  useChatKeyboardAvoidance: () => undefined,
}));
vi.mock("../../hooks/use-chat-keyboard-diagnostics", () => ({
  useChatKeyboardDiagnostics: () => undefined,
}));

import { ChatInputBar } from "../chat-input-bar";

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

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

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

  it("opens the ordered action menu, blurs input, and links to Tip", () => {
    renderBar();
    const textarea = getTextarea();
    act(() => textarea.focus());
    expect(document.activeElement).toBe(textarea);

    act(() => getButton("Open chat actions").click());

    expect(document.activeElement).not.toBe(textarea);
    const menu = container.querySelector('[aria-label="Chat actions"]');
    expect(menu).not.toBeNull();
    expect(
      Array.from(menu?.children ?? []).map((item) => item.textContent?.trim()),
    ).toEqual(["Image", "Voice", "Tip"]);
    expect(getButton("Close chat actions").getAttribute("aria-expanded")).toBe(
      "true",
    );
    expect(
      menu
        ?.querySelector<HTMLAnchorElement>('[data-analytics-key="chat.open_tip"]')
        ?.getAttribute("href"),
    ).toBe("/characters/maya/tip");

    act(() => document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })));
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();
  });

  it("injects image and voice promotions without persisting UI state", () => {
    renderBar();

    act(() => getButton("Open chat actions").click());
    act(() => getButton("Image").click());
    const imageEvent = mocks.dispatch.mock.calls[0]?.[0];
    expect(imageEvent).toMatchObject({
      type: "ChatPromotionInjected",
      promotion: {
        characterId: "maya-tan",
        promotionType: "image",
        lockType: "image_paywall",
      },
    });
    expect(imageEvent.promotion.clientLockId).toMatch(/^promotion_/);
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();

    act(() => getButton("Open chat actions").click());
    act(() => getButton("Voice").click());
    const voiceEvent = mocks.dispatch.mock.calls[1]?.[0];
    expect(voiceEvent).toMatchObject({
      type: "ChatPromotionInjected",
      promotion: {
        characterId: "maya-tan",
        promotionType: "voice",
        lockType: "voice_message",
      },
    });
    expect(voiceEvent.promotion.clientLockId).not.toBe(
      imageEvent.promotion.clientLockId,
    );
  });

  it("switches from actions to Send for non-whitespace input", () => {
    renderBar();
    act(() => getButton("Open chat actions").click());

    setTextareaValue(getTextarea(), "Hello Maya");
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();
    expect(getButton("Send message")).not.toBeNull();

    act(() => getButton("Send message").click());
    expect(mocks.dispatch).toHaveBeenCalledWith({
      type: "ChatSendMessage",
      content: "Hello Maya",
    });
    expect(getTextarea().value).toBe("");
    expect(document.activeElement).toBe(getTextarea());

    setTextareaValue(getTextarea(), "   ");
    expect(getButton("Open chat actions")).not.toBeNull();
  });

  it("suppresses the Facebook WebView click that follows a pointer send for 300ms", () => {
    const now = vi.spyOn(Date, "now").mockReturnValue(1_000);
    renderBar();
    setTextareaValue(getTextarea(), "Hello Maya");
    const sendButton = getButton("Send message");
    const pointerDown = new Event("pointerdown", {
      bubbles: true,
      cancelable: true,
    });
    Object.defineProperty(pointerDown, "pointerType", { value: "touch" });

    act(() => sendButton.dispatchEvent(pointerDown));

    expect(mocks.dispatch).toHaveBeenCalledTimes(1);
    expect(mocks.dispatch).toHaveBeenCalledWith({
      type: "ChatSendMessage",
      content: "Hello Maya",
    });
    expect(getTextarea().value).toBe("");
    expect(getButton("Open chat actions")).toBe(sendButton);

    now.mockReturnValue(1_299);
    act(() => sendButton.click());
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();

    now.mockReturnValue(1_300);
    act(() => sendButton.click());
    expect(container.querySelector('[aria-label="Chat actions"]')).not.toBeNull();
  });

  it("closes the menu on outside interaction and when disabled", () => {
    renderBar();
    act(() => getButton("Open chat actions").click());
    act(() => document.body.dispatchEvent(new Event("pointerdown", { bubbles: true })));
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();

    act(() => getButton("Open chat actions").click());
    renderBar(true);
    expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull();
    expect(getButton("Open chat actions").disabled).toBe(true);
  });

  function renderBar(disabled = false): void {
    act(() => root.render(<ChatInputBar disabled={disabled} />));
  }

  function getTextarea(): HTMLTextAreaElement {
    const textarea = container.querySelector("textarea");
    if (!textarea) throw new Error("Missing chat textarea");
    return textarea;
  }

  function getButton(label: string): HTMLButtonElement {
    const button = Array.from(
      container.querySelectorAll<HTMLButtonElement>("button"),
    ).find(
      (item) =>
        item.getAttribute("aria-label") === label ||
        item.textContent?.trim() === label,
    );
    if (!button) throw new Error(`Missing button: ${label}`);
    return button;
  }
});

function setTextareaValue(
  textarea: HTMLTextAreaElement,
  value: string,
): void {
  const setter = Object.getOwnPropertyDescriptor(
    HTMLTextAreaElement.prototype,
    "value",
  )?.set;
  act(() => {
    setter?.call(textarea, value);
    textarea.dispatchEvent(new Event("input", { bubbles: true }));
  });
}
