/**
 * #75 — the pairing code is uppercase hex (`3F7A9C21`), not digits:
 *  - the field must summon the text keyboard, not the numeric keypad;
 *  - a lowercase entry must reach `session.pair()` uppercased and space-free,
 *    because the server compares the code strictly.
 *
 * #77 lot F1 — the handshake becomes `{code, name}`:
 *  - dashes normalise away too, so a code retyped as `AB12-CD34` still pairs;
 *  - the device name is prefilled from a readable derivation, editable, and
 *    required; it is never a raw User-Agent.
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";

import { WebSession } from "@/adapters/http";
import type { FetchLike } from "@/adapters/http/httpInvoker";
import type { FlagStore } from "@/adapters/http/webSession";
import { PairingScreen } from "./PairingScreen";

function memStore(): FlagStore {
  const map = new Map<string, string>();
  return {
    getItem: (k) => map.get(k) ?? null,
    setItem: (k, v) => void map.set(k, v),
    removeItem: (k) => void map.delete(k),
  };
}

const okFetch: FetchLike = async () => ({
  ok: true,
  status: 200,
  json: async () => ({ ok: true }),
  text: async () => "{}",
});

/** A `/api/pair` that always fails with the given wire code. */
function failingFetch(status: number, code: string): FetchLike {
  return async () => ({
    ok: false,
    status,
    json: async () => ({ code, message: "server wording" }),
    text: async () => "{}",
  });
}

/** Pins the UA so the name prefill is deterministic. */
function stubUserAgent(ua: string): void {
  Object.defineProperty(window.navigator, "userAgent", {
    value: ua,
    configurable: true,
  });
}

const ORIGINAL_UA = window.navigator.userAgent;

function setup(fetchImpl: FetchLike = okFetch) {
  const session = new WebSession({ baseUrl: "https://h", fetchImpl, store: memStore() });
  const pair = vi.spyOn(session, "pair");
  const onPaired = vi.fn();
  render(<PairingScreen session={session} onPaired={onPaired} />);
  return { pair, onPaired };
}

const codeField = () => screen.getByLabelText("Code d'appairage");
const nameField = () => screen.getByLabelText("Nom de cet appareil") as HTMLInputElement;
const submit = () => screen.getByRole("button", { name: "Appairer" }) as HTMLButtonElement;

beforeEach(() => stubUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"));
afterEach(() => stubUserAgent(ORIGINAL_UA));

describe("PairingScreen code field", () => {
  it("asks for the text keyboard, not the numeric keypad", () => {
    setup();

    expect(codeField().getAttribute("inputmode")).toBe("text");
    expect(codeField().getAttribute("autocapitalize")).toBe("characters");
    expect(codeField().getAttribute("autocomplete")).toBe("one-time-code");
  });

  it("describes the code without lying about its shape", () => {
    setup();

    expect(codeField().getAttribute("placeholder")).toBe("p. ex. AB12CD34");
    expect(screen.getByText("8 caractères : chiffres et lettres A-F")).toBeTruthy();
  });

  it("uppercases and strips spaces before pairing", async () => {
    const { pair, onPaired } = setup();

    fireEvent.change(codeField(), { target: { value: " ab12cd34 " } });
    fireEvent.click(submit());

    await vi.waitFor(() => expect(onPaired).toHaveBeenCalled());
    expect(pair).toHaveBeenCalledWith("AB12CD34", "iPhone");
  });

  it("strips a dash the user retyped from the grouped display (#77)", async () => {
    const { pair, onPaired } = setup();

    fireEvent.change(codeField(), { target: { value: "AB12-CD34" } });
    fireEvent.click(submit());

    await vi.waitFor(() => expect(onPaired).toHaveBeenCalled());
    expect(pair).toHaveBeenCalledWith("AB12CD34", "iPhone");
  });
});

describe("PairingScreen device name", () => {
  it("prefills a readable name, never the raw User-Agent", () => {
    setup();

    expect(nameField().value).toBe("iPhone");
    expect(document.body.textContent).not.toContain("Mozilla/5.0");
  });

  it("sends the edited name with the code", async () => {
    const { pair, onPaired } = setup();

    fireEvent.change(codeField(), { target: { value: "AB12CD34" } });
    fireEvent.change(nameField(), { target: { value: "  Téléphone de Marie  " } });
    fireEvent.click(submit());

    await vi.waitFor(() => expect(onPaired).toHaveBeenCalled());
    expect(pair).toHaveBeenCalledWith("AB12CD34", "Téléphone de Marie");
  });

  it("caps the name at 40 characters", () => {
    setup();
    expect(nameField().maxLength).toBe(40);
  });

  it("requires both a code and a name", () => {
    setup();

    // A name alone (prefilled) is not enough.
    expect(submit().disabled).toBe(true);

    fireEvent.change(codeField(), { target: { value: "AB12CD34" } });
    expect(submit().disabled).toBe(false);

    // Clearing the name blocks it again: the list must never show a blank row.
    fireEvent.change(nameField(), { target: { value: "   " } });
    expect(submit().disabled).toBe(true);
  });

  it("keeps submit disabled for a blank or separator-only code", () => {
    setup();

    fireEvent.change(codeField(), { target: { value: "   " } });
    expect(submit().disabled).toBe(true);

    fireEvent.change(codeField(), { target: { value: " - " } });
    expect(submit().disabled).toBe(true);
  });
});

describe("PairingScreen error placement", () => {
  async function submitAndFail(status: number, code: string) {
    setup(failingFetch(status, code));
    fireEvent.change(codeField(), { target: { value: "AB12CD34" } });
    fireEvent.click(submit());
    await screen.findByRole("alert");
  }

  it("shows a rejected name on the name field, not as a form error", async () => {
    await submitAndFail(400, "invalid_name");

    // Attached to the field: the fix is one edit away, and the code is still
    // valid — a form-level error would read as "start over".
    const message = screen.getByRole("alert");
    expect(message.textContent).toBe(
      "Nom d'appareil invalide (1 à 40 caractères). Le code reste valable.",
    );
    expect(nameField().getAttribute("aria-describedby")).toBe(message.id);
    expect(nameField().getAttribute("aria-invalid")).toBe("true");
    expect(screen.queryByTestId("pairing-error")).toBeNull();
    // The code was never consumed, so it must not be painted as the culprit.
    expect(codeField().getAttribute("aria-invalid")).not.toBe("true");
  });

  it("keeps a rejected code as a form error", async () => {
    await submitAndFail(401, "invalid_or_expired");

    expect(screen.getByTestId("pairing-error").textContent).toBe("Code invalide ou expiré.");
    // The name is not at fault, so it must not be marked as such.
    expect(nameField().getAttribute("aria-invalid")).not.toBe("true");
  });
});
