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

from __future__ import annotations

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

from mgf.apiprobe.checks.contract import (
    ContentTypeJsonExpected,
    ContentTypeMissing,
    HeaderMissing,
    Server5xx,
    StatusUnexpected,
)
from mgf.apiprobe.core.finding import RootCause, Severity
from mgf.apiprobe.core.probe import Probe


class _Doc(BaseModel):
    id: str


class TestStatusUnexpected:
    def test_silent_when_no_expectations(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        findings = list(StatusUnexpected().run(probe, make_response(status=500), empty_ctx()))
        assert findings == []

    def test_silent_on_match(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_status(200)
        findings = list(StatusUnexpected().run(probe, make_response(status=200), empty_ctx()))
        assert findings == []

    def test_emits_on_mismatch(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_status(200)
        findings = list(StatusUnexpected().run(probe, make_response(status=500), empty_ctx()))
        assert len(findings) == 1
        f = findings[0]
        assert f.code == "contract.status.unexpected"
        assert f.severity is Severity.CRITICAL
        assert f.root_cause is RootCause.PROVIDER_CONTRACT
        assert f.evidence == {"expected": [200], "actual": 500}

    def test_emits_when_status_in_expectations_with_more(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_status(200, 304)
        findings = list(StatusUnexpected().run(probe, make_response(status=404), empty_ctx()))
        assert len(findings) == 1


class TestServer5xx:
    def test_silent_on_2xx(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        findings = list(Server5xx().run(probe, make_response(status=200), empty_ctx()))
        assert findings == []

    def test_silent_on_4xx(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        findings = list(Server5xx().run(probe, make_response(status=404), empty_ctx()))
        assert findings == []

    def test_emits_on_5xx(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        findings = list(Server5xx().run(probe, make_response(status=503), empty_ctx()))
        assert len(findings) == 1
        assert findings[0].code == "contract.status.server-error"
        assert findings[0].root_cause is RootCause.PROVIDER_5XX

    def test_silent_when_5xx_explicitly_expected(self) -> None:
        # Probes testing error paths should not get a Server5xx finding too.
        probe = Probe.get("https://x/y").id("p1").expect_status(503)
        findings = list(Server5xx().run(probe, make_response(status=503), empty_ctx()))
        assert findings == []


class TestHeaderMissing:
    def test_silent_when_no_expected_headers(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        findings = list(HeaderMissing().run(probe, make_response(), empty_ctx()))
        assert findings == []

    def test_silent_when_all_present(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_header("X-Request-Id")
        resp = make_response(headers={"X-Request-Id": "abc-123"})
        findings = list(HeaderMissing().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_emits_one_finding_per_missing_header(self) -> None:
        probe = (
            Probe.get("https://x/y")
            .id("p1")
            .expect_header("X-Request-Id")
            .expect_header("X-RateLimit-Remaining")
        )
        findings = list(HeaderMissing().run(probe, make_response(), empty_ctx()))
        assert len(findings) == 2
        codes = [f.evidence["expected_header"] for f in findings]
        assert "X-Request-Id" in codes
        assert "X-RateLimit-Remaining" in codes

    def test_case_insensitive_lookup(self) -> None:
        # Provider returns lowercase but probe uses canonical case.
        probe = Probe.get("https://x/y").id("p1").expect_header("X-Request-Id")
        resp = make_response(headers={"x-request-id": "abc"})
        findings = list(HeaderMissing().run(probe, resp, empty_ctx()))
        assert findings == []


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

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

    def test_emits_when_body_without_ct(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(body=b"{}")  # no headers at all
        findings = list(ContentTypeMissing().run(probe, resp, empty_ctx()))
        assert len(findings) == 1


class TestContentTypeJsonExpected:
    def test_silent_when_no_schema(self) -> None:
        probe = Probe.get("https://x/y").id("p1")
        resp = make_response(headers={"Content-Type": "text/plain"})
        findings = list(ContentTypeJsonExpected().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_when_no_content_type(self) -> None:
        # ContentTypeMissing handles this case — don't double-fire.
        probe = Probe.get("https://x/y").id("p1").expect_schema(_Doc)
        resp = make_response()
        findings = list(ContentTypeJsonExpected().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_on_application_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_Doc)
        resp = make_response(headers={"Content-Type": "application/json; charset=utf-8"})
        findings = list(ContentTypeJsonExpected().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_silent_on_problem_json(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_Doc)
        resp = make_response(headers={"Content-Type": "application/problem+json"})
        findings = list(ContentTypeJsonExpected().run(probe, resp, empty_ctx()))
        assert findings == []

    def test_emits_on_non_json_ct(self) -> None:
        probe = Probe.get("https://x/y").id("p1").expect_schema(_Doc)
        resp = make_response(headers={"Content-Type": "text/html"})
        findings = list(ContentTypeJsonExpected().run(probe, resp, empty_ctx()))
        assert len(findings) == 1
        assert findings[0].code == "contract.content-type.json-expected"
