/**
 * @file RunConsole.test.tsx
 * @description Pins the props-only boundary of `RunConsole` after its move out
 * of `pages/Run.tsx`: the envelope stream renders from the `envelopes` prop
 * (no stream subscription of its own), the token meter rolls up usage from
 * those same envelopes, the prompt editor's `/` autocomplete filters and fills
 * the prompt through `onFollowUpChange`, and `onSend` / `onStop` fire from the
 * send and stop controls.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { useState } from "react";
import { MemoryRouter } from "react-router-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { RunConsole, type SlashCommand } from "../RunConsole";
import type { Envelope } from "../../../hooks/useRunStream";
import type { RunHandle } from "../../../lib/api";

const HANDLE: RunHandle = {
  id: "run-1",
  pid: 4242,
  mode: "conversation",
  cwd: "/tmp/project",
  model: "claude-opus-5",
  permissionMode: "acceptEdits",
  effort: "",
  prompt: "hi",
  argv: [],
  resumeSessionId: null,
  status: "running",
  startedAt: 1,
  endedAt: null,
  exitCode: null,
  signal: null,
  error: null,
  sessionId: null,
  envelopeCount: 0,
  stdoutTail: "",
  stderrTail: "",
};

const COMMANDS: SlashCommand[] = [
  { name: "code-review", description: "Review the working diff", source: "project" },
  { name: "compact", description: "Compact the conversation context", source: "builtin" },
  { name: "logout", description: "Sign out", source: "builtin" },
];

/**
 * Mount the console with the parent-owned follow-up state it expects, so the
 * autocomplete assertions exercise the real controlled-input round trip.
 */
function renderConsole(
  props: Partial<React.ComponentProps<typeof RunConsole>> = {},
  onFollowUp?: (s: string) => void
) {
  const seen = { followUp: "" };
  function Harness() {
    const [followUp, setFollowUp] = useState("");
    seen.followUp = followUp;
    return (
      <RunConsole
        handle={HANDLE}
        envelopes={[]}
        mode="conversation"
        isLive
        hasFinished={false}
        followUp={followUp}
        onFollowUpChange={(s) => {
          setFollowUp(s);
          onFollowUp?.(s);
        }}
        busy={null}
        onSend={() => {}}
        onStop={() => {}}
        onNewRun={() => {}}
        slashCommands={COMMANDS}
        {...props}
      />
    );
  }
  render(
    <MemoryRouter>
      <Harness />
    </MemoryRouter>
  );
  return seen;
}

describe("RunConsole", () => {
  it("renders assistant text from the envelopes prop", () => {
    const envelopes: Envelope[] = [
      { type: "user", message: { content: "explain this repo" } },
      { type: "assistant", message: { content: [{ type: "text", text: "Here is the answer." }] } },
    ] as Envelope[];
    renderConsole({ envelopes });

    expect(screen.getByText("explain this repo")).toBeInTheDocument();
    expect(screen.getByText("Here is the answer.")).toBeInTheDocument();
  });

  it("shows the empty-stream placeholder when there are no envelopes", () => {
    renderConsole({ isLive: false });
    expect(screen.getByText("Nothing yet")).toBeInTheDocument();
  });

  it("shows the token totals computed from the envelopes", () => {
    // Transcript-shaped assistant envelope (no `message.id`), which is the
    // branch computeTokens folds into the running totals.
    const envelopes: Envelope[] = [
      {
        type: "assistant",
        message: {
          content: [{ type: "text", text: "done" }],
          usage: { input_tokens: 12_000, output_tokens: 2_500, cache_read_input_tokens: 8_000 },
        },
      },
    ] as Envelope[];
    renderConsole({ envelopes });

    // Context gauge: (input + cache read) / default 200k window.
    // The CLI-style meter is one status line: context usage as a single label,
    // then output and cache-hit figures with terminal glyphs. Input is implied
    // by the context total rather than listed separately.
    expect(screen.getByText("20.0k / 200k (10%)")).toBeInTheDocument();
    expect(screen.getByText("↑2.5k")).toBeInTheDocument(); // Output
    expect(screen.getByText("⚡8.0k")).toBeInTheDocument(); // Cache hit
  });

  it("filters slash commands as the user types and fills the prompt on pick", () => {
    const seen = renderConsole();
    const textarea = screen.getByRole("textbox");

    fireEvent.change(textarea, { target: { value: "/co" } });

    expect(screen.getByText("/code-review")).toBeInTheDocument();
    expect(screen.getByText("/compact")).toBeInTheDocument();
    expect(screen.queryByText("/logout")).not.toBeInTheDocument();

    fireEvent.click(screen.getByText("/code-review"));

    expect(seen.followUp).toBe("/code-review");
    expect(screen.queryByText("/compact")).not.toBeInTheDocument(); // dropdown closed
  });

  it("fires onSend from the send button with the prompt the parent holds", () => {
    const onSend = vi.fn();
    const seen = renderConsole({ onSend });

    fireEvent.change(screen.getByRole("textbox"), { target: { value: "follow up please" } });
    fireEvent.click(screen.getByRole("button", { name: /send/i }));

    expect(onSend).toHaveBeenCalledTimes(1);
    expect(seen.followUp).toBe("follow up please");
  });

  it("fires onStop from the stop control while live, and hides it when not", () => {
    const onStop = vi.fn();
    renderConsole({ onStop });

    fireEvent.click(screen.getByRole("button", { name: /stop/i }));
    expect(onStop).toHaveBeenCalledTimes(1);
  });

  it("hides the stop control and the follow-up editor once the run is not live", () => {
    renderConsole({ isLive: false, hasFinished: true });

    expect(screen.queryByRole("button", { name: /stop/i })).not.toBeInTheDocument();
    expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
  });
});
