// @vitest-environment jsdom

import { describe, expect, it, vi } from "vitest";
import type { Thread } from "../../../src/domain/threads/model";
import { renderThreadsViewShell } from "../../../src/features/threads-view/shell.dom";
import { type ThreadsRowModel, type ThreadsViewPanelActivity, threadRows } from "../../../src/features/threads-view/state";
import { changeInputValue, installObsidianDomShims } from "../../support/dom";

installObsidianDomShims();

function expectPresent<T>(value: T | null | undefined): T {
  if (value === null || value === undefined) throw new Error("Expected value to be present");
  return value;
}

function panelActivity(overrides: Partial<ThreadsViewPanelActivity> = {}): ThreadsViewPanelActivity {
  return {
    threadId: "thread",
    selected: false,
    pending: false,
    running: false,
    ...overrides,
  };
}

function threadFixture(overrides: Partial<Thread> = {}): Thread {
  return {
    id: "thread",
    preview: "",
    createdAt: 1,
    updatedAt: 1,
    name: null,
    archived: false,
    provenance: { kind: "interactive" },
    ...overrides,
  };
}

function rowFixture(overrides: Partial<ThreadsRowModel> = {}): ThreadsRowModel {
  const threadId = overrides.threadId ?? "thread";
  const title = overrides.title ?? "Thread";
  return {
    threadId,
    title,
    live: null,
    selected: false,
    rename: { active: false, draft: title, generating: false },
    archiveConfirm: { active: false, defaultSaveMarkdown: false },
    ...overrides,
  };
}

function threadsViewActions() {
  return {
    refresh: vi.fn(),
    loadMore: vi.fn(),
    openNewPanel: vi.fn(),
    openThread: vi.fn(),
    startRename: vi.fn(),
    updateRename: vi.fn(),
    saveRename: vi.fn(),
    cancelRename: vi.fn(),
    autoNameThread: vi.fn(),
    startArchive: vi.fn(),
    archiveThread: vi.fn(),
  };
}

describe("threads view renderer decisions", () => {
  it("prioritizes open panel live state per thread", () => {
    expect(
      threadRows(
        [threadFixture({ id: "thread" })],
        [
          panelActivity({ threadId: "thread" }),
          panelActivity({ threadId: "thread", running: true }),
          panelActivity({ threadId: "thread", pending: true }),
        ],
        new Map(),
      )[0]?.live,
    ).toMatchObject({ status: "pending" });

    expect(threadRows([threadFixture({ id: "thread" })], [panelActivity({ threadId: "thread" })], new Map())[0]?.live).toMatchObject({
      status: "open",
    });
    expect(
      threadRows([threadFixture({ id: "thread" })], [panelActivity({ threadId: null, running: true })], new Map())[0]?.live,
    ).toBeNull();
  });

  it("marks a row selected when one open panel for the thread was last focused", () => {
    const rows = threadRows(
      [
        threadFixture({ id: "closed", preview: "Closed thread" }),
        threadFixture({ id: "focused", preview: "Focused thread", updatedAt: 2 }),
      ],
      [panelActivity({ threadId: "focused", selected: true })],
      new Map(),
    );

    expect(rows.find((row) => row.threadId === "focused")?.selected).toBe(true);
    expect(rows.find((row) => row.threadId === "closed")?.selected).toBe(false);
  });

  it("renders thread rows with live state and routes open actions", () => {
    const parent = document.createElement("div");
    const actions = threadsViewActions();
    const rows = threadRows(
      [threadFixture({ id: "closed", preview: "Closed thread" }), threadFixture({ id: "open", preview: "Open thread", updatedAt: 2 })],
      [panelActivity({ threadId: "open", pending: true, selected: true })],
      new Map(),
    );

    renderThreadsViewShell(parent, { status: "2 threads", loading: false, rows }, actions);

    const main = expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__row--pending"));
    expect(main.textContent).toContain("Open thread");
    const toolbarButtons = [...parent.querySelectorAll<HTMLElement>(".codex-panel-threads__toolbar-button")];
    expect(toolbarButtons.map((button) => button.getAttribute("aria-label"))).toEqual(["Open new panel", "Refresh threads"]);
    const refresh = expectPresent(parent.querySelector<HTMLElement>('[aria-label="Refresh threads"]'));
    refresh.click();
    expect(actions.refresh).toHaveBeenCalledOnce();
    const openNewPanel = expectPresent(parent.querySelector<HTMLElement>('[aria-label="Open new panel"]'));
    openNewPanel.click();
    expect(actions.openNewPanel).toHaveBeenCalledOnce();
    expect(parent.querySelector<HTMLButtonElement>('[aria-label="Rename thread"]')).not.toBeNull();
    main.click();
    expect(actions.openThread).toHaveBeenCalledWith("open");
  });

  it("renders threads view archive confirmation with the default action on the right", () => {
    const parent = document.createElement("div");
    const actions = threadsViewActions();
    const row = rowFixture({
      archiveConfirm: { active: true, defaultSaveMarkdown: false },
    });

    renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, actions);

    const confirm = expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__archive-confirm"));
    const archiveButtons = [
      ...confirm.querySelectorAll<HTMLButtonElement>(".codex-panel-threads__archive-alternate, .codex-panel-threads__archive-default"),
    ];
    expect(archiveButtons.map((button) => button.getAttribute("aria-label"))).toEqual([
      "Save and archive thread",
      "Archive thread without saving",
    ]);
    expect(parent.querySelector<HTMLButtonElement>('[aria-label="Rename thread"]')).toBeNull();
    archiveButtons[0]?.click();
    expect(actions.archiveThread).toHaveBeenCalledWith("thread", true);
    archiveButtons[1]?.click();
    expect(actions.archiveThread).toHaveBeenCalledWith("thread", false);
  });

  it("starts threads view archive confirmation before archiving", () => {
    const parent = document.createElement("div");
    const actions = threadsViewActions();
    const row = rowFixture();

    renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, actions);
    parent.querySelector<HTMLButtonElement>('[aria-label="Archive thread"]')?.click();

    expect(actions.startArchive).toHaveBeenCalledWith("thread");
    expect(actions.archiveThread).not.toHaveBeenCalled();
  });

  it("renders rename rows and saves entered values", () => {
    const parent = document.createElement("div");
    document.body.append(parent);
    const actions = threadsViewActions();
    const row = rowFixture({
      title: "Old name",
      rename: { active: true, draft: "Old name", generating: false },
    });

    renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, actions);

    const input = expectPresent(parent.querySelector<HTMLInputElement>(".codex-panel-threads__rename-input"));
    expect(document.activeElement).toBe(input);
    expect([input.selectionStart, input.selectionEnd]).toEqual([0, input.value.length]);
    changeInputValue(input, "New name");
    expect(actions.updateRename).toHaveBeenCalledWith("thread", "New name");

    renderThreadsViewShell(
      parent,
      { status: "1 thread", loading: false, rows: [{ ...row, rename: { active: true, draft: "New name", generating: false } }] },
      actions,
    );
    const renamedInput = expectPresent(parent.querySelector<HTMLInputElement>(".codex-panel-threads__rename-input"));
    renamedInput.dispatchEvent(new FocusEvent("blur"));
    expect(actions.saveRename).toHaveBeenCalledWith("thread", "New name");
    actions.saveRename.mockClear();
    renamedInput.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, isComposing: true }));
    expect(actions.saveRename).not.toHaveBeenCalled();
    renamedInput.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
    expect(actions.saveRename).toHaveBeenCalledWith("thread", "New name");
    renamedInput.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
    expect(actions.cancelRename).toHaveBeenCalledWith("thread");

    expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__row-main")).click();
    expect(actions.openThread).not.toHaveBeenCalled();
    parent.remove();
  });

  it("renders threads view rename actions inline with auto-name", () => {
    const parent = document.createElement("div");
    const actions = threadsViewActions();
    const row = rowFixture({
      title: "Old name",
      rename: { active: true, draft: "Old name", generating: false },
    });

    renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, actions);

    expect(parent.querySelector<HTMLElement>(".codex-panel-threads__rename-form")).toBeTruthy();
    parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.click();

    expect(actions.autoNameThread).toHaveBeenCalledWith("thread");
  });

  it("renders threads view rename auto-name loading state", () => {
    const parent = document.createElement("div");
    const row = rowFixture({
      title: "Old name",
      rename: { active: true, draft: "Old name", generating: true },
    });

    renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, threadsViewActions());

    expect(parent.querySelector<HTMLInputElement>(".codex-panel-threads__rename-input")?.disabled).toBe(false);
    expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.disabled).toBe(true);
  });
});
