/**
 * @file RunHistory.test.tsx
 * @description Pins the props-only boundary of `ActiveRunsSwitcher` / `RunsModal`
 * after their move out of `pages/Run.tsx`: the switcher counts live runs and
 * opens the list, the list merges live in-memory handles with persistent history
 * (live entries winning on a shared id) newest first, marks the live and current
 * rows, filters by status / mode / free text, and fires attach / resume / view
 * with the right run — attach by run id, resume and view with the matching
 * history item.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { MemoryRouter } from "react-router-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "i18next";
import { ActiveRunsSwitcher, RunsModal, type UnifiedRunRow } from "../RunHistory";
import type { DashboardRunHistoryItem, RunListResponse } from "../../../lib/api";
import type { Session } from "../../../lib/types";

const LIVE_ID = "run-live";
const PAST_ID = "run-past";
const HEADLESS_ID = "run-headless";

const activeRuns = {
  activeCount: 1,
  maxConcurrent: 2,
  items: [
    {
      id: LIVE_ID,
      sessionId: "sess-live",
      cwd: "/tmp/live",
      model: "claude-opus-5",
      status: "running",
      startedAt: "2000-01-01T00:50:00Z",
      promptPreview: "the live prompt",
      endedAt: null,
    },
  ],
} as unknown as RunListResponse;

function historyItem(over: Partial<DashboardRunHistoryItem>): DashboardRunHistoryItem {
  return {
    id: PAST_ID,
    session_id: "sess-past",
    cwd: "/tmp/past",
    model: "sonnet",
    status: "completed",
    prompt_preview: "the past prompt",
    started_at: new Date(2000).toISOString(),
    ended_at: new Date(2500).toISOString(),
    exit_code: 0,
    permission_mode: "acceptEdits",
    effort: "",
    resume_session_id: null,
    isLive: false,
    ...over,
  } as DashboardRunHistoryItem;
}

const PAST = historyItem({});
const HEADLESS = historyItem({
  id: HEADLESS_ID,
  session_id: "sess-headless",
  cwd: "/tmp/headless",
  prompt_preview: "the headless prompt",
  started_at: new Date(1000).toISOString(),
});

function externalSession(over: Partial<Session> = {}): Session {
  return {
    id: "sess-external",
    name: "the external prompt",
    status: "active",
    cwd: "/tmp/external",
    model: "claude-opus-5",
    started_at: new Date(3000).toISOString(),
    ended_at: null,
    updated_at: new Date(3000).toISOString(),
    source: "local",
    ...over,
  } as unknown as Session;
}

function renderSwitcher(overrides: Partial<React.ComponentProps<typeof ActiveRunsSwitcher>> = {}) {
  const spies = {
    onAttach: vi.fn(),
    onResumeFromHistory: vi.fn(),
    onViewFromHistory: vi.fn(),
    onRefresh: vi.fn(),
  };
  const utils = render(
    <MemoryRouter>
      <ActiveRunsSwitcher
        activeRuns={activeRuns}
        currentHandleId={null}
        runHistory={[PAST, HEADLESS]}
        {...spies}
        {...overrides}
      />
    </MemoryRouter>
  );
  return { ...utils, spies };
}

function row(id: string, over: Partial<UnifiedRunRow> = {}): UnifiedRunRow {
  return {
    id,
    sessionId: `sess-${id}`,
    cwd: `/tmp/${id}`,
    model: "sonnet",
    status: "abandoned",
    promptPreview: `prompt of ${id}`,
    startedAt: 1000,
    endedAt: 2000,
    isLive: false,
    ...over,
  };
}

function renderModal(
  rows: UnifiedRunRow[],
  overrides: Partial<React.ComponentProps<typeof RunsModal>> = {}
) {
  const spies = {
    onAttach: vi.fn(),
    onResume: vi.fn(),
    onView: vi.fn(),
    onClose: vi.fn(),
    onRefresh: vi.fn(),
  };
  const utils = render(
    <MemoryRouter>
      <RunsModal
        rows={rows}
        currentHandleId={null}
        runHistory={[PAST, HEADLESS]}
        {...spies}
        {...overrides}
      />
    </MemoryRouter>
  );
  return { ...utils, spies };
}

/** The nth "All" chip — index 0 is the status group, 1 is the mode group. */
function allChip(nth: number): HTMLElement {
  const hits = screen.getAllByText(i18n.t("run:runs.allLabel", "All"));
  const hit = hits[nth];
  if (!hit) throw new Error(`no "All" chip at index ${nth}`);
  return hit;
}

/** A filter chip, told apart from the same word appearing in a row's status
 *  pill or mode badge by being a `<button>`. */
function chip(label: string): HTMLElement {
  const hit = screen.getAllByText(label).find((el) => el.tagName === "BUTTON");
  if (!hit) throw new Error(`no filter chip labelled ${label}`);
  return hit;
}

const openModal = () =>
  fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));

beforeEach(() => {
  i18n.changeLanguage("en");
  vi.clearAllMocks();
});

describe("ActiveRunsSwitcher", () => {
  it("labels the button with the live count and opens the list", () => {
    renderSwitcher();
    openModal();
    expect(screen.getByText("the live prompt")).toBeTruthy();
    expect(screen.getByText("the past prompt")).toBeTruthy();
  });

  it("falls back to the total count when nothing is live, and disables at zero", () => {
    const { unmount } = renderSwitcher({ activeRuns: null });
    expect(screen.getByText(i18n.t("run:runs.switcher"))).toBeTruthy();
    expect(screen.getByText("2")).toBeTruthy();
    unmount();

    renderSwitcher({ activeRuns: null, runHistory: [] });
    const button = screen.getByText(i18n.t("run:runs.switcher")).closest("button");
    expect((button as HTMLButtonElement).disabled).toBe(true);
  });

  it("lists live runs first and marks the live one", () => {
    renderSwitcher();
    openModal();
    const prompts = screen
      .getAllByText(/the (live|past|headless) prompt/)
      .map((el) => el.textContent);
    expect(prompts).toEqual(["the live prompt", "the past prompt", "the headless prompt"]);
    expect(screen.getAllByText("live")).toHaveLength(1);
  });

  it("prefers the live handle over a history row with the same id", () => {
    renderSwitcher({ runHistory: [historyItem({ id: LIVE_ID, prompt_preview: "stale copy" })] });
    openModal();
    expect(screen.getByText("the live prompt")).toBeTruthy();
    expect(screen.queryByText("stale copy")).toBeNull();
  });

  it("counts and lists a session started outside the dashboard, resumable not attachable", () => {
    const { spies } = renderSwitcher({
      activeRuns: null,
      runHistory: [],
      externalSessions: [externalSession()],
    });
    fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
    expect(screen.getByText("the external prompt")).toBeTruthy();
    expect(screen.getByText(i18n.t("run:runs.externalBadge"))).toBeTruthy();
    // No tmux session of ours to attach to — Resume is the only action.
    expect(screen.queryByText(i18n.t("run:runs.attachLabel", "Attach"))).toBeNull();
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    expect(spies.onResumeFromHistory).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "session:sess-external",
        session_id: "sess-external",
        cwd: "/tmp/external",
        status: "running",
        isLive: true,
      })
    );
  });

  it("skips external sessions already covered by a run, remote ones, and cwd-less ones", () => {
    renderSwitcher({
      runHistory: [],
      externalSessions: [
        externalSession({ id: "sess-live" }), // same session as the live run
        externalSession({ id: "sess-remote", source: "remote-1" }),
        externalSession({ id: "sess-nocwd", cwd: null }),
      ],
    });
    fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
    expect(screen.getByText("the live prompt")).toBeTruthy();
    expect(screen.queryByText("the external prompt")).toBeNull();
    expect(screen.queryByText(i18n.t("run:runs.externalBadge"))).toBeNull();
  });

  it("fires attach with the run id of the row that was clicked", () => {
    const { spies } = renderSwitcher();
    openModal();
    fireEvent.click(screen.getByText(i18n.t("run:runs.attachLabel", "Attach")));
    expect(spies.onAttach).toHaveBeenCalledWith(LIVE_ID);
    // Attaching closes the list, which is what the page relies on.
    expect(screen.queryByText("the past prompt")).toBeNull();
  });
});

describe("RunsModal", () => {
  it("offers Attach only for a live row that is not the current one", () => {
    const { unmount } = renderModal([row(LIVE_ID, { isLive: true, status: "running" })], {
      currentHandleId: LIVE_ID,
    });
    expect(screen.queryByText(i18n.t("run:runs.attachLabel", "Attach"))).toBeNull();
    expect(screen.getByText(i18n.t("run:runs.currentBadge", "current"))).toBeTruthy();
    unmount();

    const { spies } = renderModal([row(LIVE_ID, { isLive: true, status: "running" })]);
    fireEvent.click(screen.getByText(i18n.t("run:runs.attachLabel", "Attach")));
    expect(spies.onAttach).toHaveBeenCalledWith(LIVE_ID);
  });

  it("fires resume with the history item behind a finished row", () => {
    const { spies } = renderModal([row(PAST_ID)]);
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    expect(spies.onResume).toHaveBeenCalledWith(PAST);
  });

  it("fires view for a finished row", () => {
    const { spies } = renderModal([row(HEADLESS_ID)]);
    fireEvent.click(screen.getByText(i18n.t("run:runs.viewLabel")));
    expect(spies.onView).toHaveBeenCalledWith(HEADLESS);
  });

  it("filters by status and by free text", () => {
    const rows = [
      row("a", { status: "running", isLive: true, promptPreview: "alpha" }),
      row("b", { status: "killed", promptPreview: "bravo" }),
      row("c", { status: "abandoned", promptPreview: "charlie" }),
    ];
    renderModal(rows);

    fireEvent.click(chip(i18n.t("run:status.killed")));
    expect(screen.getByText("bravo")).toBeTruthy();
    expect(screen.queryByText("alpha")).toBeNull();

    fireEvent.click(allChip(0));
    fireEvent.change(
      screen.getByPlaceholderText(
        i18n.t("run:runs.searchPlaceholder", "Search prompt, cwd, model, or session id…")
      ),
      { target: { value: "alpha" } }
    );
    expect(screen.getByText("alpha")).toBeTruthy();
    expect(screen.queryByText("charlie")).toBeNull();
  });

  it("polls onRefresh while it is the foreground UI", () => {
    vi.useFakeTimers();
    try {
      const { spies } = renderModal([row("a")]);
      expect(spies.onRefresh).toHaveBeenCalledTimes(1);
      vi.advanceTimersByTime(4000);
      expect(spies.onRefresh).toHaveBeenCalledTimes(3);
    } finally {
      vi.useRealTimers();
    }
  });
});
