/**
 * @file RunSetup.test.tsx
 * @description Pins the props-only boundary of `RunSetup` after its move out of
 * `pages/Run.tsx` (where it was `ConfigCard`): every picker the panel owns —
 * mode, prompt, cwd, model, permission mode, effort — reports its selection
 * through the matching callback and nowhere else, `onStart` fires from the Run
 * button, and a missing `claude` binary is surfaced purely from the
 * `binaryFound` prop (the panel runs no probe of its own).
 *
 * @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 { RunSetup } from "../RunSetup";
import type { CwdSuggestion } from "../../../lib/api";

vi.mock("../../../lib/api", async (importOriginal) => {
  const actual = await importOriginal<Record<string, unknown>>();
  const r = (value: unknown) => vi.fn().mockResolvedValue(value);
  return {
    ...actual,
    api: {
      run: { files: r({ items: [] }) },
      sessions: { list: r({ sessions: [], total: 0, limit: 100, offset: 0 }) },
      ccConfig: { file: r({ text: "" }) },
    },
  };
});

const SUGGESTIONS: CwdSuggestion[] = [
  { kind: "home", path: "/Users/tester", label: "Home" },
  { kind: "recent", path: "/Users/tester/projects/other", label: "other" },
];

type Spies = ReturnType<typeof renderSetup>["spies"];

function renderSetup(overrides: Partial<React.ComponentProps<typeof RunSetup>> = {}) {
  const spies = {
    onPromptChange: vi.fn(),
    onCwdChange: vi.fn(),
    onModelChange: vi.fn(),
    onPermissionModeChange: vi.fn(),
    onEffortChange: vi.fn(),
    onStart: vi.fn(),
    onResumeSessionChange: vi.fn(),
    onResumeFromHistory: vi.fn(),
  };
  const utils = render(
    <MemoryRouter>
      <RunSetup
        laneId={1}
        prompt="do the thing"
        cwd="/Users/tester"
        cwdSuggestions={SUGGESTIONS}
        model=""
        permissionMode="acceptEdits"
        effort=""
        binaryFound
        busy={false}
        activeRuns={null}
        resumeSession={null}
        slashCommands={[]}
        runHistory={[]}
        {...spies}
        {...overrides}
      />
    </MemoryRouter>
  );
  return { ...utils, spies };
}

/** Open the `Select` whose trigger currently shows `currentLabel`, then pick
 *  the option labelled `optionLabel`. */
function pickFromSelect(currentLabel: string, optionLabel: string) {
  fireEvent.click(screen.getByText(currentLabel));
  fireEvent.click(screen.getByText(optionLabel));
}

/** Every callback except the named ones must stay untouched — a selection that
 *  leaks into a sibling prop is exactly the wiring bug a move can introduce. */
function onlyCalled(spies: Spies, ...called: (keyof Spies)[]) {
  for (const [name, spy] of Object.entries(spies)) {
    if (called.includes(name as keyof Spies)) continue;
    expect(spy, `${name} should not have fired`).not.toHaveBeenCalled();
  }
}

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

describe("RunSetup — selections report through callbacks", () => {
  it("reports the prompt from the editor", () => {
    const { spies } = renderSetup({ prompt: "" });
    const box = screen.getByPlaceholderText(i18n.t("run:fields.promptPlaceholder"));
    fireEvent.change(box, { target: { value: "review the diff" } });
    expect(spies.onPromptChange).toHaveBeenCalledWith("review the diff");
    onlyCalled(spies, "onPromptChange");
  });

  it("reports the cwd from typing and from a suggestion", () => {
    const { spies } = renderSetup();
    const input = screen.getByPlaceholderText(i18n.t("run:fields.cwdPlaceholder"));
    fireEvent.change(input, { target: { value: "/tmp/pro" } });
    expect(spies.onCwdChange).toHaveBeenCalledWith("/tmp/pro");
    fireEvent.click(screen.getByText("/Users/tester/projects/other"));
    expect(spies.onCwdChange).toHaveBeenLastCalledWith("/Users/tester/projects/other");
    onlyCalled(spies, "onCwdChange");
  });

  it("reports the model from the picker, including a custom id", () => {
    const { spies } = renderSetup();
    pickFromSelect(i18n.t("run:fields.modelInheritLabel"), "Sonnet 4.6");
    expect(spies.onModelChange).toHaveBeenCalledWith("sonnet");
    // "Custom…" is a sentinel, not a model id — it must not be reported as one;
    // the free-text box it reveals is what reports.
    fireEvent.click(screen.getByText(i18n.t("run:fields.modelInheritLabel")));
    fireEvent.click(screen.getByText(i18n.t("run:fields.modelCustom")));
    expect(spies.onModelChange).toHaveBeenCalledTimes(1);
    fireEvent.change(screen.getByPlaceholderText(i18n.t("run:fields.modelCustomPlaceholder")), {
      target: { value: "claude-opus-5" },
    });
    expect(spies.onModelChange).toHaveBeenLastCalledWith("claude-opus-5");
    onlyCalled(spies, "onModelChange");
  });

  it("reports the permission mode and the effort level", () => {
    const { spies } = renderSetup();
    pickFromSelect(i18n.t("run:fields.permissionAcceptEdits"), i18n.t("run:fields.permissionPlan"));
    expect(spies.onPermissionModeChange).toHaveBeenCalledWith("plan");
    pickFromSelect("Default (model decides)", "Medium");
    expect(spies.onEffortChange).toHaveBeenCalledWith("medium");
    onlyCalled(spies, "onPermissionModeChange", "onEffortChange");
  });

  it("fires onStart from the Run button", () => {
    const { spies } = renderSetup();
    fireEvent.click(screen.getByText(i18n.t("run:actions.start")));
    expect(spies.onStart).toHaveBeenCalledTimes(1);
    onlyCalled(spies, "onStart");
  });
});

describe("RunSetup — missing binary and other blocked states", () => {
  /** The Run button, found by its label rather than by DOM position. */
  function runButton(): HTMLButtonElement {
    return screen.getByText(i18n.t("run:actions.start")).closest("button") as HTMLButtonElement;
  }

  it("disables Run when the claude binary was not found", () => {
    renderSetup({ binaryFound: false });
    expect(runButton().disabled).toBe(true);
  });

  it("enables Run when the binary is found and the form is complete", () => {
    renderSetup();
    expect(runButton().disabled).toBe(false);
  });

  it("still disables Run without a prompt or without a cwd", () => {
    const { unmount } = renderSetup({ prompt: "   " });
    expect(runButton().disabled).toBe(true);
    unmount();

    const noCwd = renderSetup({ cwd: "" });
    expect(runButton().disabled).toBe(true);
    noCwd.unmount();
  });

  it("shows the Starting… label while busy", () => {
    renderSetup({ busy: true });
    expect(screen.getByText(i18n.t("run:actions.starting"))).toBeTruthy();
  });
});

describe("RunSetup — resume picker scopes sessions to the selected lane", () => {
  it("passes the lane's cwd as a filter and lists only that directory's sessions", async () => {
    const { api } = await import("../../../lib/api");
    vi.mocked(api.sessions.list).mockResolvedValue({
      sessions: [
        { id: "sess-in-lane", cwd: "/Users/tester/lane-a", started_at: "", status: "completed" },
      ],
      total: 1,
      limit: 100,
      offset: 0,
    } as never);

    renderSetup({ laneCwd: "/Users/tester/lane-a" });
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession")));

    await screen.findByText("/Users/tester/lane-a");
    // The API call itself is what enforces the scope - the server filters by
    // cwd, so the picker must never fetch without one when a lane is selected.
    expect(api.sessions.list).toHaveBeenCalledWith(
      expect.objectContaining({ cwd: "/Users/tester/lane-a" })
    );
  });

  it("starts the resume immediately when a session is picked", async () => {
    const { api } = await import("../../../lib/api");
    vi.mocked(api.sessions.list).mockResolvedValue({
      sessions: [
        { id: "sess-in-lane", cwd: "/Users/tester/lane-a", started_at: "", status: "completed" },
      ],
      total: 1,
      limit: 100,
      offset: 0,
    } as never);

    const { spies } = renderSetup({ laneCwd: "/Users/tester/lane-a", prompt: "" });
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession")));
    fireEvent.click(await screen.findByText("/Users/tester/lane-a"));

    expect(spies.onResumeSessionChange).toHaveBeenCalledWith(
      expect.objectContaining({ id: "sess-in-lane" })
    );
    // No prompt typed, no Run click - the pick itself is the start, and it
    // carries the session's own cwd rather than the form's.
    expect(spies.onStart).toHaveBeenCalledWith(
      expect.objectContaining({ resumeSessionId: "sess-in-lane", cwd: "/Users/tester/lane-a" })
    );
  });

  it("falls back to the typed cwd when no lane is selected", async () => {
    const { api } = await import("../../../lib/api");
    renderSetup({ laneCwd: undefined, cwd: "/Users/tester" });
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession")));

    await new Promise((r) => setTimeout(r, 0));
    expect(api.sessions.list).toHaveBeenCalledWith(
      expect.objectContaining({ cwd: "/Users/tester" })
    );
  });

  it("lists everything when no lane is selected and cwd is empty", async () => {
    const { api } = await import("../../../lib/api");
    renderSetup({ laneCwd: undefined, cwd: "" });
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession")));

    await new Promise((r) => setTimeout(r, 0));
    expect(api.sessions.list).toHaveBeenCalledWith(expect.objectContaining({ cwd: undefined }));
  });

  it("re-fetches with the new cwd when the selected lane changes", async () => {
    const { api } = await import("../../../lib/api");
    const { rerender } = renderSetup({ laneCwd: "/Users/tester/lane-a" });
    fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
    fireEvent.click(screen.getByText(i18n.t("run:resume.pickSession")));
    await new Promise((r) => setTimeout(r, 0));

    rerender(
      <MemoryRouter>
        <RunSetup
          laneId={1}
          prompt="do the thing"
          cwd="/Users/tester"
          cwdSuggestions={SUGGESTIONS}
          model=""
          permissionMode="acceptEdits"
          effort=""
          binaryFound
          busy={false}
          activeRuns={null}
          resumeSession={null}
          slashCommands={[]}
          runHistory={[]}
          laneCwd="/Users/tester/lane-b"
          onPromptChange={vi.fn()}
          onCwdChange={vi.fn()}
          onModelChange={vi.fn()}
          onPermissionModeChange={vi.fn()}
          onEffortChange={vi.fn()}
          onStart={vi.fn()}
          onResumeSessionChange={vi.fn()}
          onResumeFromHistory={vi.fn()}
        />
      </MemoryRouter>
    );
    await new Promise((r) => setTimeout(r, 0));

    expect(api.sessions.list).toHaveBeenCalledWith(
      expect.objectContaining({ cwd: "/Users/tester/lane-b" })
    );
  });
});
