// api-tester.jsx — Postman-like saved-request module. Per-user private,
// isolated from FlowBoard's project/task data. Three panes:
//
//   ┌──────────────┬────────────────────────────────────────────────┐
//   │ COLLECTIONS  │  Method ▾  URL ─────────────────────────  Send │
//   │              │  Params · Headers · Body · Auth                │
//   │  + New       │  ─────────────────────────────────             │
//   │  ▸ Stripe    │  Response                                      │
//   │    GET ...   │  200 OK · 142ms · 4.3KB                        │
//   │    POST ..   │  Headers · Body                                │
//   │  ▸ Internal  │                                                │
//   └──────────────┴────────────────────────────────────────────────┘
//
// All persistence flows through api.apiTester.* — see public/src/api.js.

const API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
const API_AUTH_TYPES = [
  { id: "none",   label: "No auth" },
  { id: "bearer", label: "Bearer token" },
  { id: "basic",  label: "Basic auth" },
  { id: "apikey", label: "API key (header)" },
  { id: "oauth2", label: "OAuth 2.0" },
];
const API_BODY_TYPES = [
  { id: "none",       label: "None" },
  { id: "json",       label: "JSON" },
  { id: "raw",        label: "Raw text" },
  { id: "urlencoded", label: "x-www-form-urlencoded" },
];

function _newHeaderRow() { return { key: "", value: "", enabled: true }; }
function _newCaptureRow() { return { var: "", source: "body", path: "", enabled: true }; }

// Tiny JSON path walker — supports `$.foo.bar`, `foo.bar`,
// `data.items[0].id`, `[0].name`. Plenty for "pull a token out of a
// login response". Not a full JSONPath; we deliberately avoid the
// jsonpath-plus baggage. Returns undefined when the path doesn't
// resolve so the capture rule reports "not found".
function _jsonPath(obj, path) {
  if (obj == null || !path) return undefined;
  // Strip leading `$.` or `$`
  const cleaned = String(path).replace(/^\$\.?/, "").trim();
  if (!cleaned) return obj;
  // Tokenize by `.` and `[N]`. Empty tokens (from leading [) get
  // filtered. Numeric tokens index arrays; everything else is a key.
  const tokens = cleaned
    .replace(/\[(\d+)\]/g, ".$1")
    .split(".")
    .filter(Boolean);
  let cur = obj;
  for (const t of tokens) {
    if (cur == null) return undefined;
    cur = cur[t];
  }
  return cur;
}

// Coerce captured values to strings for storage in the env (vars are
// substituted into URL/header/body strings via `{{name}}` so anything
// non-string would JSON-stringify into a confusing literal).
function stringifyValues(map) {
  const out = {};
  for (const k of Object.keys(map)) {
    const v = map[k];
    if (v == null) continue;
    out[k] = (typeof v === "string") ? v : (typeof v === "object" ? JSON.stringify(v) : String(v));
  }
  return out;
}
function _emptyRequest(name = "New request") {
  return {
    id: null, name,
    method: "GET",
    url: "",
    headers: [_newHeaderRow()],
    body: "",
    body_type: "none",
    auth_type: "none",
    auth_data: {},
    // Post-response capture rules. Each row pulls a value out of the
    // response and writes it to the active environment so the next
    // request can use it via {{name}}.
    capture: [],
  };
}

function ApiTesterView() {
  const [collections, setCollections] = React.useState([]);
  const [activeCid, setActiveCid] = React.useState(null);
  const [activeCollection, setActiveCollection] = React.useState(null); // full obj with requests + envs
  const [activeRid, setActiveRid] = React.useState(null);
  const [draft, setDraft] = React.useState(_emptyRequest());
  const [response, setResponse] = React.useState(null); // { status, headers, body, duration_ms, bytes, truncated }
  const [running, setRunning] = React.useState(false);
  const [loading, setLoading] = React.useState(true);
  const [error, setError]     = React.useState("");
  const [envEditorOpen, setEnvEditorOpen] = React.useState(false);

  // Load collections list once on mount.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const list = await api.apiTester.listCollections();
        if (cancelled) return;
        setCollections(Array.isArray(list) ? list : []);
        if (list && list.length) setActiveCid(list[0].id);
      } catch (e) {
        if (!cancelled) setError((e && e.message) || "Couldn't load collections.");
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // Whenever the active collection changes, fetch its full payload.
  React.useEffect(() => {
    if (!activeCid) { setActiveCollection(null); setActiveRid(null); return; }
    let cancelled = false;
    (async () => {
      try {
        const c = await api.apiTester.getCollection(activeCid);
        if (cancelled) return;
        setActiveCollection(c);
        // If the previously-active request is in this collection, keep it.
        const stillThere = c.requests && c.requests.find(r => r.id === activeRid);
        if (stillThere) {
          setDraft(_requestToDraft(stillThere));
        } else if (c.requests && c.requests.length) {
          setActiveRid(c.requests[0].id);
          setDraft(_requestToDraft(c.requests[0]));
        } else {
          setActiveRid(null);
          setDraft(_emptyRequest());
        }
      } catch (e) {
        if (!cancelled) setError((e && e.message) || "Couldn't load collection.");
      }
    })();
    return () => { cancelled = true; };
  }, [activeCid]);

  // Hydrate the editor when the active request changes within the
  // already-loaded collection.
  React.useEffect(() => {
    if (!activeCollection || !activeRid) return;
    const r = activeCollection.requests.find(x => x.id === activeRid);
    if (r) setDraft(_requestToDraft(r));
  }, [activeRid]);

  function _requestToDraft(r) {
    return {
      id: r.id,
      name: r.name || "",
      method: r.method || "GET",
      url: r.url || "",
      headers: Array.isArray(r.headers) && r.headers.length ? r.headers : [_newHeaderRow()],
      body: r.body || "",
      body_type: r.body_type || "none",
      auth_type: r.auth_type || "none",
      auth_data: r.auth_data || {},
      capture: Array.isArray(r.capture) ? r.capture : [],
    };
  }

  async function newCollection() {
    const name = window.prompt("Name for the new collection:", "New collection");
    if (!name) return;
    try {
      const c = await api.apiTester.createCollection({ name });
      setCollections(cs => [...cs, c]);
      setActiveCid(c.id);
    } catch (e) {
      alert("Couldn't create: " + ((e && e.message) || "network error"));
    }
  }

  // Seed a starter collection so users can try the module without
  // typing out URLs from scratch. Targets httpbin.org because it's
  // a public, CORS-safe sandbox that echoes back exactly what you
  // send — perfect for verifying the proxy + auth + variable
  // substitution all line up. Five requests covering GET / POST +
  // JSON / headers echo / Bearer auth / API key auth, plus a
  // single "httpbin (production)" environment with the matching
  // baseUrl + token variables.
  const [seeding, setSeeding] = React.useState(false);
  async function loadSampleCollection() {
    if (seeding) return;
    setSeeding(true);
    try {
      const col = await api.apiTester.createCollection({
        name: "Sample · httpbin",
        description: "Starter collection that hits httpbin.org — a public echo service. Click any request and Send to verify the proxy + auth + environment variables all work end-to-end.",
      });
      // Environment with the variables the saved requests reference.
      const env = await api.apiTester.createEnvironment(col.id, {
        name: "httpbin (production)",
        variables: {
          baseUrl: "https://httpbin.org",
          // A plausible-looking token. httpbin's /bearer endpoint
          // accepts ANY non-empty bearer, so you'll get a 200 back
          // and see the token reflected in the response body.
          token: "demo-bearer-token-1234",
          apiKey: "demo-api-key-abcdef",
        },
      });
      // Wire that env as the collection's active env.
      await api.apiTester.patchCollection(col.id, { active_env_id: env.id });

      // Five demo requests — each one demonstrates one piece of the
      // module so the user can flip through and see what works.
      const seeds = [
        {
          name: "1. GET — basic request",
          method: "GET",
          url: "{{baseUrl}}/get?ping=hello",
          headers: [
            { key: "Accept", value: "application/json", enabled: true },
          ],
          body_type: "none",
          auth_type: "none",
          auth_data: {},
        },
        {
          name: "2. POST — JSON body",
          method: "POST",
          url: "{{baseUrl}}/post",
          headers: [{ key: "Accept", value: "application/json", enabled: true }],
          body_type: "json",
          body: JSON.stringify({
            message: "Hello from FlowBoard's API Tester",
            ts: new Date().toISOString(),
            nested: { works: true, count: 3 },
          }, null, 2),
          auth_type: "none",
          auth_data: {},
        },
        {
          name: "3. GET — see your headers",
          method: "GET",
          url: "{{baseUrl}}/headers",
          headers: [
            { key: "X-Custom-Header", value: "this-is-a-test", enabled: true },
            { key: "X-Disabled-Header", value: "you-shouldnt-see-this", enabled: false },
          ],
          body_type: "none",
          auth_type: "none",
          auth_data: {},
        },
        {
          name: "4. GET — Bearer auth",
          method: "GET",
          url: "{{baseUrl}}/bearer",
          headers: [],
          body_type: "none",
          auth_type: "bearer",
          // {{token}} resolves from the env. Try editing the env to
          // an empty string and re-running — you'll get 401.
          auth_data: { token: "{{token}}" },
        },
        {
          name: "5. GET — API key (custom header)",
          method: "GET",
          url: "{{baseUrl}}/headers",
          headers: [],
          body_type: "none",
          auth_type: "apikey",
          auth_data: { header: "X-Api-Key", value: "{{apiKey}}" },
        },
      ];
      for (const r of seeds) {
        await api.apiTester.createRequest(col.id, r);
      }

      // Refresh the list + open the new collection.
      const list = await api.apiTester.listCollections();
      setCollections(list || []);
      setActiveCid(col.id);
    } catch (e) {
      alert("Couldn't load the sample: " + ((e && e.message) || "network error"));
    } finally {
      setSeeding(false);
    }
  }

  async function renameCollection() {
    if (!activeCollection) return;
    const name = window.prompt("Rename collection:", activeCollection.name);
    if (!name || name === activeCollection.name) return;
    try {
      const c = await api.apiTester.patchCollection(activeCollection.id, { name });
      setCollections(cs => cs.map(x => x.id === c.id ? c : x));
      setActiveCollection(ac => ({ ...ac, name: c.name }));
    } catch (e) {
      alert("Couldn't rename: " + ((e && e.message) || "network error"));
    }
  }

  async function deleteCollection() {
    if (!activeCollection) return;
    if (!confirm(`Delete "${activeCollection.name}"? This removes every saved request and environment in this collection.`)) return;
    try {
      await api.apiTester.deleteCollection(activeCollection.id);
      setCollections(cs => cs.filter(x => x.id !== activeCollection.id));
      const next = collections.find(c => c.id !== activeCollection.id);
      setActiveCid(next ? next.id : null);
    } catch (e) {
      alert("Couldn't delete: " + ((e && e.message) || "network error"));
    }
  }

  async function newRequest() {
    if (!activeCollection) return;
    try {
      const r = await api.apiTester.createRequest(activeCollection.id, {
        name: "Untitled request", method: "GET", url: "",
      });
      setActiveCollection(ac => ({ ...ac, requests: [...(ac.requests || []), r] }));
      setActiveRid(r.id);
      setDraft(_requestToDraft(r));
    } catch (e) {
      alert("Couldn't create: " + ((e && e.message) || "network error"));
    }
  }

  async function saveRequest() {
    if (!activeCollection) return;
    if (!draft.id) {
      // Save as a new row in the active collection.
      try {
        const created = await api.apiTester.createRequest(activeCollection.id, {
          name: draft.name || "Untitled request",
          method: draft.method, url: draft.url,
          headers: draft.headers, body: draft.body, body_type: draft.body_type,
          auth_type: draft.auth_type, auth_data: draft.auth_data,
          capture: draft.capture || [],
        });
        setActiveCollection(ac => ({ ...ac, requests: [...(ac.requests || []), created] }));
        setActiveRid(created.id);
        setDraft(_requestToDraft(created));
      } catch (e) { alert("Couldn't save: " + ((e && e.message) || "network error")); }
      return;
    }
    try {
      const updated = await api.apiTester.patchRequest(draft.id, {
        name: draft.name, method: draft.method, url: draft.url,
        headers: draft.headers, body: draft.body, body_type: draft.body_type,
        auth_type: draft.auth_type, auth_data: draft.auth_data,
        capture: draft.capture || [],
      });
      setActiveCollection(ac => ({
        ...ac,
        requests: (ac.requests || []).map(r => r.id === updated.id ? updated : r),
      }));
    } catch (e) {
      alert("Couldn't save: " + ((e && e.message) || "network error"));
    }
  }

  async function deleteRequest(rid) {
    if (!confirm("Delete this saved request?")) return;
    try {
      await api.apiTester.deleteRequest(rid);
      setActiveCollection(ac => ({
        ...ac, requests: (ac.requests || []).filter(r => r.id !== rid),
      }));
      if (activeRid === rid) {
        setActiveRid(null);
        setDraft(_emptyRequest());
      }
    } catch (e) {
      alert("Couldn't delete: " + ((e && e.message) || "network error"));
    }
  }

  // Environment lookup — resolve {{name}} placeholders. Active env
  // is the one whose id matches collection.active_env_id.
  function activeVars() {
    if (!activeCollection || !activeCollection.active_env_id) return {};
    const e = (activeCollection.environments || []).find(x => x.id === activeCollection.active_env_id);
    return (e && e.variables) || {};
  }

  async function send() {
    if (running) return;
    setRunning(true);
    setResponse(null);
    try {
      const r = await api.apiTester.proxy({
        method: draft.method,
        url: draft.url,
        headers: draft.headers,
        body: draft.body,
        body_type: draft.body_type,
        auth_type: draft.auth_type,
        auth_data: draft.auth_data,
        variables: activeVars(),
      });
      // Evaluate post-response capture rules and stash the resolved
      // values into the active environment. This is what lets a
      // login request expose a {{token}} that subsequent requests
      // pick up automatically — the headline of the script feature.
      const captured = await applyCaptureRules(r);
      // Annotate the response so the panel below can show "Captured 2
      // variables: token, user_id" after the run.
      setResponse({ ...r, _captured: captured });
    } catch (e) {
      setResponse({
        error: true,
        message: (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Request failed",
        duration_ms: 0,
      });
    } finally {
      setRunning(false);
    }
  }

  // Run each enabled capture rule against the response. Returns an
  // array of { var, value, source, path, ok } so the UI can summarise
  // what actually got pulled out (and which rules failed). Side
  // effect: writes resolved variables to the active environment.
  async function applyCaptureRules(resp) {
    const rules = Array.isArray(draft.capture) ? draft.capture : [];
    if (!rules.length || !resp) return [];
    if (resp.error) return [];

    // Parse the body once. JSON path rules need it parsed; non-JSON
    // bodies fall back to the raw string for header/status rules.
    let parsedBody = null;
    if (typeof resp.body === "string") {
      try { parsedBody = JSON.parse(resp.body); }
      catch { parsedBody = null; }
    } else if (resp.body && typeof resp.body === "object") {
      parsedBody = resp.body;
    }

    // Headers come back as { name: value } from the proxy; lowercase
    // the keys for case-insensitive lookup.
    const lowerHeaders = {};
    if (resp.headers && typeof resp.headers === "object") {
      for (const k of Object.keys(resp.headers)) {
        lowerHeaders[k.toLowerCase()] = resp.headers[k];
      }
    }

    const results = [];
    const setMap = {};
    for (const rule of rules) {
      if (!rule || rule.enabled === false) continue;
      const varName = String(rule.var || "").trim();
      if (!varName) continue;
      const source = String(rule.source || "body").toLowerCase();
      const path = String(rule.path || "").trim();
      let value;
      try {
        if (source === "header") {
          value = lowerHeaders[path.toLowerCase()];
        } else if (source === "status") {
          value = resp.status;
        } else {
          // body — JSON path
          value = _jsonPath(parsedBody, path);
        }
      } catch { value = undefined; }
      const ok = value !== undefined && value !== null;
      results.push({ var: varName, value, source, path, ok });
      if (ok) setMap[varName] = value;
    }

    // If we got at least one value AND the collection has an active
    // environment, persist the new variables to it. No active env →
    // we still surface the captured values in the result panel; the
    // user gets a hint to pick / create an environment to save them.
    if (Object.keys(setMap).length && activeCollection && activeCollection.active_env_id) {
      const envId = activeCollection.active_env_id;
      const env = (activeCollection.environments || []).find(e => e.id === envId);
      if (env) {
        const merged = { ...(env.variables || {}), ...stringifyValues(setMap) };
        try {
          const updated = await api.apiTester.patchEnvironment(envId, { variables: merged });
          // Reflect the new vars in local state so later requests in
          // the same session see them without a reload.
          setActiveCollection(ac => ({
            ...ac,
            environments: (ac.environments || []).map(e => e.id === envId ? updated : e),
          }));
        } catch (e) {
          console.warn("[capture] couldn't save to environment:", e && e.message);
        }
      }
    }
    return results;
  }

  // Mark capture rules as "no env to save into" so the UI can prompt
  // the user without us guessing the wrong intent.
  const _hasActiveEnv = !!(activeCollection && activeCollection.active_env_id);

  // Auto-save the draft 800ms after the last edit, but only when
  // there's an active request id (don't accidentally create an
  // empty record from an idle pane).
  React.useEffect(() => {
    if (!draft.id) return;
    const t = setTimeout(() => { saveRequest(); }, 800);
    return () => clearTimeout(t);
  }, [draft.name, draft.method, draft.url, draft.body, draft.body_type, draft.auth_type, JSON.stringify(draft.headers), JSON.stringify(draft.auth_data), JSON.stringify(draft.capture)]);

  if (loading) {
    return (
      <div className="api-tester-root">
        <div className="api-tester-loading">Loading…</div>
      </div>
    );
  }

  return (
    <div className="api-tester-root">
      {/* ── Left rail: collections + requests ─────────────────── */}
      <aside className="apit-rail">
        <div className="apit-rail-head">
          <div style={{ flex: 1, fontWeight: 700, fontSize: 13, color: "var(--ink-strong)" }}>
            Collections
          </div>
          <button className="apit-rail-add" title="New collection" onClick={newCollection}>+</button>
        </div>
        {collections.length === 0 && (
          <div className="apit-empty">
            No collections yet. Click <b>+</b> to create one.
          </div>
        )}
        <div className="apit-collection-list">
          {collections.map(c => (
            <button key={c.id}
                    className={"apit-collection-row" + (c.id === activeCid ? " is-active" : "")}
                    onClick={() => setActiveCid(c.id)}
                    title={c.is_mine === false && c.owner_name
                      ? `Shared by ${c.owner_name}`
                      : c.name}>
              <span className="apit-collection-icon" aria-hidden="true">
                {c.is_mine === false ? "🔗" : "📁"}
              </span>
              <span className="apit-collection-name">{c.name}</span>
              {c.is_mine === false && c.owner_name && (
                <span className="apit-collection-shared-by" title={`Created by ${c.owner_name}`}>
                  {c.owner_name.split(/\s+/)[0]}
                </span>
              )}
            </button>
          ))}
        </div>
        {activeCollection && (
          <>
            <div className="apit-rail-section">
              <div className="apit-rail-section-head">
                <span>Requests</span>
                <button className="apit-rail-add" title="New request" onClick={newRequest}>+</button>
              </div>
              {(activeCollection.requests || []).length === 0 && (
                <div className="apit-empty">No requests yet.</div>
              )}
              {(activeCollection.requests || []).map(r => (
                <div key={r.id}
                     className={"apit-request-row" + (r.id === activeRid ? " is-active" : "")}>
                  <button className="apit-request-pick" onClick={() => setActiveRid(r.id)}>
                    <span className={"apit-method-pill apit-method-" + (r.method || "GET").toLowerCase()}>
                      {r.method || "GET"}
                    </span>
                    <span className="apit-request-name">{r.name || "Untitled"}</span>
                  </button>
                  <button className="apit-row-delete"
                          title="Delete saved request"
                          onClick={() => deleteRequest(r.id)}>×</button>
                </div>
              ))}
            </div>
          </>
        )}
      </aside>

      {/* ── Main pane: editor + response ──────────────────────── */}
      <main className="apit-main">
        {!activeCollection ? (
          <div className="apit-blank">
            <div className="apit-blank-icon">🛰️</div>
            <h2>API Tester</h2>
            <p>
              Create a collection to start saving requests. Each user's collections are private — your tokens never leave your account.
            </p>
            <div style={{ display: "inline-flex", gap: 10, flexWrap: "wrap", justifyContent: "center" }}>
              <button className="btn btn-primary" onClick={newCollection}>+ New collection</button>
              <button className="btn"
                      onClick={loadSampleCollection}
                      disabled={seeding}
                      title="Creates a sample collection against httpbin.org so you can verify everything works">
                {seeding ? "Setting up…" : "✨ Try a sample collection"}
              </button>
            </div>
            <p style={{ fontSize: 12, color: "var(--ink-muted)", marginTop: 18, lineHeight: 1.6 }}>
              The sample hits <code style={{ background: "rgba(0,0,0,.05)", padding: "1px 6px", borderRadius: 3 }}>httpbin.org</code> — a public echo service.<br/>
              Five requests cover GET, POST + JSON, header echo, Bearer auth, and API-key auth, with a matching environment for <code>{`{{baseUrl}}`}</code> and <code>{`{{token}}`}</code>.
            </p>
          </div>
        ) : (
          <>
            {/* ── Top bar — collection breadcrumb + environment picker
                 + collection-level actions. Environment lives here
                 (Postman-style) so it's always one click away from
                 every request without leaving the rail visible. */}
            <div className="apit-topbar">
              <div className="apit-breadcrumb">
                <span className="apit-breadcrumb-collection" title={activeCollection.name}>
                  <span className="apit-breadcrumb-icon" aria-hidden="true">📁</span>
                  {activeCollection.name}
                </span>
                {/* Workspace-shared indicator. Shown when the user
                    isn't the creator — makes it obvious why Rename /
                    Delete are disabled and who to ask if they want
                    a change. */}
                {activeCollection.is_mine === false && (
                  <span
                    className="apit-breadcrumb-shared"
                    title={activeCollection.owner_name
                      ? `Created by ${activeCollection.owner_name} · shared with the workspace`
                      : "Shared with the workspace"}>
                    <span aria-hidden="true">🔗</span>
                    Shared{activeCollection.owner_name ? ` · ${activeCollection.owner_name}` : ""}
                  </span>
                )}
                <button className="apit-breadcrumb-action"
                        onClick={renameCollection}
                        disabled={activeCollection.is_mine === false}
                        title={activeCollection.is_mine === false
                          ? `Only ${activeCollection.owner_name || "the creator"} or a workspace owner can rename this`
                          : "Rename collection"}>Rename</button>
                <button className="apit-breadcrumb-action is-danger"
                        onClick={deleteCollection}
                        disabled={activeCollection.is_mine === false}
                        title={activeCollection.is_mine === false
                          ? `Only ${activeCollection.owner_name || "the creator"} or a workspace owner can delete this`
                          : "Delete this collection and everything in it"}>Delete</button>
              </div>
              <div className="apit-env-bar">
                <span className="apit-env-label" title="Variables in the active environment substitute into URL / headers / body via {{name}}">
                  <span aria-hidden="true">🌐</span> Environment
                </span>
                <select
                  className="apit-env-select"
                  value={activeCollection.active_env_id || ""}
                  onChange={async (e) => {
                    const id = e.target.value || null;
                    try {
                      const c = await api.apiTester.patchCollection(activeCollection.id, { active_env_id: id });
                      setActiveCollection(ac => ({ ...ac, active_env_id: c.active_env_id }));
                    } catch {}
                  }}>
                  <option value="">— No environment —</option>
                  {(activeCollection.environments || []).map(env => (
                    <option key={env.id} value={env.id}>{env.name}</option>
                  ))}
                </select>
                <button className="apit-env-edit" onClick={() => setEnvEditorOpen(true)}
                        title="Manage environments + variables">
                  <span aria-hidden="true">⚙</span> Manage
                </button>
              </div>
            </div>

            {/* ── Request title — single-line, click-to-edit */}
            <div className="apit-titlebar">
              <input
                className="apit-title-input"
                value={draft.name}
                placeholder="Untitled request"
                onChange={(e) => setDraft(d => ({ ...d, name: e.target.value }))}/>
              <button className="btn" onClick={saveRequest} title="Save request (auto-saves as you edit too)">Save</button>
            </div>

            {/* ── URL bar — method + URL + send */}
            <div className="apit-urlbar">
              <select
                className={"apit-method-select apit-method-" + draft.method.toLowerCase()}
                value={draft.method}
                onChange={(e) => setDraft(d => ({ ...d, method: e.target.value }))}>
                {API_METHODS.map(m => <option key={m} value={m}>{m}</option>)}
              </select>
              <input
                className="apit-url-input"
                value={draft.url}
                placeholder="https://api.example.com/endpoint  —  {{baseUrl}}/path also works"
                onChange={(e) => setDraft(d => ({ ...d, url: e.target.value }))}
                onKeyDown={(e) => { if (e.key === "Enter") send(); }}/>
              <button className="btn btn-primary apit-send"
                      onClick={send}
                      disabled={running || !draft.url.trim()}>
                {running ? "Sending…" : "Send"}
              </button>
            </div>

            {/* Tabs — headers / body / auth */}
            <RequestTabs draft={draft} setDraft={setDraft}/>

            {/* Response panel */}
            <ResponsePanel response={response} running={running}/>
          </>
        )}
      </main>

      {envEditorOpen && activeCollection && (
        <EnvironmentEditor
          collection={activeCollection}
          onClose={() => setEnvEditorOpen(false)}
          onChange={(updated) => setActiveCollection(updated)}/>
      )}
    </div>
  );
}

// ── Request tabs (Headers / Body / Auth / Capture) ─────────────────
function RequestTabs({ draft, setDraft }) {
  const [tab, setTab] = React.useState("headers");
  const captureCount = (draft.capture || []).filter(r => r && r.enabled !== false && (r.var || "").trim()).length;
  return (
    <div className="apit-tabs">
      <div className="apit-tab-strip">
        <button className={"apit-tab" + (tab === "headers" ? " is-on" : "")} onClick={() => setTab("headers")}>
          Headers <span className="apit-tab-count">{(draft.headers || []).filter(h => h.enabled !== false && (h.key || "").trim()).length}</span>
        </button>
        <button className={"apit-tab" + (tab === "body" ? " is-on" : "")} onClick={() => setTab("body")}>
          Body{draft.body_type !== "none" && <span className="apit-tab-count">{draft.body_type}</span>}
        </button>
        <button className={"apit-tab" + (tab === "auth" ? " is-on" : "")} onClick={() => setTab("auth")}>
          Auth{draft.auth_type !== "none" && <span className="apit-tab-count">{draft.auth_type}</span>}
        </button>
        <button className={"apit-tab" + (tab === "capture" ? " is-on" : "")}
                onClick={() => setTab("capture")}
                title="Pull values out of the response and save them to the active environment">
          Capture{captureCount > 0 && <span className="apit-tab-count">{captureCount}</span>}
        </button>
      </div>
      <div className="apit-tab-body">
        {tab === "headers" && <HeadersEditor headers={draft.headers} onChange={(h) => setDraft(d => ({ ...d, headers: h }))}/>}
        {tab === "body"    && <BodyEditor   draft={draft} setDraft={setDraft}/>}
        {tab === "auth"    && <AuthEditor   draft={draft} setDraft={setDraft}/>}
        {tab === "capture" && <CaptureEditor rules={draft.capture} onChange={(c) => setDraft(d => ({ ...d, capture: c }))}/>}
      </div>
    </div>
  );
}

// ── Capture rules editor ───────────────────────────────────────────
// Rows of "var name | source (body / header / status) | json path |
// enabled". After Send, each enabled rule is run against the
// response and the resolved value is written to the active env.
//
// Examples:
//   var: token      source: body    path: $.access_token
//   var: user_id    source: body    path: $.user.id
//   var: csrf       source: header  path: x-csrf-token
//   var: code       source: status  path: (ignored)
function CaptureEditor({ rules, onChange }) {
  const list = Array.isArray(rules) ? rules : [];
  function patch(i, p) { onChange(list.map((r, idx) => idx === i ? { ...r, ...p } : r)); }
  function remove(i)   { onChange(list.filter((_, idx) => idx !== i)); }
  function add()       { onChange([...list, _newCaptureRow()]); }

  return (
    <div className="apit-capture">
      <div className="apit-capture-blurb">
        After the request runs, pull values out of the response and
        save them to the <b>active environment</b>. Reference them in
        any later request via <code>{"{{var_name}}"}</code>.
        <span className="apit-capture-hint">
          Tip: a Login request can capture <code>$.access_token</code> as
          <code> token</code>, then a follow-up request uses
          <code> Authorization: Bearer {"{{token}}"}</code>.
        </span>
      </div>
      <div className="apit-capture-grid">
        <div className="apit-capture-head">
          <span></span>
          <span>Variable name</span>
          <span>Source</span>
          <span>Path</span>
          <span></span>
        </div>
        {list.length === 0 && (
          <div className="apit-capture-empty">No capture rules yet — add one below.</div>
        )}
        {list.map((r, i) => (
          <div key={i} className="apit-capture-row">
            <input type="checkbox"
                   checked={r.enabled !== false}
                   onChange={(e) => patch(i, { enabled: e.target.checked })}
                   title="Enable / disable this capture rule"/>
            <input type="text"
                   value={r.var || ""}
                   onChange={(e) => patch(i, { var: e.target.value })}
                   placeholder="token"
                   className="apit-input"/>
            <select className="apit-input"
                    value={r.source || "body"}
                    onChange={(e) => patch(i, { source: e.target.value })}>
              <option value="body">Body (JSON path)</option>
              <option value="header">Response header</option>
              <option value="status">Status code</option>
            </select>
            <input type="text"
                   value={r.path || ""}
                   onChange={(e) => patch(i, { path: e.target.value })}
                   placeholder={
                     (r.source === "header") ? "x-csrf-token" :
                     (r.source === "status") ? "(no path needed)" :
                     "$.access_token"
                   }
                   disabled={r.source === "status"}
                   className="apit-input apit-input-mono"/>
            <button type="button" className="apit-row-x" onClick={() => remove(i)} title="Remove rule">×</button>
          </div>
        ))}
      </div>
      <button type="button" className="apit-add-row" onClick={add}>+ Add capture rule</button>
    </div>
  );
}

function HeadersEditor({ headers, onChange }) {
  const list = Array.isArray(headers) ? headers : [];
  function patch(i, p) { onChange(list.map((h, idx) => idx === i ? { ...h, ...p } : h)); }
  function remove(i)   { onChange(list.filter((_, idx) => idx !== i)); }
  function add()       { onChange([...list, { key: "", value: "", enabled: true }]); }
  return (
    <div className="apit-headers">
      {list.map((h, i) => (
        <div key={i} className={"apit-header-row" + (h.enabled === false ? " is-off" : "")}>
          <input type="checkbox" checked={h.enabled !== false} onChange={(e) => patch(i, { enabled: e.target.checked })}/>
          <input className="apit-header-key"   placeholder="Header"
                 value={h.key   || ""}
                 onChange={(e) => patch(i, { key: e.target.value })}/>
          <input className="apit-header-value" placeholder="Value"
                 value={h.value || ""}
                 onChange={(e) => patch(i, { value: e.target.value })}/>
          <button className="apit-row-delete" onClick={() => remove(i)} title="Remove header">×</button>
        </div>
      ))}
      <button className="apit-add-row" onClick={add}>+ Add header</button>
    </div>
  );
}

function BodyEditor({ draft, setDraft }) {
  return (
    <div className="apit-body-pane">
      <div className="apit-body-types">
        {API_BODY_TYPES.map(bt => (
          <label key={bt.id} className={"apit-body-type" + (draft.body_type === bt.id ? " is-on" : "")}>
            <input type="radio" name="bt"
                   checked={draft.body_type === bt.id}
                   onChange={() => setDraft(d => ({ ...d, body_type: bt.id }))}/>
            {bt.label}
          </label>
        ))}
      </div>
      {draft.body_type !== "none" && (
        <textarea
          className="apit-body-text"
          value={draft.body || ""}
          onChange={(e) => setDraft(d => ({ ...d, body: e.target.value }))}
          placeholder={
            draft.body_type === "json" ? '{\n  "key": "value"\n}'
            : draft.body_type === "urlencoded" ? "key1=value1&key2=value2"
            : "Raw body"
          }
          spellCheck={false}/>
      )}
    </div>
  );
}

function AuthEditor({ draft, setDraft }) {
  const a = draft.auth_data || {};
  function setA(p) { setDraft(d => ({ ...d, auth_data: { ...(d.auth_data || {}), ...p } })); }
  return (
    <div className="apit-auth">
      <div className="apit-auth-types">
        {API_AUTH_TYPES.map(at => (
          <label key={at.id} className={"apit-auth-type" + (draft.auth_type === at.id ? " is-on" : "")}>
            <input type="radio" name="at"
                   checked={draft.auth_type === at.id}
                   onChange={() => setDraft(d => ({ ...d, auth_type: at.id }))}/>
            {at.label}
          </label>
        ))}
      </div>
      {draft.auth_type === "bearer" && (
        <div className="apit-auth-fields">
          <Field label="Token">
            <input value={a.token || ""} onChange={(e) => setA({ token: e.target.value })}
                   placeholder="paste your bearer token (or {{token}})"/>
          </Field>
        </div>
      )}
      {draft.auth_type === "basic" && (
        <div className="apit-auth-fields">
          <Field label="Username">
            <input value={a.username || ""} onChange={(e) => setA({ username: e.target.value })}/>
          </Field>
          <Field label="Password">
            <input type="password" value={a.password || ""} onChange={(e) => setA({ password: e.target.value })}/>
          </Field>
        </div>
      )}
      {draft.auth_type === "apikey" && (
        <div className="apit-auth-fields">
          <Field label="Header name">
            <input value={a.header || ""} onChange={(e) => setA({ header: e.target.value })}
                   placeholder="X-Api-Key"/>
          </Field>
          <Field label="Header value">
            <input value={a.value || ""} onChange={(e) => setA({ value: e.target.value })}
                   placeholder="your secret"/>
          </Field>
        </div>
      )}
      {draft.auth_type === "oauth2" && (
        <div className="apit-auth-fields">
          <div className="apit-auth-note">
            v1 supports the <b>client_credentials</b> grant. Fill the fields below, click <b>Get token</b>,
            and the access token gets stored on this request and added as <code>Authorization: Bearer …</code> on every Send.
          </div>
          <Field label="Token URL">
            <input value={a.token_url || ""} onChange={(e) => setA({ token_url: e.target.value })}
                   placeholder="https://issuer.example.com/oauth/token"/>
          </Field>
          <Field label="Client ID">
            <input value={a.client_id || ""} onChange={(e) => setA({ client_id: e.target.value })}/>
          </Field>
          <Field label="Client secret">
            <input type="password" value={a.client_secret || ""} onChange={(e) => setA({ client_secret: e.target.value })}/>
          </Field>
          <Field label="Scope (optional)">
            <input value={a.scope || ""} onChange={(e) => setA({ scope: e.target.value })}
                   placeholder="read write"/>
          </Field>
          <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 8 }}>
            <button className="btn" onClick={() => oauthFetchToken(a, setA)}>Get new access token</button>
            {a.access_token && (
              <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>
                Have a token · {String(a.access_token).slice(0, 14)}…
              </span>
            )}
          </div>
          <Field label="Access token (optional — paste manually)">
            <input value={a.access_token || ""} onChange={(e) => setA({ access_token: e.target.value })}
                   placeholder="filled by Get token, or paste your own"/>
          </Field>
        </div>
      )}
    </div>
  );
}

function Field({ label, children }) {
  return (
    <label className="apit-field">
      <span className="apit-field-label">{label}</span>
      {children}
    </label>
  );
}

// Best-effort OAuth2 client_credentials helper. Routes through the
// proxy so we get the same SSRF guard + CORS bypass as the main
// request path. Result is written back into the auth_data.
async function oauthFetchToken(a, setA) {
  if (!a || !a.token_url || !a.client_id) {
    alert("Token URL and Client ID are required.");
    return;
  }
  const body = "grant_type=client_credentials"
    + "&client_id=" + encodeURIComponent(a.client_id)
    + "&client_secret=" + encodeURIComponent(a.client_secret || "")
    + (a.scope ? "&scope=" + encodeURIComponent(a.scope) : "");
  try {
    const r = await api.apiTester.proxy({
      method: "POST",
      url: a.token_url,
      headers: [{ key: "Content-Type", value: "application/x-www-form-urlencoded", enabled: true }],
      body, body_type: "urlencoded",
      auth_type: "none", auth_data: {},
      variables: {},
    });
    if (!r || !r.ok || !r.body) {
      alert("Token request failed: " + ((r && r.message) || "no response"));
      return;
    }
    let parsed;
    try { parsed = JSON.parse(r.body); }
    catch {
      alert("Token endpoint returned a non-JSON response:\n" + String(r.body).slice(0, 200));
      return;
    }
    if (!parsed.access_token) {
      alert("No access_token in the response. Got:\n" + JSON.stringify(parsed, null, 2).slice(0, 400));
      return;
    }
    setA({
      access_token:  parsed.access_token,
      refresh_token: parsed.refresh_token || null,
      expires_at:    parsed.expires_in ? new Date(Date.now() + Number(parsed.expires_in) * 1000).toISOString() : null,
    });
  } catch (e) {
    alert("Token request failed: " + ((e && e.message) || "network"));
  }
}

// ── Response panel ────────────────────────────────────────────────
function ResponsePanel({ response, running }) {
  const [tab, setTab] = React.useState("body");
  if (running) {
    return <div className="apit-response apit-response--loading">Sending…</div>;
  }
  if (!response) {
    return <div className="apit-response apit-response--idle">Click <b>Send</b> to fire the request.</div>;
  }
  if (response.error) {
    return (
      <div className="apit-response apit-response--error">
        <div className="apit-response-head">
          <span className="apit-status-bad">Failed</span>
          <span className="apit-response-meta">{response.duration_ms}ms</span>
        </div>
        <pre className="apit-body-text apit-body-text--ro">{response.message}</pre>
      </div>
    );
  }
  const status = Number(response.status || 0);
  const tone = status >= 500 ? "is-bad" : status >= 400 ? "is-warn" : status >= 200 ? "is-good" : "is-mid";
  const headerCount = response.headers ? Object.keys(response.headers).length : 0;
  return (
    <div className="apit-response">
      <div className="apit-response-head">
        <span className={"apit-status " + tone}>
          {status} {response.status_text || ""}
        </span>
        <span className="apit-response-meta">{response.duration_ms}ms</span>
        <span className="apit-response-meta">{_fmtBytes(response.bytes)}{response.truncated ? " · truncated" : ""}</span>
        <div className="apit-response-tabs">
          <button className={"apit-tab" + (tab === "body" ? " is-on" : "")} onClick={() => setTab("body")}>
            Body
          </button>
          <button className={"apit-tab" + (tab === "headers" ? " is-on" : "")} onClick={() => setTab("headers")}>
            Headers <span className="apit-tab-count">{headerCount}</span>
          </button>
        </div>
      </div>
      {tab === "body" && (
        response.body_base64
          ? <div className="apit-body-text apit-body-text--ro">[binary — {_fmtBytes(response.bytes)}]</div>
          : <pre className="apit-body-text apit-body-text--ro">{_prettyMaybeJSON(response.body)}</pre>
      )}
      {tab === "headers" && (
        <div className="apit-response-headers">
          {response.headers && Object.entries(response.headers).map(([k, v]) => (
            <div key={k} className="apit-header-line">
              <span className="apit-header-line-key">{k}</span>
              <span className="apit-header-line-val">{Array.isArray(v) ? v.join(", ") : v}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
function _prettyMaybeJSON(s) {
  if (typeof s !== "string") return s == null ? "" : String(s);
  const t = s.trim();
  if (!t) return "";
  if (t[0] === "{" || t[0] === "[") {
    try { return JSON.stringify(JSON.parse(t), null, 2); } catch {}
  }
  return s;
}
function _fmtBytes(n) {
  n = Number(n) || 0;
  if (n < 1024) return n + " B";
  if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
  return (n / (1024 * 1024)).toFixed(2) + " MB";
}

// ── Environment editor modal ──────────────────────────────────────
function EnvironmentEditor({ collection, onClose, onChange }) {
  const [envs, setEnvs] = React.useState(collection.environments || []);
  const [activeId, setActiveId] = React.useState(envs[0] ? envs[0].id : null);
  const active = envs.find(e => e.id === activeId);

  async function newEnv() {
    const name = window.prompt("Environment name:", "Production");
    if (!name) return;
    try {
      const e = await api.apiTester.createEnvironment(collection.id, { name, variables: {} });
      const next = [...envs, e];
      setEnvs(next);
      setActiveId(e.id);
      onChange({ ...collection, environments: next });
    } catch (err) { alert("Couldn't create: " + ((err && err.message) || "network error")); }
  }
  async function rename() {
    if (!active) return;
    const name = window.prompt("Rename:", active.name);
    if (!name || name === active.name) return;
    try {
      const e = await api.apiTester.patchEnvironment(active.id, { name });
      const next = envs.map(x => x.id === e.id ? e : x);
      setEnvs(next);
      onChange({ ...collection, environments: next });
    } catch (err) { alert("Couldn't rename: " + ((err && err.message) || "network error")); }
  }
  async function remove() {
    if (!active) return;
    if (!confirm("Delete this environment?")) return;
    try {
      await api.apiTester.deleteEnvironment(active.id);
      const next = envs.filter(x => x.id !== active.id);
      setEnvs(next);
      setActiveId(next[0] ? next[0].id : null);
      onChange({
        ...collection,
        environments: next,
        active_env_id: collection.active_env_id === active.id ? null : collection.active_env_id,
      });
    } catch (err) { alert("Couldn't delete: " + ((err && err.message) || "network error")); }
  }
  async function patchVars(nextVars) {
    if (!active) return;
    setEnvs(es => es.map(x => x.id === active.id ? { ...x, variables: nextVars } : x));
    try {
      const e = await api.apiTester.patchEnvironment(active.id, { variables: nextVars });
      const next = envs.map(x => x.id === e.id ? e : x);
      setEnvs(next);
      onChange({ ...collection, environments: next });
    } catch {}
  }
  function addVar() {
    if (!active) return;
    const k = window.prompt("Variable name:", "baseUrl");
    if (!k) return;
    patchVars({ ...(active.variables || {}), [k]: "" });
  }
  function setVar(k, v) {
    if (!active) return;
    patchVars({ ...(active.variables || {}), [k]: v });
  }
  function delVar(k) {
    if (!active) return;
    const next = { ...(active.variables || {}) };
    delete next[k];
    patchVars(next);
  }

  return ReactDOM.createPortal(
    <div className="modal-backdrop apit-modal-backdrop"
         onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="apit-env-modal" onMouseDown={(e) => e.stopPropagation()}>
        <header>
          <h3>Environments — {collection.name}</h3>
          <button className="apit-row-delete" onClick={onClose} title="Close">×</button>
        </header>
        <div className="apit-env-body">
          <aside className="apit-env-list">
            {envs.map(e => (
              <button key={e.id}
                      className={"apit-env-row" + (e.id === activeId ? " is-active" : "")}
                      onClick={() => setActiveId(e.id)}>
                {e.name}
              </button>
            ))}
            <button className="apit-add-row" onClick={newEnv}>+ New environment</button>
          </aside>
          <section className="apit-env-vars">
            {active ? (
              <>
                <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
                  <button className="btn" onClick={rename}>Rename</button>
                  <button className="btn" onClick={remove} style={{ color: "#c0223a" }}>Delete env</button>
                </div>
                {Object.entries(active.variables || {}).map(([k, v]) => (
                  <div key={k} className="apit-var-row">
                    <span className="apit-var-key">{k}</span>
                    <input className="apit-var-val" value={String(v == null ? "" : v)}
                           onChange={(e) => setVar(k, e.target.value)}
                           placeholder="value"/>
                    <button className="apit-row-delete" onClick={() => delVar(k)} title="Remove">×</button>
                  </div>
                ))}
                <button className="apit-add-row" onClick={addVar}>+ Add variable</button>
              </>
            ) : (
              <div className="apit-empty">No environment selected. Create one on the left.</div>
            )}
          </section>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ── CSS injection (one shot) ─────────────────────────────────────
if (typeof document !== "undefined" && !document.getElementById("apit-css")) {
  const s = document.createElement("style");
  s.id = "apit-css";
  s.textContent = `
    .api-tester-root {
      display: grid;
      grid-template-columns: 260px 1fr;
      gap: 0;
      background: var(--bg-app);
      flex: 1; min-height: 0;
      overflow: hidden;
    }
    @media (max-width: 900px) {
      .api-tester-root { grid-template-columns: 1fr; }
    }
    .apit-loading { padding: 40px; color: var(--ink-muted); text-align: center; }

    /* ── Left rail ─────────────────────────────────────────── */
    .apit-rail {
      background: white;
      border-right: 1px solid var(--border);
      display: flex; flex-direction: column;
      overflow-y: auto;
      padding: 8px 0 12px;
    }
    .apit-rail-head, .apit-rail-section-head {
      display: flex; align-items: center;
      padding: 8px 14px;
      gap: 6px;
    }
    .apit-rail-section { border-top: 1px solid var(--border); margin-top: 6px; padding-top: 2px; }
    .apit-rail-section-head { font-size: 10.5px; font-weight: 700; color: var(--ink-muted); letter-spacing: .08em; text-transform: uppercase; }
    .apit-rail-section-head span { flex: 1; }
    .apit-rail-add {
      width: 22px; height: 22px;
      border: 1px solid var(--border);
      background: white; border-radius: 5px;
      cursor: pointer; font-weight: 700;
      color: var(--ink-strong);
      transition: background .12s, border-color .12s, color .12s;
      display: inline-flex; align-items: center; justify-content: center;
      line-height: 1;
    }
    .apit-rail-add:hover { background: var(--brand-soft); border-color: var(--brand); color: var(--brand); }

    .apit-collection-list { display: flex; flex-direction: column; padding: 2px 8px; gap: 1px; }
    .apit-collection-row {
      display: flex; align-items: center; gap: 8px;
      padding: 8px 10px;
      border: 0; background: transparent; text-align: left;
      cursor: pointer; font-size: 13px; color: var(--ink-strong);
      border-radius: 6px;
      transition: background .1s;
    }
    .apit-collection-row:hover { background: #f4f5f8; }
    .apit-collection-row.is-active {
      background: var(--brand-soft);
      color: var(--brand);
      font-weight: 600;
    }
    .apit-collection-icon { font-size: 13px; flex: none; }
    .apit-collection-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

    .apit-request-row {
      display: flex; align-items: center;
      margin: 0 8px;
      padding: 0 6px 0 10px;
      border-radius: 6px;
      transition: background .1s;
    }
    .apit-request-row:hover { background: #f4f5f8; }
    .apit-request-row.is-active {
      background: rgba(162,93,220,.08);
      box-shadow: inset 3px 0 0 var(--brand);
    }
    .apit-request-row:hover .apit-row-delete { opacity: 1; }
    .apit-request-pick {
      flex: 1; min-width: 0;
      display: flex; align-items: center; gap: 8px;
      padding: 7px 0;
      background: transparent; border: 0;
      cursor: pointer; text-align: left;
      font-size: 12.5px; color: var(--ink-strong);
    }
    .apit-request-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

    /* Method pill colors mirror Postman's (good faith homage). */
    .apit-method-pill {
      flex: none;
      padding: 2px 6px;
      font-size: 10px; font-weight: 800;
      letter-spacing: .05em;
      border-radius: 3px;
      font-family: ui-monospace, "SF Mono", Menlo, monospace;
      text-transform: uppercase;
      min-width: 42px; text-align: center;
    }
    .apit-method-get     { color: #15803d; background: rgba(34,197,94,.12); }
    .apit-method-post    { color: #b45309; background: rgba(245,158,11,.14); }
    .apit-method-put     { color: #1e40af; background: rgba(0,115,234,.12); }
    .apit-method-patch   { color: #6d28d9; background: rgba(162,93,220,.14); }
    .apit-method-delete  { color: #b91c1c; background: rgba(226,68,92,.12); }
    .apit-method-head    { color: var(--ink-muted); background: rgba(0,0,0,.06); }
    .apit-method-options { color: var(--ink-muted); background: rgba(0,0,0,.06); }

    .apit-row-delete {
      width: 22px; height: 22px;
      border: 0; background: transparent;
      color: var(--ink-faint, #a3a8b6);
      font-size: 16px; line-height: 1;
      border-radius: 4px;
      cursor: pointer;
      /* Always visible. Earlier we hover-faded these in to keep rows
         calm, but env variable rows + header rows never got their
         own hover-reveal rule, leaving the delete button perpetually
         invisible. Showing it muted-by-default is friendlier and
         mobile-tappable; the red hover state still indicates intent. */
      opacity: .7;
      transition: opacity .12s, background .12s, color .12s;
      display: inline-flex; align-items: center; justify-content: center;
      flex-shrink: 0;
    }
    .apit-row-delete:hover {
      background: rgba(226,68,92,.10);
      color: #c0223a;
      opacity: 1;
    }
    /* Keep the original hover-reveal on the saved-request rail rows
       — those are bigger lists where the always-on × would clutter. */
    .apit-request-row .apit-row-delete { opacity: 0; }
    .apit-request-row:hover .apit-row-delete { opacity: 1; }
    .apit-empty {
      padding: 14px 16px; font-size: 12px;
      color: var(--ink-muted); font-style: italic;
    }

    /* ── Main pane ─────────────────────────────────────────── */
    .apit-main {
      display: flex; flex-direction: column;
      min-height: 0; overflow-y: auto;
      padding: 0 24px 24px;
      background: var(--bg-app);
    }

    /* Sticky topbar — collection breadcrumb + env selector. Stays
       visible as the user scrolls through long requests / responses
       so changing env is always one click away. */
    .apit-topbar {
      position: sticky; top: 0;
      display: flex; align-items: center; justify-content: space-between;
      gap: 14px; flex-wrap: wrap;
      padding: 12px 0 12px;
      margin-bottom: 4px;
      background: var(--bg-app);
      border-bottom: 1px solid var(--border-row);
      z-index: 5;
    }
    .apit-breadcrumb {
      display: inline-flex; align-items: center; gap: 8px;
      font-size: 13px;
      color: var(--ink-strong);
      min-width: 0; flex: 1;
    }
    .apit-breadcrumb-collection {
      display: inline-flex; align-items: center; gap: 6px;
      font-weight: 700;
      max-width: 320px;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .apit-breadcrumb-icon { font-size: 14px; flex: none; }
    .apit-breadcrumb-action {
      border: 0; background: transparent;
      color: var(--brand); cursor: pointer;
      font-size: 11.5px; font-weight: 600;
      padding: 4px 8px; border-radius: 4px;
    }
    .apit-breadcrumb-action:hover { background: var(--brand-soft); }
    .apit-breadcrumb-action.is-danger { color: #c0223a; }
    .apit-breadcrumb-action.is-danger:hover { background: rgba(226,68,92,.10); }
    .apit-breadcrumb-action[disabled] {
      color: var(--ink-faint, #a3a8b6); cursor: not-allowed;
    }
    .apit-breadcrumb-action[disabled]:hover { background: transparent; }

    /* "Shared · Created by …" chip in the breadcrumb when the user
       isn't the collection's creator. Sits between the title and
       the rename/delete buttons; subtle so it doesn't compete with
       the breadcrumb itself. */
    .apit-breadcrumb-shared {
      display: inline-flex; align-items: center; gap: 4px;
      font-size: 11px; font-weight: 600;
      color: var(--ink-muted);
      background: rgba(0,115,234,.08);
      border: 1px solid rgba(0,115,234,.18);
      padding: 2px 8px;
      border-radius: 999px;
      margin-left: 4px;
    }

    /* Shared-by chip on the rail collection rows — tiny first-name
       caption so the user knows which collections are someone else's. */
    .apit-collection-shared-by {
      margin-left: auto;
      font-size: 10.5px; font-weight: 600;
      color: var(--ink-muted, #676879);
      background: rgba(0,0,0,.05);
      padding: 1px 7px; border-radius: 999px;
      flex-shrink: 0;
      max-width: 80px;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .apit-collection-row.is-active .apit-collection-shared-by {
      background: rgba(255,255,255,.18);
      color: rgba(255,255,255,.85);
    }

    .apit-env-bar {
      display: inline-flex; align-items: center; gap: 8px;
      background: white;
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 4px 4px 4px 12px;
      box-shadow: var(--shadow-sm);
    }
    .apit-env-label {
      font-size: 11.5px; font-weight: 700;
      color: var(--ink-muted);
      letter-spacing: .04em;
      text-transform: uppercase;
      display: inline-flex; align-items: center; gap: 5px;
    }
    .apit-env-select {
      border: 0;
      background: transparent;
      padding: 6px 8px;
      font-size: 13px; font-weight: 600;
      color: var(--ink-strong);
      cursor: pointer;
      outline: none;
      min-width: 140px;
      max-width: 220px;
    }
    .apit-env-select:focus { color: var(--brand); }
    .apit-env-edit {
      border: 0;
      background: var(--brand-soft, rgba(162,93,220,.08));
      color: var(--brand);
      padding: 6px 12px;
      font-size: 12px; font-weight: 600;
      border-radius: 6px;
      cursor: pointer;
      display: inline-flex; align-items: center; gap: 4px;
    }
    .apit-env-edit:hover { background: rgba(162,93,220,.16); }
    .apit-blank {
      max-width: 520px; margin: 80px auto 60px;
      text-align: center;
      padding: 32px 24px;
      background: white;
      border: 1px solid var(--border);
      border-radius: 12px;
      box-shadow: var(--shadow-sm);
    }
    .apit-blank-icon { font-size: 56px; margin-bottom: 14px; line-height: 1; }
    .apit-blank h2 { margin: 0 0 10px; color: var(--ink-strong); font-size: 22px; }
    .apit-blank p  { color: var(--ink-muted); font-size: 13.5px; line-height: 1.6; margin: 0 0 20px; }

    .apit-titlebar {
      display: flex; gap: 8px; align-items: center;
      margin: 4px 0 14px;
    }
    .apit-title-input {
      flex: 1; min-width: 0;
      padding: 10px 12px;
      font-size: 18px; font-weight: 700;
      color: var(--ink-strong);
      border: 1px solid transparent; border-radius: 6px;
      background: transparent;
      transition: border-color .12s, background .12s;
      letter-spacing: -0.01em;
    }
    .apit-title-input::placeholder { color: var(--ink-faint, #a3a8b6); font-style: italic; font-weight: 600; }
    .apit-title-input:hover { background: #f4f5f8; }
    .apit-title-input:focus { outline: none; border-color: var(--brand); background: white; box-shadow: 0 0 0 3px rgba(162,93,220,.10); }

    .apit-urlbar {
      display: flex; gap: 0;
      border: 1px solid var(--border-strong);
      border-radius: 10px;
      overflow: hidden;
      background: white;
      box-shadow: 0 1px 3px rgba(15,23,41,.06);
      transition: border-color .12s, box-shadow .12s;
    }
    .apit-urlbar:focus-within {
      border-color: var(--brand);
      box-shadow: 0 0 0 3px rgba(162,93,220,.12);
    }
    .apit-method-select {
      border: 0; background: transparent;
      padding: 0 14px;
      font-weight: 800; font-size: 12px;
      letter-spacing: .05em;
      font-family: ui-monospace, "SF Mono", Menlo, monospace;
      cursor: pointer;
      border-right: 1px solid var(--border);
      min-width: 100px;
      color: var(--ink-strong);
      text-transform: uppercase;
    }
    .apit-method-select:focus { outline: none; }
    /* Tint the method dropdown by selected method — matches the
       saved-request pills for instant recognition. */
    .apit-method-select.apit-method-get     { color: #15803d; }
    .apit-method-select.apit-method-post    { color: #b45309; }
    .apit-method-select.apit-method-put     { color: #1e40af; }
    .apit-method-select.apit-method-patch   { color: #6d28d9; }
    .apit-method-select.apit-method-delete  { color: #b91c1c; }
    .apit-url-input {
      flex: 1; min-width: 0;
      border: 0; padding: 13px 14px;
      font-family: ui-monospace, "SF Mono", Menlo, monospace;
      font-size: 13.5px;
      color: var(--ink-strong);
      outline: none;
    }
    .apit-url-input::placeholder { color: var(--ink-faint, #a3a8b6); }
    .apit-send {
      border-radius: 0 !important;
      border: 0 !important;
      padding: 0 28px !important;
      font-weight: 700;
      letter-spacing: 0.02em;
      min-width: 100px;
    }

    /* ── Tabs (request) + (response) ────────────────────── */
    .apit-tabs {
      margin-top: 14px;
      background: white;
      border: 1px solid var(--border); border-radius: 8px;
      overflow: hidden;
    }
    .apit-tab-strip {
      display: flex;
      border-bottom: 1px solid var(--border-row);
      background: #fafbfc;
    }
    .apit-tab {
      padding: 10px 14px;
      border: 0; background: transparent;
      font-size: 12.5px; font-weight: 600;
      color: var(--ink-muted);
      cursor: pointer;
      display: inline-flex; align-items: center; gap: 6px;
      border-bottom: 2px solid transparent;
      margin-bottom: -1px;
    }
    .apit-tab:hover { color: var(--ink-strong); }
    .apit-tab.is-on {
      color: var(--ink-strong);
      border-bottom-color: var(--brand);
      background: white;
    }
    .apit-tab-count {
      background: rgba(0,0,0,.06);
      color: var(--ink-muted);
      padding: 0 6px; border-radius: 10px;
      font-size: 10.5px; font-weight: 700;
      letter-spacing: .03em;
    }
    .apit-tab-body { padding: 14px 16px; }

    /* Headers grid */
    .apit-headers { display: flex; flex-direction: column; gap: 6px; }
    .apit-header-row {
      display: grid; grid-template-columns: 22px 1fr 1.6fr 22px;
      gap: 6px; align-items: center;
    }
    .apit-header-row.is-off .apit-header-key,
    .apit-header-row.is-off .apit-header-value {
      opacity: 0.5; text-decoration: line-through;
    }
    .apit-header-key, .apit-header-value {
      padding: 7px 10px;
      border: 1px solid var(--border); border-radius: 4px;
      font: 13px ui-monospace, "SF Mono", Menlo, monospace;
      background: white;
    }
    .apit-header-key:focus, .apit-header-value:focus {
      outline: none; border-color: var(--brand);
      box-shadow: 0 0 0 2px rgba(162,93,220,.18);
    }
    .apit-add-row {
      align-self: flex-start;
      border: 1px dashed var(--border-strong);
      background: white; padding: 6px 12px;
      font-size: 12px; font-weight: 600; color: var(--ink-muted);
      border-radius: 4px; cursor: pointer;
      margin-top: 4px;
    }
    .apit-add-row:hover { border-color: var(--brand); color: var(--brand); }

    /* Body */
    .apit-body-pane { display: flex; flex-direction: column; gap: 10px; }
    .apit-body-types, .apit-auth-types {
      display: flex; gap: 6px; flex-wrap: wrap;
    }
    .apit-body-type, .apit-auth-type {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 5px 10px;
      border: 1px solid var(--border); border-radius: 999px;
      font-size: 12px; cursor: pointer;
      background: white;
    }
    .apit-body-type.is-on, .apit-auth-type.is-on {
      background: var(--brand-soft);
      border-color: var(--brand);
      color: var(--brand);
      font-weight: 600;
    }
    .apit-body-type input, .apit-auth-type input { display: none; }
    .apit-body-text {
      width: 100%;
      min-height: 180px;
      font: 12.5px ui-monospace, "SF Mono", Menlo, monospace;
      padding: 10px 12px;
      border: 1px solid var(--border); border-radius: 6px;
      background: #fbfcfe;
      resize: vertical;
      color: var(--ink-strong);
    }
    .apit-body-text:focus { outline: none; border-color: var(--brand); background: white; }
    .apit-body-text--ro { white-space: pre-wrap; word-break: break-word; }

    /* Auth */
    .apit-auth-fields { display: grid; gap: 10px; margin-top: 12px; }
    .apit-field { display: grid; gap: 4px; }
    .apit-field-label { font-size: 11.5px; font-weight: 600; color: var(--ink-muted); }
    .apit-field input {
      padding: 8px 10px; border: 1px solid var(--border); border-radius: 4px;
      font: 13px ui-monospace, "SF Mono", Menlo, monospace;
    }
    .apit-field input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(162,93,220,.18); }
    .apit-auth-note {
      padding: 10px 12px;
      background: rgba(0,115,234,.06);
      border: 1px solid rgba(0,115,234,.18);
      border-radius: 6px;
      font-size: 12px; color: #0044a3;
    }
    .apit-auth-note code {
      background: rgba(0,0,0,.05); padding: 1px 6px; border-radius: 3px;
      font-size: 11.5px;
    }

    /* Response */
    .apit-response {
      margin-top: 14px;
      background: white;
      border: 1px solid var(--border); border-radius: 8px;
      overflow: hidden;
    }
    .apit-response--idle, .apit-response--loading {
      padding: 30px; text-align: center; color: var(--ink-muted);
      font-size: 13px;
    }
    .apit-response-head {
      display: flex; align-items: center; gap: 12px;
      padding: 10px 14px;
      background: #fafbfc;
      border-bottom: 1px solid var(--border-row);
      flex-wrap: wrap;
    }
    .apit-status {
      padding: 3px 10px;
      border-radius: 999px;
      font: 12px ui-monospace, "SF Mono", Menlo, monospace;
      font-weight: 700;
    }
    .apit-status.is-good { background: rgba(34,197,94,.14); color: #166534; }
    .apit-status.is-warn { background: rgba(245,158,11,.16); color: #7a4205; }
    .apit-status.is-bad  { background: rgba(226,68,92,.14); color: #8a1024; }
    .apit-status.is-mid  { background: rgba(0,115,234,.10); color: #0044a3; }
    .apit-status-bad { color: #8a1024; font-weight: 700; }
    .apit-response-meta { font-size: 11.5px; color: var(--ink-muted); }
    .apit-response-tabs { margin-left: auto; display: flex; gap: 0; }
    .apit-response-tabs .apit-tab { padding: 4px 10px; }
    .apit-response-tabs .apit-tab.is-on { background: white; }
    .apit-response-headers {
      padding: 8px 14px;
      max-height: 360px; overflow-y: auto;
    }
    .apit-header-line {
      display: grid; grid-template-columns: 220px 1fr;
      gap: 12px; padding: 4px 0;
      border-bottom: 1px dashed var(--border-row);
      font: 12px ui-monospace, "SF Mono", Menlo, monospace;
    }
    .apit-header-line:last-child { border-bottom: 0; }
    .apit-header-line-key { color: var(--ink-muted); font-weight: 600; }
    .apit-header-line-val { color: var(--ink-strong); word-break: break-word; }
    .apit-response .apit-body-text--ro {
      border: 0; border-radius: 0; background: white;
      max-height: 480px; overflow: auto;
      padding: 12px 14px;
      margin: 0;
    }

    /* Environment editor modal */
    .apit-env-modal {
      width: min(820px, 92vw); max-height: 80vh;
      background: white; border-radius: 12px; overflow: hidden;
      display: flex; flex-direction: column;
      box-shadow: 0 20px 50px rgba(15,23,41,.30);
    }
    .apit-env-modal header {
      display: flex; align-items: center; gap: 12px;
      padding: 14px 18px; border-bottom: 1px solid var(--border);
    }
    .apit-env-modal header h3 { margin: 0; font-size: 14px; flex: 1; color: var(--ink-strong); }
    .apit-env-body {
      display: grid; grid-template-columns: 220px 1fr;
      flex: 1; min-height: 0;
    }
    .apit-env-list {
      border-right: 1px solid var(--border);
      padding: 8px;
      overflow-y: auto;
      display: flex; flex-direction: column; gap: 2px;
    }
    .apit-env-row {
      padding: 8px 10px;
      text-align: left;
      border: 0; background: transparent;
      cursor: pointer; border-radius: 4px;
      font-size: 13px; color: var(--ink-strong);
    }
    .apit-env-row:hover { background: #f8f9fb; }
    .apit-env-row.is-active { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
    .apit-env-vars {
      padding: 14px 16px;
      overflow-y: auto;
    }
    .apit-var-row {
      display: grid; grid-template-columns: 180px 1fr 22px;
      gap: 8px; align-items: center;
      padding: 4px 0;
    }
    .apit-var-key {
      font: 12.5px ui-monospace, "SF Mono", Menlo, monospace;
      color: var(--ink-strong);
      padding: 6px 8px;
      background: rgba(0,0,0,.04);
      border-radius: 4px;
    }
    .apit-var-val {
      padding: 6px 10px;
      border: 1px solid var(--border); border-radius: 4px;
      font: 12.5px ui-monospace, "SF Mono", Menlo, monospace;
    }
    .apit-var-val:focus { outline: none; border-color: var(--brand); }
  `;
  document.head.appendChild(s);
}

Object.assign(window, { ApiTesterView });
