"""Tests for ``mgf.apiprobe.checks.schema``."""

from __future__ import annotations

from pydantic import BaseModel
from tests.unit.checks.conftest import empty_ctx, make_response

from mgf.apiprobe.checks.schema import (
    BodyEmptyOnSuccess,
    BodyInvalidJson,
    SchemaShapeMismatch,
)
from mgf.apiprobe.core.finding import RootCause, Severity
from mgf.apiprobe.core.probe import Probe


class _User(BaseModel):
    id: str
    email: str


JSON_HEADERS = {"Content-Type": "application/json"}


class TestSchemaShapeMismatch:
    def test_silent_when_no_schema(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b'{"id": "x"}', headers=JSON_HEADERS)
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_when_body_empty(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(body=b"", headers=JSON_HEADERS)
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert findings == []  # BodyEmptyOnSuccess handles this case.

    def test_silent_when_ct_not_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(body=b"<xml/>", headers={"Content-Type": "text/xml"})
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert findings == []  # ContentTypeJsonExpected handles this case.

    def test_silent_when_body_invalid_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(body=b"not-json", headers=JSON_HEADERS)
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert findings == []  # BodyInvalidJson handles this case.

    def test_silent_on_valid_payload(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(
            body=b'{"id": "abc", "email": "a@b.c"}',
            headers=JSON_HEADERS,
        )
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_emits_with_issues(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(body=b'{"id": "abc"}', headers=JSON_HEADERS)
        findings = list(SchemaShapeMismatch().run(probe, resp, empty_ctx()))
        assert len(findings) == 1
        f = findings[0]
        assert f.code == "schema.body.shape-mismatch"
        assert f.severity is Severity.CRITICAL
        assert f.root_cause is RootCause.PROVIDER_CONTRACT
        assert f.evidence["issues"]
        assert f.evidence["issues"][0]["path"] == "$.email"


class TestBodyInvalidJson:
    def test_silent_when_no_body(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b"", headers=JSON_HEADERS)
        findings = list(BodyInvalidJson().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_when_ct_not_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b"<xml/>", headers={"Content-Type": "text/xml"})
        findings = list(BodyInvalidJson().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_on_valid_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b'{"a": 1}', headers=JSON_HEADERS)
        findings = list(BodyInvalidJson().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_emits_on_malformed_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b"{not valid", headers=JSON_HEADERS)
        findings = list(BodyInvalidJson().run(probe, resp, empty_ctx()))
        assert len(findings) == 1
        f = findings[0]
        assert f.code == "schema.body.invalid-json"
        assert "body_snippet" in f.evidence
        assert "parse_error" in f.evidence

    def test_evidence_truncates_long_body(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        big = b"{" * 1000
        resp = make_response(body=big, headers=JSON_HEADERS)
        findings = list(BodyInvalidJson().run(probe, resp, empty_ctx()))
        assert len(findings) == 1
        assert len(findings[0].evidence["body_snippet"]) <= 256


class TestBodyEmptyOnSuccess:
    def test_silent_when_no_schema(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(status=200, body=b"")
        findings = list(BodyEmptyOnSuccess().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_when_body_present(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(status=200, body=b'{"id":"x","email":"a@b.c"}', headers=JSON_HEADERS)
        findings = list(BodyEmptyOnSuccess().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_on_204(self) -> None:
        # 204 No Content is the canonical "intentionally empty 2xx".
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(status=204, body=b"")
        findings = list(BodyEmptyOnSuccess().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_on_non_2xx(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(status=404, body=b"")
        findings = list(BodyEmptyOnSuccess().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_emits_when_2xx_empty_with_schema(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_User)
        resp = make_response(status=200, body=b"")
        findings = list(BodyEmptyOnSuccess().run(probe, resp, empty_ctx()))
        assert len(findings) == 1
        assert findings[0].code == "schema.body.empty-on-success"
