"""Tests for the local run viewer (strix.interface.viewer) and its path helpers."""

from __future__ import annotations

import json
import os
import sqlite3
import urllib.error
import urllib.request
from typing import TYPE_CHECKING
from urllib.parse import urlsplit

from strix.core.paths import latest_run_dir, runs_base_dir
from strix.interface.viewer.server import serve
from strix.interface.viewer.transcript import (
    build_run_state,
    read_report_markdown,
    read_run_summary,
    read_vulnerabilities,
)


if TYPE_CHECKING:
    from collections.abc import Mapping
    from pathlib import Path

    import pytest


def _make_run(base: Path, name: str, *, status: str, end_time: str | None) -> Path:
    run_dir = base / "strix_runs" / name
    state_dir = run_dir / ".state"
    state_dir.mkdir(parents=True)
    record = {"run_name": name, "status": status, "end_time": end_time}
    (run_dir / "run.json").write_text(json.dumps(record), encoding="utf-8")
    agents = {
        "statuses": {"root": "completed", "child": "running"},
        "names": {"root": "strix", "child": "recon"},
        "parent_of": {"root": None, "child": "root"},
    }
    (state_dir / "agents.json").write_text(json.dumps(agents), encoding="utf-8")
    return run_dir


def test_latest_run_dir_none_when_no_runs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.chdir(tmp_path)
    assert latest_run_dir() is None
    assert runs_base_dir() == tmp_path / "strix_runs"


def test_latest_run_dir_picks_newest_by_record_mtime(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    older = _make_run(tmp_path, "old", status="completed", end_time="2026-01-01T00:00:00Z")
    newer = _make_run(tmp_path, "new", status="running", end_time=None)
    # Force a newer mtime on the second run's record.
    os.utime(newer / "run.json", (2_000_000_000, 2_000_000_000))
    os.utime(older / "run.json", (1_000_000_000, 1_000_000_000))
    assert latest_run_dir() == newer


def test_read_run_summary_finished_flag(tmp_path: Path) -> None:
    finished = _make_run(tmp_path, "done", status="completed", end_time="2026-01-01T00:00:00Z")
    live = _make_run(tmp_path, "live", status="running", end_time=None)
    assert read_run_summary(finished)["finished"] is True
    assert read_run_summary(live)["finished"] is False
    # A terminal status without an end_time is not "finished".
    partial = _make_run(tmp_path, "partial", status="failed", end_time=None)
    assert read_run_summary(partial)["finished"] is False


def test_read_missing_artifacts_return_defaults(tmp_path: Path) -> None:
    run_dir = _make_run(tmp_path, "empty", status="running", end_time=None)
    assert read_vulnerabilities(run_dir) == []
    assert read_report_markdown(run_dir) == ""


def test_build_run_state_from_agents_json(tmp_path: Path) -> None:
    run_dir = _make_run(tmp_path, "graph", status="running", end_time=None)
    state = build_run_state(run_dir)
    ids = {a["id"] for a in state["agents"]}
    assert ids == {"root", "child"}
    child = next(a for a in state["agents"] if a["id"] == "child")
    assert child["parent_id"] == "root"
    assert child["name"] == "recon"
    # No agents.db, so no message/tool events.
    assert state["events"] == []


def test_build_run_state_keeps_same_call_id_separate_per_agent(tmp_path: Path) -> None:
    run_dir = _make_run(tmp_path, "tools", status="completed", end_time=None)
    agents_db = run_dir / ".state" / "agents.db"
    rows = [
        (
            "root",
            {
                "type": "function_call",
                "call_id": "exec_command_0",
                "name": "exec_command",
                "arguments": json.dumps({"cmd": "echo root"}),
            },
        ),
        (
            "root",
            {
                "type": "function_call_output",
                "call_id": "exec_command_0",
                "output": json.dumps({"success": True, "output": "root"}),
            },
        ),
        (
            "child",
            {
                "type": "function_call",
                "call_id": "exec_command_0",
                "name": "exec_command",
                "arguments": json.dumps({"cmd": "echo child"}),
            },
        ),
        (
            "child",
            {
                "type": "function_call_output",
                "call_id": "exec_command_0",
                "output": json.dumps({"success": True, "output": "child"}),
            },
        ),
    ]
    with sqlite3.connect(agents_db) as conn:
        conn.execute(
            """
            create table agent_messages (
                id integer primary key,
                session_id text not null,
                message_data text not null,
                created_at text not null
            )
            """
        )
        conn.executemany(
            """
            insert into agent_messages (session_id, message_data, created_at)
            values (?, ?, '2026-01-01T00:00:00+00:00')
            """,
            [(agent_id, json.dumps(message)) for agent_id, message in rows],
        )

    state = build_run_state(run_dir)
    tools = [event for event in state["events"] if event["type"] == "tool"]

    assert len(tools) == 2
    by_agent = {event["agent_id"]: event for event in tools}
    assert by_agent["root"]["data"]["args"] == {"cmd": "echo root"}
    assert by_agent["root"]["data"]["result"]["output"] == "root"
    assert by_agent["child"]["data"]["args"] == {"cmd": "echo child"}
    assert by_agent["child"]["data"]["result"]["output"] == "child"


def _get(url: str, *, cookie: str | None = None) -> tuple[int, str, bytes]:
    headers = {"Cookie": cookie} if cookie else {}
    req = urllib.request.Request(url, headers=headers)  # noqa: S310 - localhost test server
    with urllib.request.urlopen(req) as resp:  # noqa: S310 - localhost test server  # nosec B310
        return resp.status, resp.headers.get("Content-Type", ""), resp.read()


def test_server_serves_api_and_static(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    run_dir = _make_run(tmp_path, "served", status="completed", end_time="2026-01-01T00:00:00Z")

    assets = tmp_path / "bundle"
    (assets / "assets").mkdir(parents=True)
    (assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
    (assets / "assets" / "app.js").write_text("console.log(1)", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        status, ctype, body = _get(f"{url}/api/run")
        assert status == 200
        assert "application/json" in ctype
        assert json.loads(body)["finished"] is True

        status, _, body = _get(f"{url}/api/transcript")
        assert {a["id"] for a in json.loads(body)["agents"]} == {"root", "child"}

        # Real asset is served.
        status, ctype, _ = _get(f"{url}/assets/app.js")
        assert status == 200

        # Unknown non-API route falls back to index.html (SPA routing).
        status, ctype, body = _get(f"{url}/agents/root")
        assert status == 200
        assert b"<div id=root>" in body
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_server_event_endpoint_forwards_cta(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "evt", status="running", end_time=None)
    assets = tmp_path / "bundle"
    assets.mkdir()
    (assets / "index.html").write_text("x", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)

    seen: list[tuple[str, str | None]] = []
    monkeypatch.setattr(
        "strix.telemetry.posthog.viewer_cta_clicked",
        lambda cta, surface=None: seen.append((cta, surface)),
    )

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        body = json.dumps(
            {"event": "cta_clicked", "cta": "PR reviews", "surface": "sidebar_nav"}
        ).encode()
        req = urllib.request.Request(  # noqa: S310 - localhost test server
            f"{url}/api/event", data=body, headers={"Content-Type": "application/json"}
        )
        with urllib.request.urlopen(req) as resp:  # noqa: S310  # nosec B310
            assert resp.status == 204
        assert seen == [("PR reviews", "sidebar_nav")]
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_server_event_endpoint_forwards_email_funnel(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "evt2", status="running", end_time=None)
    assets = tmp_path / "bundle"
    assets.mkdir()
    (assets / "index.html").write_text("x", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)

    seen: list[tuple[str, str | None]] = []
    monkeypatch.setattr(
        "strix.telemetry.posthog.viewer_email_event",
        lambda step, purpose=None: seen.append((step, purpose)),
    )

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        # A whitelisted funnel event is forwarded; an unknown event is ignored.
        for payload, expected in (
            ({"event": "email_verified", "purpose": "report"}, [("email_verified", "report")]),
            ({"event": "not_a_real_event"}, [("email_verified", "report")]),
        ):
            req = urllib.request.Request(  # noqa: S310 - localhost test server
                f"{url}/api/event",
                data=json.dumps(payload).encode(),
                headers={"Content-Type": "application/json"},
            )
            with urllib.request.urlopen(req) as resp:  # noqa: S310  # nosec B310
                assert resp.status == 204
            assert seen == expected
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_server_event_endpoint_forwards_agent_steered(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "steerevt", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)

    seen: list[bool] = []
    monkeypatch.setattr("strix.telemetry.posthog.viewer_agent_steered", lambda: seen.append(True))

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        req = urllib.request.Request(  # noqa: S310 - localhost test server
            f"{url}/api/event",
            data=json.dumps({"event": "agent_steered"}).encode(),
            headers={"Content-Type": "application/json"},
        )
        with urllib.request.urlopen(req) as resp:  # noqa: S310  # nosec B310
            assert resp.status == 204
        assert seen == [True]
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_feedback_records_telemetry_on_success(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "fbtel", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)

    sent: list[bool] = []
    monkeypatch.setattr("strix.interface.viewer.auth.feedback_submit", lambda *_a: None)
    monkeypatch.setattr(
        "strix.telemetry.posthog.viewer_feedback_submitted", lambda: sent.append(True)
    )

    httpd, url, token = serve(run_dir, open_browser=False)
    try:
        cookie = _session_cookie(url, token)
        # A successful, session-holding submission relays and records telemetry.
        status, _ = _post(
            url, "/api/feedback", {"email": "a@b.com", "message": "hi"}, cookie=cookie
        )
        assert status == 200
        assert sent == [True]

        # A cookie-less caller is rejected and records nothing.
        sent.clear()
        status, _ = _post(url, "/api/feedback", {"email": "a@b.com", "message": "hi"})
        assert status == 403
        assert sent == []
    finally:
        httpd.shutdown()
        httpd.server_close()


def _post(
    url: str, path: str, payload: Mapping[str, object], *, cookie: str | None = None
) -> tuple[int, bytes]:
    headers = {"Content-Type": "application/json"}
    if cookie:
        headers["Cookie"] = cookie
    req = urllib.request.Request(  # noqa: S310 - localhost test server
        url + path, data=json.dumps(payload).encode(), headers=headers, method="POST"
    )
    try:
        with urllib.request.urlopen(req) as resp:  # noqa: S310  # nosec B310
            return resp.status, resp.read()
    except urllib.error.HTTPError as exc:
        return exc.code, exc.read()


def _session_cookie(url: str, token: str) -> str:
    """Bootstrap a session via the tokened URL and return its ``name=value`` cookie."""
    bootstrap = f"{url}/?token={token}"
    with urllib.request.urlopen(bootstrap) as resp:  # noqa: S310 - localhost test server  # nosec B310
        raw = str(resp.headers.get("Set-Cookie", ""))
    return raw.split(";", 1)[0]


def _cookie_name(url: str) -> str:
    """The per-server session cookie name, derived from the bound port."""
    return f"strix_viewer_session_{urlsplit(url).port}"


def _get_status(url: str, *, cookie: str | None = None) -> int:
    headers = {"Cookie": cookie} if cookie else {}
    req = urllib.request.Request(url, headers=headers)  # noqa: S310 - localhost test server
    try:
        with urllib.request.urlopen(req) as resp:  # noqa: S310  # nosec B310
            return int(resp.status)
    except urllib.error.HTTPError as exc:
        return int(exc.code)


def _bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    assets = tmp_path / "bundle"
    assets.mkdir()
    (assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)


def test_capability_issued_only_for_tokened_bootstrap(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "cookie", status="running", end_time=None)
    assets = tmp_path / "bundle"
    (assets / "assets").mkdir(parents=True)
    (assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
    (assets / "assets" / "app.js").write_text("1", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)

    httpd, url, token = serve(run_dir, open_browser=False)
    try:
        # A bare index load -- all a reachable client can do -- hands out nothing.
        with urllib.request.urlopen(url + "/") as resp:  # noqa: S310  # nosec B310
            assert resp.headers.get("Set-Cookie") is None

        # A wrong token is likewise refused the capability.
        with urllib.request.urlopen(f"{url}/?token=wrong") as resp:  # noqa: S310  # nosec B310
            assert resp.headers.get("Set-Cookie") is None

        # Only the correct bootstrap token mints the session cookie.
        with urllib.request.urlopen(f"{url}/?token={token}") as resp:  # noqa: S310  # nosec B310
            cookie = str(resp.headers.get("Set-Cookie", ""))
        assert f"{_cookie_name(url)}=" in cookie
        assert "HttpOnly" in cookie and "SameSite=Strict" in cookie

        # Static assets never carry it.
        with urllib.request.urlopen(url + "/assets/app.js") as resp:  # noqa: S310  # nosec B310
            assert resp.headers.get("Set-Cookie") is None
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_unauthorized_client_cannot_acquire_capability(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "exposed", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)

    delivered: list[tuple[str, str]] = []

    def handler(agent_id: str, message: str) -> bool:
        delivered.append((agent_id, message))
        return True

    httpd, url, _ = serve(run_dir, open_browser=False, steer_handler=handler)
    try:
        # A direct network client can reach the page but is handed no capability,
        # so replaying an empty/guessed cookie cannot steer a live scan.
        with urllib.request.urlopen(url + "/") as resp:  # noqa: S310  # nosec B310
            assert resp.headers.get("Set-Cookie") is None
        status, _ = _post(
            url,
            "/api/agents/steer",
            {"agent_id": "root", "message": "pwn"},
            cookie=f"{_cookie_name(url)}=",
        )
        assert status == 403
        assert delivered == []
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_auth_status_reflects_expiry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    run_dir = _make_run(tmp_path, "status", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)
    monkeypatch.setattr(
        "strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
    )
    verified = {"value": True}
    monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])

    httpd, url, token = serve(run_dir, open_browser=False)
    try:
        cookie = _session_cookie(url, token)
        _, _, body = _get(f"{url}/api/auth/status", cookie=cookie)
        assert json.loads(body) == {"verified": True, "email": "a@b.com"}

        # Once expired, status must advertise unverified so the SPA re-prompts.
        verified["value"] = False
        _, _, body = _get(f"{url}/api/auth/status", cookie=cookie)
        assert json.loads(body)["verified"] is False

        # A cookie-less caller never sees the cached email or verified state.
        verified["value"] = True
        _, _, body = _get(f"{url}/api/auth/status")
        assert json.loads(body) == {"verified": False, "email": None}
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_auth_mutations_require_session(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    run_dir = _make_run(tmp_path, "authmut", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)
    forgotten = {"value": False}
    monkeypatch.setattr("strix.interface.viewer.auth.forget", lambda: forgotten.update(value=True))

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        for path in ("/api/auth/forget", "/api/auth/otp/start", "/api/auth/otp/verify"):
            status, _ = _post(url, path, {"email": "a@b.com", "code": "123456"})
            assert status == 403, path
        assert forgotten["value"] is False
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_steer_requires_session_cookie(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    run_dir = _make_run(tmp_path, "steer", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)

    delivered: list[tuple[str, str]] = []

    def handler(agent_id: str, message: str) -> bool:
        delivered.append((agent_id, message))
        return True

    httpd, url, token = serve(run_dir, open_browser=False, steer_handler=handler)
    try:
        body = {"agent_id": "root", "message": "focus on auth"}
        # No cookie: rejected before reaching the live coordinator.
        status, _ = _post(url, "/api/agents/steer", body)
        assert status == 403
        assert delivered == []

        # With the session cookie the message is delivered.
        status, raw = _post(url, "/api/agents/steer", body, cookie=_session_cookie(url, token))
        assert status == 200
        assert json.loads(raw)["ok"] is True
        assert delivered == [("root", "focus on auth")]
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_report_send_requires_session_cookie(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    run_dir = _make_run(tmp_path, "report", status="completed", end_time="2026-01-01T00:00:00Z")
    _bundle(tmp_path, monkeypatch)

    # A verified machine token exists, but that alone must not authorize a caller.
    monkeypatch.setattr(
        "strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
    )

    httpd, url, token = serve(run_dir, open_browser=False)
    try:
        # No cookie: forbidden before the machine token is ever consulted.
        status, _ = _post(url, "/api/report/send", {})
        assert status == 403

        # With the cookie the request clears the session gate; it then reaches
        # the run resolver, so an unknown run is a 404 rather than a 403.
        status, _ = _post(
            url, "/api/report/send", {"run": "does-not-exist"}, cookie=_session_cookie(url, token)
        )
        assert status == 404
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_report_send_rejects_live_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    # A running scan would only produce a partial report, so the endpoint must
    # fail closed even for a verified, session-holding caller.
    run_dir = _make_run(tmp_path, "live", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)
    monkeypatch.setattr(
        "strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
    )

    httpd, url, token = serve(run_dir, open_browser=False)
    try:
        status, _ = _post(url, "/api/report/send", {}, cookie=_session_cookie(url, token))
        assert status == 409
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_historical_run_data_requires_verification(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    launched = _make_run(tmp_path, "launched", status="completed", end_time="2026-01-01T00:00:00Z")
    _make_run(tmp_path, "other", status="completed", end_time="2026-01-01T00:00:00Z")
    _bundle(tmp_path, monkeypatch)

    verified = {"value": False}
    monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])

    httpd, url, token = serve(launched, open_browser=False)
    try:
        # The launched run is always viewable, no verification and no cookie.
        status, _, _ = _get(f"{url}/api/run")
        assert status == 200

        cookie = _session_cookie(url, token)

        # A different run needs the session capability first: a cookie-less
        # caller is forbidden even once the machine is verified.
        verified["value"] = True
        assert _get_status(f"{url}/api/run?run=other") == 403

        # With the cookie but not verified, the history gate returns 401.
        verified["value"] = False
        assert _get_status(f"{url}/api/run?run=other", cookie=cookie) == 401

        # With both the cookie and verification, the historical run resolves.
        verified["value"] = True
        assert _get_status(f"{url}/api/run?run=other", cookie=cookie) == 200
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_runs_list_requires_session_and_verification(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    launched = _make_run(tmp_path, "launched", status="completed", end_time="2026-01-01T00:00:00Z")
    _make_run(tmp_path, "other", status="completed", end_time="2026-01-01T00:00:00Z")
    _bundle(tmp_path, monkeypatch)

    monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: True)

    def _runs(cookie: str | None) -> dict[str, object]:
        headers = {"Cookie": cookie} if cookie else {}
        req = urllib.request.Request(f"{url}/api/runs", headers=headers)  # noqa: S310
        with urllib.request.urlopen(req) as resp:  # noqa: S310 - localhost test server  # nosec B310
            return dict(json.loads(resp.read()))

    httpd, url, token = serve(launched, open_browser=False)
    try:
        # A cookie-less caller (even with the machine verified) only sees the
        # teaser count, never the run entries.
        payload = _runs(None)
        assert payload["locked"] is True
        assert payload["count"] == 2
        assert payload["runs"] == []

        # With the session cookie and verification, the entries unlock.
        payload = _runs(_session_cookie(url, token))
        assert payload["locked"] is False
        assert {r["name"] for r in payload["runs"]} == {"launched", "other"}  # type: ignore[attr-defined]
    finally:
        httpd.shutdown()
        httpd.server_close()


def test_concurrent_servers_use_distinct_cookies(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Cookies are host-scoped, not port-scoped: two viewers on 127.0.0.1 must
    not share a cookie slot, and one server's cookie must not pass the other's
    session gate."""
    run_a = _make_run(tmp_path / "a", "run-a", status="running", end_time=None)
    run_b = _make_run(tmp_path / "b", "run-b", status="running", end_time=None)
    _bundle(tmp_path, monkeypatch)
    monkeypatch.setattr(
        "strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
    )
    monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: True)

    httpd_a, url_a, token_a = serve(run_a, open_browser=False)
    httpd_b, url_b, token_b = serve(run_b, open_browser=False)
    try:
        cookie_a = _session_cookie(url_a, token_a)
        cookie_b = _session_cookie(url_b, token_b)

        # The two servers mint differently named cookies, so a browser stores both.
        assert cookie_a.split("=", 1)[0] == _cookie_name(url_a)
        assert cookie_b.split("=", 1)[0] == _cookie_name(url_b)
        assert cookie_a.split("=", 1)[0] != cookie_b.split("=", 1)[0]

        def _status(url: str, cookie: str) -> dict[str, object]:
            _, _, body = _get(f"{url}/api/auth/status", cookie=cookie)
            return dict(json.loads(body))

        # Each server honors its own cookie...
        assert _status(url_a, cookie_a)["verified"] is True
        assert _status(url_b, cookie_b)["verified"] is True
        # ...but treats the other server's cookie as session-less.
        assert _status(url_a, cookie_b)["verified"] is False
        assert _status(url_b, cookie_a)["verified"] is False
        # Even both cookies together (what a real browser would send) only
        # match the token minted by the receiving server.
        both = f"{cookie_a}; {cookie_b}"
        assert _status(url_a, both)["verified"] is True
        assert _status(url_b, both)["verified"] is True
    finally:
        httpd_a.shutdown()
        httpd_a.server_close()
        httpd_b.shutdown()
        httpd_b.server_close()


def test_server_rejects_path_traversal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    run_dir = _make_run(tmp_path, "guard", status="completed", end_time="2026-01-01T00:00:00Z")
    secret = tmp_path / "secret.txt"
    secret.write_text("top secret", encoding="utf-8")

    assets = tmp_path / "bundle"
    assets.mkdir()
    (assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
    monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)

    httpd, url, _ = serve(run_dir, open_browser=False)
    try:
        # A traversal target must never leak the file; it falls back to index.html.
        _, _, body = _get(f"{url}/..%2f..%2fsecret.txt")
        assert b"top secret" not in body
    finally:
        httpd.shutdown()
        httpd.server_close()
