"""Contract: the broker echoes `correlation_id` verbatim in every
response when the request carried one (Part 13 §5.6.3 / §7.1).

The wire contract pinned here:

  • Present in request → echoed in response (both success + error).
  • Absent in request → absent in response. No silent default.
  • Charset restricted (ASCII alphanumeric + `-_.`); out-of-charset
    or oversized (>128 bytes) values rejected at the gate with
    BadRequest.
  • SDK's `Client.correlation_scope("id")` context manager
    auto-injects the id into every wire op within the block.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

import pytest

from mgf.hrb import Client
from tests.contract.conftest import _SOCK_PATH, rt

if TYPE_CHECKING:
    import socket

pytestmark = pytest.mark.contract


def test_present_in_request_echoes_in_success(raw_broker: socket.socket) -> None:
    """Success response: when the request carries `correlation_id`,
    the broker echoes the EXACT same string. Charset exercise: every
    allowed character class (alphanumeric + - _ .)."""
    cid = "vmgr-claim-001.alpha_BETA-2026"
    resp = rt(raw_broker, {"op": "list", "scope": "hw:/test/**", "correlation_id": cid})
    assert resp.get("ok") is True
    assert resp.get("correlation_id") == cid


def test_absent_means_absent(raw_broker: socket.socket) -> None:
    """G2 honesty: a request WITHOUT `correlation_id` MUST produce a
    response without the field. No broker-side default."""
    resp = rt(raw_broker, {"op": "list", "scope": "hw:/test/**"})
    assert resp.get("ok") is True
    assert "correlation_id" not in resp


def test_echoes_on_error_response_too(raw_broker: socket.socket) -> None:
    """Error responses (ok:false) MUST also carry the echo — consumers
    building causal traces correlate failures, not just successes."""
    cid = "err-trace-A"
    resp = rt(raw_broker, {"op": "inspect", "urn": "hw:/nonexistent", "correlation_id": cid})
    assert resp.get("ok") is False
    assert resp.get("correlation_id") == cid


def test_out_of_charset_is_bad_request(raw_broker: socket.socket) -> None:
    """Charset filter (alphanumeric + `-_.`) enforced at the gate.
    Anything else → BadRequest. This is the safety promise that
    justifies the no-escape splice."""
    for bad in ["has space", "angle<bracket", "slash/inside"]:
        resp = rt(
            raw_broker,
            {
                "op": "list",
                "scope": "hw:/test/**",
                "correlation_id": bad,
            },
        )
        assert resp.get("ok") is False, f"expected reject for {bad!r}: {resp!r}"
        assert resp.get("err") == "BadRequest"


def test_oversized_is_bad_request(raw_broker: socket.socket) -> None:
    """Beyond 128 bytes → BadRequest. Bound stops a hostile request
    from forcing a large memmove during the splice."""
    huge = "a" * 130
    resp = rt(
        raw_broker,
        {
            "op": "list",
            "scope": "hw:/test/**",
            "correlation_id": huge,
        },
    )
    assert resp.get("ok") is False
    assert resp.get("err") == "BadRequest"


def test_non_string_type_is_bad_request(raw_broker: socket.socket) -> None:
    """A non-string value (number, bool, array, object, null) →
    BadRequest. The parser refuses to coerce silently."""
    for raw_value in ["42", "true", '["x"]', '{"k":1}', "null"]:
        line = '{"op":"list","scope":"hw:/test/**","correlation_id":' + raw_value + "}\n"
        raw_broker.send(line.encode())
        buf = b""
        while b"\n" not in buf:
            buf += raw_broker.recv(4096)
        resp = json.loads(buf.split(b"\n", 1)[0])
        assert resp.get("ok") is False, f"expected reject for {raw_value}: {resp!r}"
        assert resp.get("err") == "BadRequest"


# ----- SDK scope behavior (uses the SAME broker subprocess) ----------------


def test_sdk_correlation_scope_nests() -> None:
    """Nested scopes: inner shadows outer; the outer value is
    restored on inner-exit. Pure transport-state test — no real
    broker connection needed; the scope is a Client-level concept
    that mutates Transport state."""
    client = Client.__new__(Client)
    client._t = type("FakeT", (), {"_correlation_id": None})()  # type: ignore[assignment]

    assert client._t._correlation_id is None
    with client.correlation_scope("outer"):
        assert client._t._correlation_id == "outer"
        with client.correlation_scope("inner"):
            assert client._t._correlation_id == "inner"
        assert client._t._correlation_id == "outer"
    assert client._t._correlation_id is None


def test_sdk_correlation_scope_restored_on_exception() -> None:
    """An exception inside the scope MUST still restore the previous
    correlation_id — otherwise a single try/except in user code could
    leak the scope across unrelated future calls."""
    client = Client.__new__(Client)
    client._t = type("FakeT", (), {"_correlation_id": None})()  # type: ignore[assignment]

    with pytest.raises(ValueError, match="user-code"):
        with client.correlation_scope("temporary"):
            assert client._t._correlation_id == "temporary"
            raise ValueError("user-code error")
    assert client._t._correlation_id is None, "scope leaked across the exception boundary"


def test_sdk_correlation_scope_auto_injects_on_real_broker(
    raw_broker: socket.socket,
) -> None:
    """End-to-end: open a Client to the same broker the raw fixture
    is talking to; calls inside `correlation_scope` MUST hit the
    broker carrying the id (verifiable because the broker echoes it,
    but the SDK abstracts the response — so we verify indirectly
    by asserting the scope passes through without breaking the call)."""
    client = Client.connect(_SOCK_PATH)
    try:
        with client.correlation_scope("scope-end-to-end"):
            urns = client.list("hw:/test/**")
            assert "hw:/test/dev0" in urns
        # Outside the scope: still works.
        assert "hw:/test/dev0" in client.list("hw:/test/**")
    finally:
        client.close()
