// @vitest-environment happy-dom

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

import {
  ChatHistoryList,
  type ChatHistoryItem,
  type ChatHistorySection,
} from "./ChatHistoryList.js";

function item(
  overrides: Partial<ChatHistoryItem> & { id: string },
): ChatHistoryItem {
  return {
    title: overrides.id,
    ...overrides,
  };
}

/** React tracks the DOM input's value via a wrapped setter to decide whether
 * to fire its synthetic change handler, so setting `.value` directly is not
 * observed. Go through the native prototype setter instead, matching the
 * pattern used by CommandMenu.spec.tsx. */
function typeIntoInput(input: HTMLInputElement, value: string) {
  const setter = Object.getOwnPropertyDescriptor(
    window.HTMLInputElement.prototype,
    "value",
  )?.set;
  setter?.call(input, value);
  input.dispatchEvent(new Event("input", { bubbles: true }));
  input.dispatchEvent(new Event("change", { bubbles: true }));
}

async function openMenu(trigger: HTMLButtonElement) {
  await act(async () => {
    trigger.dispatchEvent(
      new PointerEvent("pointerdown", {
        bubbles: true,
        button: 0,
        pointerType: "mouse",
      }),
    );
    await Promise.resolve();
  });
}

function getMenuItems() {
  return Array.from(
    document.body.querySelectorAll<HTMLElement>(
      ".an-chat-history-row__menu-item",
    ),
  );
}

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

  beforeEach(() => {
    vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
    vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
      cb(0);
      return 0;
    });
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
    document.body.innerHTML = "";
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("renders a flat item list and calls onSelect when a row is clicked", () => {
    const onSelect = vi.fn();
    const items: ChatHistoryItem[] = [
      item({ id: "thread-1", title: "First chat", timestamp: "Yesterday" }),
      item({ id: "thread-2", title: "Second chat", timestamp: "2 min ago" }),
    ];

    act(() => {
      root.render(<ChatHistoryList items={items} onSelect={onSelect} />);
    });

    const titles = Array.from(
      container.querySelectorAll(".an-chat-history-row__title"),
    ).map((el) => el.textContent);
    expect(titles).toEqual(["First chat", "Second chat"]);
    expect(container.textContent).toContain("Yesterday");
    expect(container.textContent).toContain("2 min ago");

    const buttons = container.querySelectorAll(".an-chat-history-row__button");
    act(() => {
      (buttons[1] as HTMLButtonElement).click();
    });
    expect(onSelect).toHaveBeenCalledWith("thread-2");
  });

  it("highlights the active item", () => {
    const items: ChatHistoryItem[] = [
      item({ id: "thread-1" }),
      item({ id: "thread-2" }),
    ];

    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          activeId="thread-2"
          onSelect={() => {}}
        />,
      );
    });

    const rows = container.querySelectorAll(".an-chat-history-row");
    expect(rows[0].className).not.toContain("an-chat-history-row--active");
    expect(rows[1].className).toContain("an-chat-history-row--active");
  });

  it("renders grouped sections with labels", () => {
    const sections: ChatHistorySection[] = [
      {
        id: "scoped",
        label: "This deck",
        items: [item({ id: "thread-1", title: "Deck chat" })],
      },
      {
        id: "other",
        label: "All chats",
        items: [item({ id: "thread-2", title: "Other chat" })],
      },
    ];

    act(() => {
      root.render(<ChatHistoryList sections={sections} onSelect={() => {}} />);
    });

    const labels = Array.from(
      container.querySelectorAll(".an-chat-history__section-label"),
    ).map((el) => el.textContent);
    expect(labels).toEqual(["This deck", "All chats"]);
  });

  it("shows the empty state, distinguishing plain-empty from no-search-results", () => {
    act(() => {
      root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
    });
    expect(container.textContent).toContain("No chats yet");

    act(() => {
      root.render(
        <ChatHistoryList
          items={[]}
          searchValue="foo"
          onSearchChange={() => {}}
          onSelect={() => {}}
        />,
      );
    });
    expect(container.textContent).toContain("No matching chats");
  });

  it("shows a loading state instead of the list", () => {
    const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          loading
          loadingLabel="Searching..."
          onSelect={() => {}}
        />,
      );
    });
    expect(container.textContent).toContain("Searching...");
    expect(container.querySelector(".an-chat-history-row")).toBeNull();
  });

  it("shows an error state that takes priority over loading and items", () => {
    const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          loading
          error="Could not load chats"
          onSelect={() => {}}
        />,
      );
    });
    expect(container.textContent).toContain("Could not load chats");
    expect(container.querySelector(".an-chat-history-row")).toBeNull();
  });

  it("renders a controlled search box only when onSearchChange is supplied", () => {
    const onSearchChange = vi.fn();
    act(() => {
      root.render(
        <ChatHistoryList
          items={[]}
          searchValue="abc"
          onSearchChange={onSearchChange}
          onSelect={() => {}}
        />,
      );
    });
    const input = container.querySelector<HTMLInputElement>(
      ".an-chat-history__search-input",
    );
    expect(input).not.toBeNull();
    expect(input!.value).toBe("abc");

    act(() => {
      typeIntoInput(input!, "abcd");
    });
    expect(onSearchChange).toHaveBeenCalledWith("abcd");

    act(() => {
      root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
    });
    expect(
      container.querySelector(".an-chat-history__search-input"),
    ).toBeNull();
  });

  it("does not render a row action menu when no menu callbacks are given", () => {
    const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
    act(() => {
      root.render(<ChatHistoryList items={items} onSelect={() => {}} />);
    });
    expect(container.querySelector(".an-chat-history-row__menu")).toBeNull();
  });

  it("supports pin toggling via the row action menu", async () => {
    const onTogglePin = vi.fn();
    const items: ChatHistoryItem[] = [
      item({ id: "thread-1", title: "First chat" }),
    ];
    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          onSelect={() => {}}
          onTogglePin={onTogglePin}
        />,
      );
    });

    const trigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    expect(trigger).not.toBeNull();
    await openMenu(trigger!);

    const pinItem = getMenuItems().find((el) =>
      el.textContent?.includes("Pin to top"),
    );
    expect(pinItem).toBeDefined();
    act(() => {
      (pinItem as HTMLButtonElement).click();
    });
    expect(onTogglePin).toHaveBeenCalledWith("thread-1");
  });

  it("ports rail row menus outside the scroll container", async () => {
    act(() => {
      root.render(
        <ChatHistoryList
          variant="rail"
          items={[item({ id: "thread-1" })]}
          onSelect={() => {}}
          onDelete={() => {}}
        />,
      );
    });

    const trigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    await openMenu(trigger!);

    const menu = document.body.querySelector<HTMLElement>(
      ".an-chat-history-row__menu-content",
    );
    expect(menu).not.toBeNull();
    expect(menu?.closest(".an-chat-history__list")).toBeNull();
    expect(menu?.getAttribute("role")).toBe("menu");
  });

  it("uses custom labels for localized row actions", async () => {
    const onTogglePin = vi.fn();
    const onDelete = vi.fn();
    const labels = {
      options: "Conversation actions",
      renameInput: "Rename analytics conversation",
      rename: "Rename conversation",
      pin: "Keep at top",
      unpin: "Remove from top",
      delete: "Archive conversation",
    };
    const render = (pinned = false) => {
      act(() => {
        root.render(
          <ChatHistoryList
            key={String(pinned)}
            items={[item({ id: "thread-1", titleText: "Campaign", pinned })]}
            onSelect={() => {}}
            onRename={() => {}}
            onTogglePin={onTogglePin}
            onDelete={onDelete}
            labels={labels}
          />,
        );
      });
    };

    render();
    const trigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    expect(trigger?.getAttribute("aria-label")).toBe(labels.options);
    await openMenu(trigger!);
    expect(document.body.textContent).toContain(labels.rename);
    expect(document.body.textContent).toContain(labels.pin);
    expect(document.body.textContent).toContain(labels.delete);

    const archiveItem = getMenuItems().find((element) =>
      element.textContent?.includes(labels.delete),
    );
    act(() => {
      (archiveItem as HTMLButtonElement).click();
    });
    expect(onDelete).toHaveBeenCalledWith("thread-1");

    render();
    const renameTrigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    await openMenu(renameTrigger!);
    const renameItem = getMenuItems().find((element) =>
      element.textContent?.includes(labels.rename),
    );
    act(() => {
      (renameItem as HTMLButtonElement).click();
    });
    expect(
      container
        .querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
        ?.getAttribute("aria-label"),
    ).toBe(labels.renameInput);

    render(true);
    const pinnedTrigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    await openMenu(pinnedTrigger!);
    expect(document.body.textContent).toContain(labels.unpin);
  });

  it("supports inline rename via the row action menu", async () => {
    const onRename = vi.fn();
    const items: ChatHistoryItem[] = [
      item({ id: "thread-1", title: "Old title", titleText: "Old title" }),
    ];
    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          onSelect={() => {}}
          onRename={onRename}
          renameMaxLength={160}
        />,
      );
    });

    const trigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    await openMenu(trigger!);
    const renameItem = getMenuItems().find((el) =>
      el.textContent?.includes("Rename"),
    );
    act(() => {
      (renameItem as HTMLButtonElement).click();
    });

    const input = container.querySelector<HTMLInputElement>(
      ".an-chat-history-row__rename-input",
    );
    expect(input).not.toBeNull();
    expect(input!.value).toBe("Old title");
    expect(input!.maxLength).toBe(160);

    act(() => {
      typeIntoInput(input!, "New title");
    });
    act(() => {
      input!.dispatchEvent(
        new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
      );
    });
    expect(onRename).toHaveBeenCalledWith("thread-1", "New title");
  });

  it("supports item-aware labels and app-specific actions", async () => {
    const onShare = vi.fn();
    const items: ChatHistoryItem[] = [
      item({ id: "thread-1", title: "Campaign", titleText: "Campaign" }),
    ];
    act(() => {
      root.render(
        <ChatHistoryList
          items={items}
          onSelect={() => {}}
          onRename={() => {}}
          labels={{
            options: (chat) => `Options for ${chat.titleText}`,
            renameInput: (chat) => `Rename ${chat.titleText}`,
          }}
          renderAdditionalRowActions={(chat, closeMenu) => (
            <button
              type="button"
              className="an-chat-history-row__menu-item"
              onClick={() => {
                closeMenu();
                onShare(chat.id);
              }}
            >
              Share
            </button>
          )}
        />,
      );
    });

    const trigger = container.querySelector<HTMLButtonElement>(
      ".an-chat-history-row__menu-trigger",
    );
    expect(trigger?.getAttribute("aria-label")).toBe("Options for Campaign");
    await openMenu(trigger!);

    const shareItem = getMenuItems().find(
      (element) => element.textContent === "Share",
    );
    act(() => {
      (shareItem as HTMLButtonElement).click();
    });
    expect(onShare).toHaveBeenCalledWith("thread-1");
    expect(
      document.body.querySelector(".an-chat-history-row__menu-content"),
    ).toBeNull();

    await openMenu(trigger!);
    const renameItem = getMenuItems().find(
      (element) => element.textContent === "Rename",
    );
    act(() => {
      (renameItem as HTMLButtonElement).click();
    });
    expect(
      container
        .querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
        ?.getAttribute("aria-label"),
    ).toBe("Rename Campaign");
  });

  it("renders the footer inside the scroll region regardless of list state", () => {
    act(() => {
      root.render(
        <ChatHistoryList
          items={[]}
          onSelect={() => {}}
          footer={<div data-testid="footer">Load older chats</div>}
        />,
      );
    });
    expect(container.textContent).toContain("Load older chats");
  });
});
