"""Tests for the standalone local provisioning service script."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Self

import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient

import scripts.local_mindroom_provisioning_service as provisioning
from mindroom.matrix import provisioning as matrix_provisioning

if TYPE_CHECKING:
    from pathlib import Path

    import httpx


def _service_config(
    state_path: Path,
    *,
    google_oauth_client_id: str | None = None,
    google_oauth_client_secret: str | None = None,
) -> provisioning.ServiceConfig:
    return provisioning.ServiceConfig(
        matrix_homeserver="https://mindroom.chat",
        matrix_server_name="mindroom.chat",
        matrix_ssl_verify=True,
        matrix_registration_token="server-secret-token",  # noqa: S106
        state_path=state_path,
        pair_code_ttl_seconds=600,
        pair_poll_interval_seconds=3,
        cors_origins=["https://chat.mindroom.chat"],
        listen_host="127.0.0.1",
        listen_port=8776,
        google_oauth_client_id=google_oauth_client_id,
        google_oauth_client_secret=google_oauth_client_secret,
    )


@pytest.mark.parametrize(
    ("client_id", "client_secret"),
    [("google-client-id", None), (None, "google-client-secret")],
)
def test_service_config_rejects_partial_google_oauth_client(
    monkeypatch: pytest.MonkeyPatch,
    client_id: str | None,
    client_secret: str | None,
) -> None:
    """The provisioning service fails at startup when only half the Google client is configured."""
    monkeypatch.setenv("MATRIX_REGISTRATION_TOKEN", "server-secret-token")
    monkeypatch.delenv("MATRIX_REGISTRATION_TOKEN_FILE", raising=False)
    monkeypatch.delenv("MINDROOM_GOOGLE_OAUTH_CLIENT_SECRET_FILE", raising=False)
    if client_id is None:
        monkeypatch.delenv("MINDROOM_GOOGLE_OAUTH_CLIENT_ID", raising=False)
    else:
        monkeypatch.setenv("MINDROOM_GOOGLE_OAUTH_CLIENT_ID", client_id)
    if client_secret is None:
        monkeypatch.delenv("MINDROOM_GOOGLE_OAUTH_CLIENT_SECRET", raising=False)
    else:
        monkeypatch.setenv("MINDROOM_GOOGLE_OAUTH_CLIENT_SECRET", client_secret)

    with pytest.raises(ValueError, match="must be configured together"):
        provisioning._load_service_config_from_env()


def _patch_matrix_auth(monkeypatch: pytest.MonkeyPatch) -> None:
    token_to_user = {
        "token-alice": "@alice:mindroom.chat",
        "token-bob": "@bob:mindroom.chat",
    }

    async def _fake_matrix_whoami(config: provisioning.ServiceConfig, access_token: str) -> str:
        del config
        user_id = token_to_user.get(access_token)
        if user_id is None:
            raise HTTPException(status_code=401, detail="Invalid Matrix access token")
        return user_id

    monkeypatch.setattr(provisioning, "_matrix_whoami", _fake_matrix_whoami)


def _managed_agent_username(entity_name: str, namespace: str) -> str:
    return f"{provisioning.MANAGED_AGENT_USERNAME_PREFIX}{entity_name}_{namespace}"


def _invalid_managed_agent_username(case: str, namespace: str) -> str:
    if case == "missing_entity_between_prefix_and_namespace":
        return _managed_agent_username("", namespace)
    if case == "wrong_namespace_suffix":
        return f"{_managed_agent_username('code', namespace)}x"
    if case == "wrong_prefix_for_namespace":
        return f"other_code_{namespace}"
    if case == "plain_username_without_namespace":
        return f"{provisioning.MANAGED_AGENT_USERNAME_PREFIX}foo"
    if case == "invalid_localpart_for_namespace":
        return _managed_agent_username("Foo", namespace)
    msg = f"Unknown invalid username case: {case}"
    raise ValueError(msg)


def _pair_local_client(client: TestClient) -> dict[str, str]:
    pair_code = client.post(
        "/v1/local-mindroom/pair/start",
        headers={"Authorization": "Bearer token-alice"},
    ).json()["pair_code"]
    complete = client.post(
        "/v1/local-mindroom/pair/complete",
        json={
            "pair_code": pair_code,
            "client_name": "alice-macbook",
            "client_pubkey_or_fingerprint": "sha256:abc123",
        },
    )
    assert complete.status_code == 200
    return complete.json()


def _set_connection_namespace(state_path: Path, connection_id: str, namespace: str | None) -> None:
    """Edit the persisted state file the way an operator would (service stopped)."""
    payload = json.loads(state_path.read_text(encoding="utf-8"))
    for item in payload["connections"]:
        if item["id"] == connection_id:
            if namespace is None:
                item.pop("namespace", None)
            else:
                item["namespace"] = namespace
    state_path.write_text(json.dumps(payload), encoding="utf-8")


def _install_fake_register(monkeypatch: pytest.MonkeyPatch, register_calls: list[str]) -> None:
    async def _fake_register(
        config: provisioning.ServiceConfig,
        payload: provisioning.RegisterAgentRequest,
    ) -> provisioning.RegisterAgentResponse:
        del config
        register_calls.append(payload.username)
        return provisioning.RegisterAgentResponse(
            status="created",
            user_id=f"@{payload.username}:mindroom.chat",
        )

    monkeypatch.setattr(provisioning, "_register_agent_with_matrix", _fake_register)


def _post_register_agent(client: TestClient, complete: dict[str, str], username: str) -> httpx.Response:
    return client.post(
        "/v1/local-mindroom/register-agent",
        json={
            "homeserver": "https://mindroom.chat",
            "username": username,
            "password": "agent-pass-123",
            "display_name": "CodeAgent",
        },
        headers={
            "X-Local-MindRoom-Client-Id": complete["client_id"],
            "X-Local-MindRoom-Client-Secret": complete["client_secret"],
        },
    )


def test_pairing_and_register_agent_flow(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """End-to-end happy path: pair -> complete -> register agent -> revoke."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    async def _fake_register(
        config: provisioning.ServiceConfig,
        payload: provisioning.RegisterAgentRequest,
    ) -> provisioning.RegisterAgentResponse:
        del config
        return provisioning.RegisterAgentResponse(
            status="created",
            user_id=f"@{payload.username}:mindroom.chat",
        )

    monkeypatch.setattr(provisioning, "_register_agent_with_matrix", _fake_register)

    with TestClient(app) as client:
        start = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert start.status_code == 200
        start_payload = start.json()
        pair_code = start_payload["pair_code"]
        pair_session_id = start_payload["pair_session_id"]

        pending = client.get(
            "/v1/local-mindroom/pair/status",
            headers={
                "Authorization": "Bearer token-alice",
                provisioning.PAIR_STATUS_SESSION_HEADER: pair_session_id,
            },
        )
        assert pending.status_code == 200
        assert pending.json()["status"] == "pending"

        complete = client.post(
            "/v1/local-mindroom/pair/complete",
            json={
                "pair_code": pair_code,
                "client_name": "alice-macbook",
                "client_pubkey_or_fingerprint": "sha256:abc123",
            },
        )
        assert complete.status_code == 200
        payload = complete.json()
        client_id = payload["client_id"]
        client_secret = payload["client_secret"]
        assert payload["owner_user_id"] == "@alice:mindroom.chat"
        assert isinstance(payload["namespace"], str)
        assert len(payload["namespace"]) == 8
        assert payload["namespace"] == payload["connection"]["namespace"]
        agent_username = _managed_agent_username("code", payload["namespace"])

        connected = client.get(
            "/v1/local-mindroom/pair/status",
            headers={
                "Authorization": "Bearer token-alice",
                provisioning.PAIR_STATUS_SESSION_HEADER: pair_session_id,
            },
        )
        assert connected.status_code == 200
        assert connected.json()["status"] == "connected"

        register = client.post(
            "/v1/local-mindroom/register-agent",
            json={
                "homeserver": "https://mindroom.chat",
                "username": agent_username,
                "password": "agent-pass-123",
                "display_name": "CodeAgent",
            },
            headers={
                "X-Local-MindRoom-Client-Id": client_id,
                "X-Local-MindRoom-Client-Secret": client_secret,
            },
        )
        assert register.status_code == 200
        assert register.json()["status"] == "created"
        assert register.json()["user_id"] == f"@{agent_username}:mindroom.chat"

        revoke = client.delete(
            f"/v1/local-mindroom/connections/{client_id}",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert revoke.status_code == 200
        assert revoke.json()["revoked"] is True

        register_after_revoke = client.post(
            "/v1/local-mindroom/register-agent",
            json={
                "homeserver": "https://mindroom.chat",
                "username": _managed_agent_username("other", payload["namespace"]),
                "password": "agent-pass-123",
                "display_name": "OtherAgent",
            },
            headers={
                "X-Local-MindRoom-Client-Id": client_id,
                "X-Local-MindRoom-Client-Secret": client_secret,
            },
        )
        assert register_after_revoke.status_code == 403


def test_paired_client_fetches_google_oauth_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Only an authenticated paired runtime receives the installed-app client."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(
        _service_config(
            tmp_path / "state.json",
            google_oauth_client_id="google-client-id",
            google_oauth_client_secret="google-client-secret",  # noqa: S106
        ),
    )

    with TestClient(app) as client:
        complete = _pair_local_client(client)
        missing_auth = client.get("/v1/local-mindroom/oauth/google-client")
        response = client.get(
            "/v1/local-mindroom/oauth/google-client",
            headers={
                "X-Local-MindRoom-Client-Id": complete["client_id"],
                "X-Local-MindRoom-Client-Secret": complete["client_secret"],
            },
        )

    assert missing_auth.status_code == 401
    assert response.status_code == 200
    assert response.headers["Cache-Control"] == "no-store"
    assert response.json() == {
        "client_id": "google-client-id",
        "client_secret": "google-client-secret",
    }


def test_google_oauth_client_endpoint_requires_server_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """A paired client receives an explicit error when the server has no Google app."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    with TestClient(app) as client:
        complete = _pair_local_client(client)
        response = client.get(
            "/v1/local-mindroom/oauth/google-client",
            headers={
                "X-Local-MindRoom-Client-Id": complete["client_id"],
                "X-Local-MindRoom-Client-Secret": complete["client_secret"],
            },
        )

    assert response.status_code == 503
    assert response.json()["detail"] == "Google OAuth client is not configured"


def test_pair_status_accepts_session_header_without_pair_code_query(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Pair status polling should not require putting the pair code in the URL."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    with TestClient(app) as client:
        start = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert start.status_code == 200
        pair_session_id = start.json()["pair_session_id"]

        pending = client.get(
            "/v1/local-mindroom/pair/status",
            headers={
                "Authorization": "Bearer token-alice",
                provisioning.PAIR_STATUS_SESSION_HEADER: pair_session_id,
            },
        )

        assert pending.status_code == 200
        assert pending.json()["status"] == "pending"


def test_pair_status_rejects_pair_code_query_without_session_header(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Pair status polling should not accept the short pair code in the URL."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    with TestClient(app) as client:
        start = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert start.status_code == 200

        pending = client.get(
            "/v1/local-mindroom/pair/status",
            params={"pair_code": start.json()["pair_code"]},
            headers={"Authorization": "Bearer token-alice"},
        )

        assert pending.status_code == 400
        assert pending.json()["detail"] == "Missing pair session id"


def test_pair_status_rejects_missing_session_header(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Pair status should require the opaque session header."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    with TestClient(app) as client:
        result = client.get(
            "/v1/local-mindroom/pair/status",
            headers={"Authorization": "Bearer token-alice"},
        )

        assert result.status_code == 400
        assert result.json()["detail"] == "Missing pair session id"


@pytest.mark.parametrize(
    ("invalid_username_case", "expected_status"),
    [
        ("missing_entity_between_prefix_and_namespace", 403),
        ("wrong_namespace_suffix", 403),
        ("wrong_prefix_for_namespace", 403),
        ("plain_username_without_namespace", 403),
        ("invalid_localpart_for_namespace", 400),
    ],
)
def test_register_agent_rejects_username_outside_connection_namespace(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    invalid_username_case: str,
    expected_status: int,
) -> None:
    """A local client must not register Matrix users outside its assigned namespace."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))
    register_calls: list[str] = []

    async def _fake_register(
        config: provisioning.ServiceConfig,
        payload: provisioning.RegisterAgentRequest,
    ) -> provisioning.RegisterAgentResponse:
        del config
        register_calls.append(payload.username)
        return provisioning.RegisterAgentResponse(
            status="created",
            user_id=f"@{payload.username}:mindroom.chat",
        )

    monkeypatch.setattr(provisioning, "_register_agent_with_matrix", _fake_register)

    with TestClient(app) as client:
        pair_code = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        ).json()["pair_code"]
        complete = client.post(
            "/v1/local-mindroom/pair/complete",
            json={
                "pair_code": pair_code,
                "client_name": "alice-macbook",
                "client_pubkey_or_fingerprint": "sha256:abc123",
            },
        ).json()
        namespace = complete["namespace"]

        register = client.post(
            "/v1/local-mindroom/register-agent",
            json={
                "homeserver": "https://mindroom.chat",
                "username": _invalid_managed_agent_username(invalid_username_case, namespace),
                "password": "agent-pass-123",
                "display_name": "CodeAgent",
            },
            headers={
                "X-Local-MindRoom-Client-Id": complete["client_id"],
                "X-Local-MindRoom-Client-Secret": complete["client_secret"],
            },
        )

        assert register.status_code == expected_status
        if expected_status == 403:
            assert register.json()["detail"] == "Requested username is outside this local connection namespace"
        else:
            assert "not a valid Matrix localpart" in register.json()["detail"]
        assert register_calls == []


def test_register_agent_allows_plain_username_for_namespace_exempt_connection(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """A connection with operator-set namespace "" may register plain mindroom_<entity> usernames."""
    _patch_matrix_auth(monkeypatch)
    state_path = tmp_path / "state.json"
    register_calls: list[str] = []
    _install_fake_register(monkeypatch, register_calls)

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        complete = _pair_local_client(client)

    _set_connection_namespace(state_path, complete["client_id"], "")

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        register = _post_register_agent(client, complete, "mindroom_foo")

        assert register.status_code == 200
        assert register.json()["status"] == "created"
        assert register.json()["user_id"] == "@mindroom_foo:mindroom.chat"
        assert register_calls == ["mindroom_foo"]


@pytest.mark.parametrize(
    ("username", "expected_status"),
    [
        ("other_foo", 403),  # missing managed prefix
        ("mindroom_", 403),  # bare prefix without entity
        ("mindroom_Foo", 400),  # invalid Matrix localpart (uppercase)
    ],
)
def test_register_agent_namespace_exempt_connection_still_requires_managed_prefix(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    username: str,
    expected_status: int,
) -> None:
    """Namespace-exempt connections still only get mindroom_-prefixed valid localparts."""
    _patch_matrix_auth(monkeypatch)
    state_path = tmp_path / "state.json"
    register_calls: list[str] = []
    _install_fake_register(monkeypatch, register_calls)

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        complete = _pair_local_client(client)

    _set_connection_namespace(state_path, complete["client_id"], "")

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        register = _post_register_agent(client, complete, username)

        assert register.status_code == expected_status
        assert register_calls == []


@pytest.mark.parametrize("corrupt_namespace", [None, "null", " "], ids=["missing_key", "json_null", "whitespace"])
def test_state_load_fails_closed_for_missing_or_blank_namespace(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    corrupt_namespace: str | None,
) -> None:
    """Only a literal "" exempts; missing, null, or whitespace namespaces must fail closed to a derived one."""
    _patch_matrix_auth(monkeypatch)
    state_path = tmp_path / "state.json"
    register_calls: list[str] = []
    _install_fake_register(monkeypatch, register_calls)

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        complete = _pair_local_client(client)

    if corrupt_namespace == "null":
        payload = json.loads(state_path.read_text(encoding="utf-8"))
        payload["connections"][0]["namespace"] = None
        state_path.write_text(json.dumps(payload), encoding="utf-8")
    else:
        _set_connection_namespace(state_path, complete["client_id"], corrupt_namespace)

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        register = _post_register_agent(client, complete, "mindroom_foo")
        assert register.status_code == 403
        assert register_calls == []

        listed = client.get(
            "/v1/local-mindroom/connections",
            headers={"Authorization": "Bearer token-alice"},
        )
        derived = listed.json()["connections"][0]["namespace"]
        assert isinstance(derived, str)
        assert derived.strip() != ""


def test_state_round_trip_preserves_empty_namespace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """An operator-set empty namespace must survive load and re-persist cycles."""
    _patch_matrix_auth(monkeypatch)
    state_path = tmp_path / "state.json"
    register_calls: list[str] = []
    _install_fake_register(monkeypatch, register_calls)

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        complete = _pair_local_client(client)

    _set_connection_namespace(state_path, complete["client_id"], "")

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        listed = client.get(
            "/v1/local-mindroom/connections",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert listed.status_code == 200
        assert listed.json()["connections"][0]["namespace"] == ""

        # Registering updates last_seen_at and re-persists state to disk.
        assert _post_register_agent(client, complete, "mindroom_foo").status_code == 200

    persisted = json.loads(state_path.read_text(encoding="utf-8"))
    assert [item["namespace"] for item in persisted["connections"]] == [""]

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        assert _post_register_agent(client, complete, "mindroom_bar").status_code == 200


def test_register_agent_validates_homeserver(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Register-agent should reject homeserver mismatches."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    async def _fake_register(
        config: provisioning.ServiceConfig,
        payload: provisioning.RegisterAgentRequest,
    ) -> provisioning.RegisterAgentResponse:
        del config, payload
        return provisioning.RegisterAgentResponse(status="created", user_id="@mindroom_code:mindroom.chat")

    monkeypatch.setattr(provisioning, "_register_agent_with_matrix", _fake_register)

    with TestClient(app) as client:
        pair_code = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        ).json()["pair_code"]
        complete = client.post(
            "/v1/local-mindroom/pair/complete",
            json={
                "pair_code": pair_code,
                "client_name": "alice-macbook",
                "client_pubkey_or_fingerprint": "sha256:abc123",
            },
        ).json()

        register = client.post(
            "/v1/local-mindroom/register-agent",
            json={
                "homeserver": "https://other.example",
                "username": "mindroom_code",
                "password": "agent-pass-123",
                "display_name": "CodeAgent",
            },
            headers={
                "X-Local-MindRoom-Client-Id": complete["client_id"],
                "X-Local-MindRoom-Client-Secret": complete["client_secret"],
            },
        )
        assert register.status_code == 400


def test_browser_auth_required_for_pair_start(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Pair start should reject requests without browser Matrix auth token."""
    _patch_matrix_auth(monkeypatch)
    app = provisioning.create_app(_service_config(tmp_path / "state.json"))

    with TestClient(app) as client:
        result = client.post("/v1/local-mindroom/pair/start")
        assert result.status_code == 401


def test_state_persists_between_restarts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Connections should survive process restarts via JSON state file."""
    _patch_matrix_auth(monkeypatch)
    state_path = tmp_path / "state.json"

    with TestClient(provisioning.create_app(_service_config(state_path))) as client:
        start = client.post(
            "/v1/local-mindroom/pair/start",
            headers={"Authorization": "Bearer token-alice"},
        )
        pair_code = start.json()["pair_code"]
        complete = client.post(
            "/v1/local-mindroom/pair/complete",
            json={
                "pair_code": pair_code,
                "client_name": "alice-linux",
                "client_pubkey_or_fingerprint": "sha256:def456",
            },
        )
        assert complete.status_code == 200

    with TestClient(provisioning.create_app(_service_config(state_path))) as restarted_client:
        listed = restarted_client.get(
            "/v1/local-mindroom/connections",
            headers={"Authorization": "Bearer token-alice"},
        )
        assert listed.status_code == 200
        assert len(listed.json()["connections"]) == 1
        assert isinstance(listed.json()["connections"][0]["namespace"], str)


@pytest.mark.asyncio
async def test_register_agent_user_in_use_respects_matrix_server_name_override(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """User-in-use should return user_id on configured MATRIX_SERVER_NAME domain."""
    config = provisioning.ServiceConfig(
        matrix_homeserver="https://internal-matrix:8448",
        matrix_server_name="mindroom.chat",
        matrix_ssl_verify=True,
        matrix_registration_token="server-secret-token",  # noqa: S106
        state_path=tmp_path / "state.json",
        pair_code_ttl_seconds=600,
        pair_poll_interval_seconds=3,
        cors_origins=["https://chat.mindroom.chat"],
        listen_host="127.0.0.1",
        listen_port=8776,
    )

    class _FakeResponse:
        status_code = 400
        is_success = False
        text = "M_USER_IN_USE"

        @staticmethod
        def json() -> dict[str, str]:
            return {
                "errcode": "M_USER_IN_USE",
                "error": "User ID already taken",
            }

    class _FakeAsyncClient:
        def __init__(self, *args: object, **kwargs: object) -> None:
            del args, kwargs

        async def __aenter__(self) -> Self:
            return self

        async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
            del exc_type, exc, tb

        async def post(
            self,
            url: str,
            *,
            json: dict[str, object],
            headers: dict[str, str] | None = None,
        ) -> _FakeResponse:
            del url, json, headers
            return _FakeResponse()

    monkeypatch.setattr(provisioning.httpx, "AsyncClient", _FakeAsyncClient)
    payload = provisioning.RegisterAgentRequest(
        homeserver="https://internal-matrix:8448",
        username="mindroom_code",
        password="agent-pass",  # noqa: S106
        display_name="CodeAgent",
    )

    result = await provisioning._register_agent_with_matrix(config, payload)
    assert result.status == "user_in_use"
    assert result.user_id == "@mindroom_code:mindroom.chat"


def test_client_error_detail_constants_match_service() -> None:
    """The runtime client classifies register-agent 403s by these exact strings."""
    assert matrix_provisioning._CONNECTION_REVOKED_DETAIL == provisioning.CONNECTION_REVOKED_DETAIL
    assert matrix_provisioning._NAMESPACE_MISMATCH_DETAIL == provisioning.NAMESPACE_MISMATCH_DETAIL
