"""User-perspective tests for ``mgf.apiprobe.core`` primitives.

Surfaces exercised:
  - Probe — the fluent builder.
  - Suite — accepts probes + checks.
  - Context — variables container.
  - Severity / RootCause / Tag — enum shapes.
  - Finding — frozen dataclass round-trip.
  - Response — typed snapshot.
  - MISSING — sentinel.
"""

from __future__ import annotations

from mgf.apiprobe.checks import DEFAULT_CHECKS
from mgf.apiprobe.core import (
    MISSING,
    Context,
    Finding,
    Probe,
    Response,
    RootCause,
    Severity,
    Suite,
    Tag,
)


class TestProbeBuilder:
    def test_get_constructor(self) -> None:
        p = Probe.get("https://example.com/users")
        assert p.method == "GET"
        assert "users" in p.path

    def test_post_with_json_body(self) -> None:
        p = Probe.post("https://example.com/users").body({"name": "x"})
        assert p.method == "POST"
        assert p.json_body == {"name": "x"}

    def test_with_header(self) -> None:
        p = Probe.get("https://example.com/").header("Accept", "application/json")
        assert ("Accept", "application/json") in p.headers

    def test_expect_status(self) -> None:
        p = Probe.get("https://example.com/").expect_status(200)
        assert 200 in p.expected_statuses

    def test_immutable_builder(self) -> None:
        # Fluent calls return NEW probes; the original is unchanged.
        p1 = Probe.get("https://example.com/")
        p2 = p1.expect_status(200)
        assert p1 is not p2
        assert p1.expected_statuses == ()


class TestEnums:
    def test_severity_three_levels(self) -> None:
        assert {s.value for s in Severity} == {"info", "warn", "critical"}

    def test_root_cause_categories_present(self) -> None:
        # Spot-check the load-bearing categories.
        names = {rc.name for rc in RootCause}
        assert "NETWORK" in names
        assert "AUTH" in names
        assert "PROVIDER_5XX" in names
        assert "UNKNOWN" in names

    def test_tag_values_present(self) -> None:
        names = {t.name for t in Tag}
        for required in ("READ", "WRITE", "PUBLIC", "PRIVATE", "UNSAFE"):
            assert required in names


class TestFinding:
    def test_finding_construct(self) -> None:
        f = Finding(
            code="security.headers.missing-hsts",
            severity=Severity.WARN,
            root_cause=RootCause.PROVIDER_SECURITY,
            message="HSTS missing",
            evidence={"header": "Strict-Transport-Security"},
        )
        assert f.code == "security.headers.missing-hsts"
        assert f.severity == Severity.WARN
        assert f.root_cause == RootCause.PROVIDER_SECURITY


class TestResponse:
    def test_response_carries_status_and_body(self) -> None:
        r = Response(
            status=200,
            headers=(("Content-Type", "application/json"),),
            body=b'{"ok":true}',
            elapsed_ms=12.5,
            url="https://example.com/api",
        )
        assert r.status == 200
        assert r.elapsed_ms == 12.5


class TestContext:
    def test_default_construct(self) -> None:
        ctx = Context()
        assert ctx.variables == {}

    def test_with_variables(self) -> None:
        ctx = Context(variables={"user_id": "abc"})
        assert ctx.variables["user_id"] == "abc"


class TestSuite:
    def test_suite_construct(self) -> None:
        probe = Probe.get("https://example.com/api")
        s = Suite(probes=[probe], checks=DEFAULT_CHECKS)
        # Suite is constructed; the actual run happens in unit /
        # integration tests where the network is mocked / live.
        assert s is not None


class TestMissingSentinel:
    def test_missing_is_singleton_with_repr(self) -> None:
        assert repr(MISSING) == "MISSING"
        assert MISSING is MISSING
