/**
 * L5 — the first-run wizard wired to the stateful {@link MockProfileGateway} via
 * the real {@link DIProvider}. Covers: pre-filled editable rows (only the
 * selectable Claude/Codex profiles, §17.3/D7), detection ✓/✗, the **absence** of
 * any custom-profile block, and finishing (configure ⇒ first run closed ⇒ onDone).
 */
import { describe, it, expect, vi } from "vitest";
import {
  act,
  render,
  screen,
  waitFor,
  fireEvent,
} from "@testing-library/react";

import { MockProfileGateway } from "@/adapters/mock";
import type { ProfileAvailability } from "@/domain";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { FirstRunWizard } from "./FirstRunWizard";
import { DETECT_TIMEOUT_MS } from "./useFirstRun";

function renderWizard(
  profile: MockProfileGateway = new MockProfileGateway(),
  onDone = vi.fn(),
) {
  const gateways = { profile } as unknown as Gateways;
  return {
    profile,
    onDone,
    ...render(
      <DIProvider gateways={gateways}>
        <FirstRunWizard onDone={onDone} />
      </DIProvider>,
    ),
  };
}

async function waitForLoaded() {
  await screen.findByLabelText("first run setup");
}

describe("FirstRunWizard (with MockProfileGateway)", () => {
  it("renders only the selectable (Claude/Codex) pre-filled, editable profiles", async () => {
    renderWizard();
    await waitForLoaded();

    // Only structured-drivable profiles are offered (§17.3/D7).
    for (const name of ["Claude Code", "OpenAI Codex CLI"]) {
      expect(screen.getByText(name)).toBeTruthy();
    }
    // Gemini/Aider are no longer proposed for selection.
    expect(screen.queryByText("Gemini CLI")).toBeNull();
    expect(screen.queryByText("Aider")).toBeNull();
    expect(screen.queryByLabelText("Aider command")).toBeNull();

    // Commands are editable inputs, pre-filled.
    const claudeCmd = screen.getByLabelText(
      "Claude Code command",
    ) as HTMLInputElement;
    expect(claudeCmd.value).toBe("claude");
  });

  it("editing a command updates the input value", async () => {
    renderWizard();
    await waitForLoaded();
    const cmd = screen.getByLabelText(
      "OpenAI Codex CLI command",
    ) as HTMLInputElement;
    fireEvent.change(cmd, { target: { value: "codex-2" } });
    expect(cmd.value).toBe("codex-2");
  });

  it("detection shows ✓ for claude and ✗ for the rest", async () => {
    renderWizard();
    await waitForLoaded();

    fireEvent.click(screen.getByRole("button", { name: "Detect installed CLIs" }));

    await waitFor(() => {
      expect(
        screen.getByLabelText("Claude Code availability").textContent,
      ).toMatch(/installed/);
    });
    expect(
      screen.getByLabelText("OpenAI Codex CLI availability").textContent,
    ).toMatch(/not found/);
  });

  // §17.3/D7 — anti-regression guard. The custom-profile block was removed
  // because we cannot drive an arbitrary command in structured mode. This test
  // asserts the *absence* of that block: it must fail the instant any
  // `AddCustomProfile` UI (its label/role/inputs) reappears.
  it("offers no custom-profile block (removed in D7)", async () => {
    renderWizard();
    await waitForLoaded();

    expect(screen.queryByLabelText("add custom profile")).toBeNull();
    expect(
      screen.queryByRole("button", { name: "Add custom profile" }),
    ).toBeNull();
    expect(screen.queryByLabelText("custom name")).toBeNull();
    expect(screen.queryByLabelText("custom command")).toBeNull();
  });

  it("auto-detects on open and pre-checks only installed CLIs", async () => {
    renderWizard();
    await waitForLoaded();

    // The mock reports only `claude` as installed → only it is pre-checked.
    await waitFor(() =>
      expect(
        (screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
      ).toBe(true),
    );
    expect(
      (screen.getByLabelText("use OpenAI Codex CLI") as HTMLInputElement).checked,
    ).toBe(false);
    // Availability was filled automatically, without clicking the button.
    expect(
      screen.getByLabelText("Claude Code availability").textContent,
    ).toMatch(/installed/);
  });

  it("finishing persists the auto-selected (installed) profiles and closes the first run", async () => {
    const { profile, onDone } = renderWizard();
    await waitForLoaded();
    await waitFor(() =>
      expect(
        (screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
      ).toBe(true),
    );

    fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));

    await waitFor(() => expect(onDone).toHaveBeenCalled());
    await waitFor(() =>
      expect(screen.queryByLabelText("first run setup")).toBeNull(),
    );

    // Only the installed CLI was pre-selected, so only it is persisted.
    const saved = await profile.listProfiles();
    expect(saved.map((p) => p.command)).toEqual(["claude"]);
    expect((await profile.firstRunState()).isFirstRun).toBe(false);
  });

  it("ticking an extra profile persists it alongside the installed ones", async () => {
    const { profile } = renderWizard();
    await waitForLoaded();
    await waitFor(() =>
      expect(
        (screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
      ).toBe(true),
    );

    // Keep Codex too (not installed, unchecked by default).
    fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI"));
    fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      expect(saved.map((p) => p.command)).toEqual(["claude", "codex"]);
    });
  });

  it("renders nothing when it is not the first run", async () => {
    const profile = new MockProfileGateway();
    await profile.configureProfiles([]); // closes first run
    const { container } = renderWizard(profile);

    // No wizard section ever appears.
    await waitFor(() =>
      expect(screen.queryByLabelText("first run setup")).toBeNull(),
    );
    expect(container.querySelector("section")).toBeNull();
  });
});

describe("FirstRunWizard — OpenAI-compatible local/LAN profile (ticket #14)", () => {
  const OLLAMA = "Ollama / OpenAI-compatible local model";

  it("offers the local model as a selectable, pre-filled HTTP config row", async () => {
    renderWizard();
    await waitForLoaded();

    expect(screen.getByText(OLLAMA)).toBeTruthy();
    // The structured HTTP fields are rendered, pre-filled from the reference.
    expect(
      (screen.getByLabelText(`${OLLAMA} endpoint`) as HTMLInputElement).value,
    ).toBe("http://localhost:11434/v1");
    expect(
      (screen.getByLabelText(`${OLLAMA} model`) as HTMLInputElement).value,
    ).toBe("qwen2.5-coder");
    // Claude/Codex rows must NOT grow HTTP fields (additive, no regression).
    expect(screen.queryByLabelText("Claude Code endpoint")).toBeNull();
  });

  it("labels the API key field as an env var NAME and flags a raw key", async () => {
    renderWizard();
    await waitForLoaded();

    const apiKey = screen.getByLabelText(`${OLLAMA} api key env`) as HTMLInputElement;
    // The label/placeholder make the env-var-name intent explicit.
    expect(apiKey.placeholder.toLowerCase()).toContain("variable name");

    // A raw secret is not a valid env var identifier ⇒ inline error.
    fireEvent.change(apiKey, { target: { value: "sk-secret-123" } });
    expect(
      screen.getByText(/valid env var name \(not the key itself\)/i),
    ).toBeTruthy();

    // A proper env var name clears the error.
    fireEvent.change(apiKey, { target: { value: "OPENAI_API_KEY" } });
    expect(
      screen.queryByText(/valid env var name \(not the key itself\)/i),
    ).toBeNull();
  });

  it("flags an invalid endpoint scheme inline", async () => {
    renderWizard();
    await waitForLoaded();

    const endpoint = screen.getByLabelText(`${OLLAMA} endpoint`);
    fireEvent.change(endpoint, { target: { value: "ftp://oops" } });
    expect(screen.getByText(/must start with http:\/\/ or https:\/\//i)).toBeTruthy();
  });

  it("exposes the timeout / tool-guard fields pre-filled from the reference", async () => {
    renderWizard();
    await waitForLoaded();

    expect(
      (screen.getByLabelText(`${OLLAMA} request timeout`) as HTMLInputElement).value,
    ).toBe("120000");
    expect(
      (screen.getByLabelText(`${OLLAMA} connect timeout`) as HTMLInputElement).value,
    ).toBe("5000");
    expect(
      (screen.getByLabelText(`${OLLAMA} max tool iterations`) as HTMLInputElement)
        .value,
    ).toBe("16");
  });

  it("persists edited timeouts / tool-guard round-trip into chatHttp", async () => {
    const { profile } = renderWizard();
    await waitForLoaded();

    fireEvent.click(screen.getByLabelText(`use ${OLLAMA}`));
    fireEvent.change(screen.getByLabelText(`${OLLAMA} request timeout`), {
      target: { value: "90000" },
    });
    fireEvent.change(screen.getByLabelText(`${OLLAMA} connect timeout`), {
      target: { value: "3000" },
    });
    fireEvent.change(screen.getByLabelText(`${OLLAMA} max tool iterations`), {
      target: { value: "8" },
    });

    fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      const ollama = saved.find((p) => p.command === "openai-compatible");
      expect(ollama?.chatHttp?.requestTimeoutMs).toBe(90000);
      expect(ollama?.chatHttp?.connectTimeoutMs).toBe(3000);
      expect(ollama?.chatHttp?.maxToolIterations).toBe(8);
    });
  });

  it("flags a zero timeout inline (mirror of the backend guard)", async () => {
    renderWizard();
    await waitForLoaded();

    fireEvent.change(screen.getByLabelText(`${OLLAMA} request timeout`), {
      target: { value: "0" },
    });
    expect(screen.getByText(/positive integer \(ms\)/i)).toBeTruthy();
  });

  it("persists the edited HTTP config round-trip when selected and saved", async () => {
    const { profile } = renderWizard();
    await waitForLoaded();

    // Select the local model and edit its model name.
    fireEvent.click(screen.getByLabelText(`use ${OLLAMA}`));
    fireEvent.change(screen.getByLabelText(`${OLLAMA} model`), {
      target: { value: "qwen3" },
    });
    fireEvent.change(screen.getByLabelText(`${OLLAMA} api key env`), {
      target: { value: "LAN_KEY" },
    });

    fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));

    await waitFor(async () => {
      const saved = await profile.listProfiles();
      const ollama = saved.find((p) => p.command === "openai-compatible");
      expect(ollama?.structuredAdapter).toBe("openAiCompatible");
      expect(ollama?.chatHttp?.model).toBe("qwen3");
      expect(ollama?.chatHttp?.apiKeyEnv).toBe("LAN_KEY");
      // The endpoint round-trips untouched.
      expect(ollama?.chatHttp?.endpoint).toBe("http://localhost:11434/v1");
    });
  });
});

// Ticket #28 — the wizard must survive a detection that never answers. When the
// backend `detect_profiles` command panics, the `invoke` promise is neither
// resolved nor rejected: nothing after `await detectProfiles(...)` ever runs.
// `busy` must therefore never be tied to detection, or both buttons stay greyed
// out for good and the first run cannot be completed.
describe("FirstRunWizard — detection that never answers (ticket #28)", () => {
  /** A gateway whose `detectProfiles` promise stays pending forever. */
  class HangingDetectGateway extends MockProfileGateway {
    override detectProfiles(): Promise<ProfileAvailability[]> {
      return new Promise<ProfileAvailability[]>(() => {});
    }
  }

  it("keeps Save and Detect enabled once firstRunState answered", async () => {
    renderWizard(new HangingDetectGateway());
    await waitForLoaded();

    // The rows are rendered, so the blocking load is over: both actions are live.
    const save = screen.getByRole("button", { name: "Save and continue" });
    const detect = screen.getByRole("button", { name: "Detect installed CLIs" });
    expect((save as HTMLButtonElement).disabled).toBe(false);
    expect((detect as HTMLButtonElement).disabled).toBe(false);

    // Detection is merely reported as in flight, never as a blocking state.
    expect(screen.getByRole("status").textContent).toMatch(/detecting/i);

    // Re-probing by hand must not freeze them either.
    fireEvent.click(detect);
    expect((save as HTMLButtonElement).disabled).toBe(false);
    expect((detect as HTMLButtonElement).disabled).toBe(false);
  });

  it("still finishes the first run while detection hangs", async () => {
    const profile = new HangingDetectGateway();
    const { onDone } = renderWizard(profile, vi.fn());
    await waitForLoaded();

    // Nothing got pre-checked (detection never answered): tick a profile by hand.
    fireEvent.click(screen.getByLabelText("use Claude Code"));
    fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));

    await waitFor(() => expect(onDone).toHaveBeenCalled());
    const saved = await profile.listProfiles();
    expect(saved.map((p) => p.command)).toEqual(["claude"]);
  });

  it("drops the detecting flag on timeout even if the promise never settles", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    try {
      renderWizard(new HangingDetectGateway());
      await waitForLoaded();
      expect(screen.getByRole("status").textContent).toMatch(/detecting/i);

      await act(async () => {
        vi.advanceTimersByTime(DETECT_TIMEOUT_MS + 1);
      });
      expect(screen.queryByRole("status")).toBeNull();
    } finally {
      vi.useRealTimers();
    }
  });
});

describe("FirstRunWizard reopening after the first run (forceOpen)", () => {
  /** A gateway whose first run is already done (profiles configured). */
  async function configuredGateway() {
    const profile = new MockProfileGateway();
    await profile.configureProfiles([]); // marks first run as done
    return profile;
  }

  it("stays hidden once the first run is done (default)", async () => {
    const profile = await configuredGateway();
    const gateways = { profile } as unknown as Gateways;
    render(
      <DIProvider gateways={gateways}>
        <FirstRunWizard />
      </DIProvider>,
    );
    // Give the async first-run state time to resolve to `false`.
    await waitFor(() =>
      expect(screen.queryByLabelText("first run setup")).toBeNull(),
    );
  });

  it("renders when forced open (Settings ▸ Configure profiles)", async () => {
    const profile = await configuredGateway();
    const gateways = { profile } as unknown as Gateways;
    render(
      <DIProvider gateways={gateways}>
        <FirstRunWizard forceOpen />
      </DIProvider>,
    );
    expect(await screen.findByLabelText("first run setup")).toBeTruthy();
  });
});
