/**
 * Ticket #68 — `Settings → Deployment`, driven through the real `DIProvider`
 * and the mock gateway (no backend).
 *
 * These pin the UX invariants the screen exists for, not its styling: the mode
 * is a radio choice with consequences, the authorized-proxy field is explicitly
 * *not* a listen address, addresses come from the backend, a refusal is
 * actionable, and no pairing code is ever shown here (#77).
 *
 * Labels asserted here are French (ticket #78 — UX decision: uniform French UI
 * per surface, technical acronyms like URL/LAN/IP/CIDR/HTTPS excepted).
 */

import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor, fireEvent, within } from "@testing-library/react";

import { MockDesktopServerGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { DeploymentSettings } from "./DeploymentSettings";

function renderView(desktopServer = new MockDesktopServerGateway()) {
  const gateways = { desktopServer } as unknown as Gateways;
  return {
    desktopServer,
    ...render(
      <DIProvider gateways={gateways}>
        <DeploymentSettings />
      </DIProvider>,
    ),
  };
}

/** Waits past the hook's preview debounce. */
async function settle() {
  await screen.findByRole("radiogroup", { name: "mode d'accès réseau" });
  await waitFor(() => expect(screen.getByLabelText("Port")).toBeTruthy());
}

function selectMode(title: string) {
  fireEvent.click(screen.getByRole("radio", { name: new RegExp(title) }));
}

describe("DeploymentSettings", () => {
  it("offers the three exposure modes as radio cards with their consequences", async () => {
    renderView();
    await settle();

    const group = screen.getByRole("radiogroup", { name: "mode d'accès réseau" });
    expect(within(group).getAllByRole("radio")).toHaveLength(3);

    // The plain-language consequence is part of the choice, not a tooltip.
    expect(
      within(group).getByText(/ne peuvent pas se connecter/),
    ).toBeTruthy();
    expect(
      within(group).getByText(/même machine qu'IdeA Desktop/),
    ).toBeTruthy();
    expect(
      within(group).getByText(/n'acceptera que le trafic provenant de ce proxy/),
    ).toBeTruthy();
  });

  it("shows no exposure fields in 'Cet ordinateur uniquement'", async () => {
    renderView();
    await settle();

    // Default is localOnly: nothing to configure, nothing to get wrong.
    expect(screen.queryByLabelText("Origine publique")).toBeNull();
    expect(screen.queryByLabelText("IP/CIDR du proxy autorisé")).toBeNull();
    expect(screen.queryByLabelText("Adresse LAN d'écoute")).toBeNull();
  });

  it("asks only for a public origin when the proxy is on this computer", async () => {
    renderView();
    await settle();
    selectMode("Accès distant, proxy sur cet ordinateur");

    expect(await screen.findByLabelText("Origine publique")).toBeTruthy();
    // The proxy is local, so there is nothing to authorize and nothing to bind.
    expect(screen.queryByLabelText("IP/CIDR du proxy autorisé")).toBeNull();
    expect(screen.queryByLabelText("Adresse LAN d'écoute")).toBeNull();
  });

  it("explains that the authorized proxy is not a listen address, and warns about the mode", async () => {
    renderView();
    await settle();
    selectMode("Accès distant, proxy sur une autre machine");

    // This help text is the whole point of the screen: it corrects the
    // "that's where IdeA listens" misreading.
    expect(
      await screen.findByText(
        "Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA.",
      ),
    ).toBeTruthy();

    // The permanent warning about the silent-timeout failure mode.
    expect(
      screen.getByText(/le proxy peut expirer sans erreur IdeA/),
    ).toBeTruthy();
  });

  it("offers LAN addresses from the backend rather than deriving any", async () => {
    const gateway = new MockDesktopServerGateway();
    const preview = vi.spyOn(gateway, "previewExposure");
    renderView(gateway);
    await settle();
    selectMode("Accès distant, proxy sur une autre machine");

    const select = await screen.findByLabelText("Adresse LAN d'écoute");
    const offered = within(select as HTMLElement)
      .getAllByRole("option")
      .map((o) => (o as HTMLOptionElement).value)
      .filter(Boolean);

    // Exactly the gateway's candidates — the UI invents nothing.
    const fromBackend = (await preview.mock.results[0]!.value).candidateLanAddresses;
    expect(offered).toEqual(fromBackend);
  });

  it("surfaces the backend's refusal as a concrete correction", async () => {
    renderView();
    await settle();
    // A remote mode with no origin: the backend says exactly what to fix.
    selectMode("Accès distant, proxy sur cet ordinateur");

    const alert = await screen.findByRole("alert");
    expect(alert.textContent).toMatch(/requires publicOrigin/);
  });

  it("shows the upstream to paste, read-only, once the config is valid", async () => {
    renderView();
    await settle();
    selectMode("Accès distant, proxy sur cet ordinateur");
    fireEvent.change(await screen.findByLabelText("Origine publique"), {
      target: { value: "https://idea.example.com" },
    });

    const upstream = await screen.findByRole("button", {
      name: "copier l'URL upstream",
    });
    expect(upstream).toBeTruthy();
    // The upstream is IdeA-provided, never a field the user edits.
    expect(screen.queryByRole("textbox", { name: /upstream/i })).toBeNull();
  });

  it("never shows a pairing code, running or not (#77)", async () => {
    // A code is no longer a property of a running server: it exists only when
    // asked for. Starting the server must not make one appear here — this is
    // what the old "runtime-only code" test asserted, and it can no longer be
    // true of any backend response.
    renderView();
    await settle();

    expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();

    fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
    await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());

    expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
    expect(screen.queryByText(/generate a pairing code/i)).toBeNull();
  });

  it("points to where pairing now lives instead of dead-ending", async () => {
    // Someone who just started the server from this screen needs to know where
    // to go next; silence would be a cul-de-sac.
    renderView();
    await settle();

    const pairing = screen.getByText(/L'appairage est géré dans/);
    expect(within(pairing).getByText("Paramètres → Appareils")).toBeTruthy();
  });

  it("starts the server and reports the local URL", async () => {
    renderView();
    await settle();
    fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));

    await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
    expect(screen.getByText(/URL locale :/)).toBeTruthy();
  });

  it("persists the draft before starting, so what runs is what is shown", async () => {
    const gateway = new MockDesktopServerGateway();
    const save = vi.spyOn(gateway, "saveExposureSettings");
    renderView(gateway);
    await settle();

    fireEvent.change(screen.getByLabelText("Port"), { target: { value: "18080" } });
    fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));

    await waitFor(() => expect(save).toHaveBeenCalled());
    expect(save.mock.calls[0]![0]).toMatchObject({ port: 18080 });
    await waitFor(() =>
      expect(screen.getByText("http://127.0.0.1:18080")).toBeTruthy(),
    );
  });

  it("reports a failed start with the backend message", async () => {
    const gateway = new MockDesktopServerGateway();
    renderView(gateway);
    await settle();

    gateway.setStatus({
      state: "failed",
      error: { code: "INVALID", message: "port 17373 already in use" },
    });

    await waitFor(() => expect(screen.getByText("Échec")).toBeTruthy());
    expect(screen.getByRole("alert").textContent).toMatch(/already in use/);
  });
});
