"""Contract checks — status codes, headers, content-types.

See: ``DESIGN.md`` §7.2.

5 checks, all with ``RootCause.PROVIDER_CONTRACT`` (or ``PROVIDER_5XX``
for the dedicated server-error check):

  - ``StatusUnexpected``     — status not in ``probe.expected_statuses``
  - ``Server5xx``             — status 5xx
  - ``HeaderMissing``        — ``probe.expected_headers`` not all present
  - ``ContentTypeMissing``   — no ``Content-Type`` on a response with a body
  - ``ContentTypeJsonExpected`` — schema expected but Content-Type isn't JSON-shaped
"""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from mgf.apiprobe.core.check import Check
from mgf.apiprobe.core.finding import RootCause, Severity

if TYPE_CHECKING:
    from collections.abc import Iterable

    from mgf.apiprobe.core.context import Context
    from mgf.apiprobe.core.finding import Finding
    from mgf.apiprobe.core.probe import Probe
    from mgf.apiprobe.core.response import Response


class StatusUnexpected(Check):
    """Status not in ``probe.expected_statuses``."""

    code: ClassVar[str] = "contract.status.unexpected"
    severity_default: ClassVar[Severity] = Severity.CRITICAL
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_CONTRACT

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if not probe.expected_statuses:
            return  # Probe accepts any status.
        if response.status in probe.expected_statuses:
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message=(f"Expected status in {list(probe.expected_statuses)}, got {response.status}."),
            evidence={
                "expected": list(probe.expected_statuses),
                "actual": response.status,
            },
            remediation=(
                "If the new status is intentional, update the probe's "
                "expect_status(...) call. Otherwise, investigate the provider."
            ),
        )


class Server5xx(Check):
    """5xx response. Provider is unhealthy until proven otherwise."""

    code: ClassVar[str] = "contract.status.server-error"
    severity_default: ClassVar[Severity] = Severity.CRITICAL
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_5XX

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if not response.is_server_error:
            return
        # Skip if the probe explicitly accepts this status (provider-
        # specific test of an error path); StatusUnexpected covers
        # the "not in expected" case.
        if response.status in probe.expected_statuses:
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message=f"Server error: status {response.status}.",
            evidence={"status": response.status, "url": response.url},
            remediation=(
                "Check provider service health, on-call channels, "
                "and recent deploys. Retry with backoff if transient."
            ),
        )


class HeaderMissing(Check):
    """One or more ``probe.expected_headers`` is absent from the response."""

    code: ClassVar[str] = "contract.header.missing"
    severity_default: ClassVar[Severity] = Severity.WARN
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_CONTRACT

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        for name in probe.expected_headers:
            if response.header(name) is None:
                yield self.finding(
                    probe_id=probe.probe_id,
                    message=f"Expected header {name!r} not present in response.",
                    evidence={
                        "expected_header": name,
                        "headers_present": sorted(response.headers.keys()),
                    },
                    remediation=(
                        f"Provider should set {name!r}. If the contract changed, "
                        f"update the probe's expect_header(...) call."
                    ),
                )


class ContentTypeMissing(Check):
    """Response has a body but no ``Content-Type`` header."""

    code: ClassVar[str] = "contract.content-type.missing"
    severity_default: ClassVar[Severity] = Severity.WARN
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_CONTRACT

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if not response.body:
            return  # No body, no Content-Type required.
        if response.header("Content-Type") is not None:
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message="Response has a body but no Content-Type header.",
            evidence={"body_bytes": len(response.body)},
            remediation=(
                "Provider should set Content-Type for any non-empty response. "
                "Without it, downstream parsers must guess the format."
            ),
        )


class ContentTypeJsonExpected(Check):
    """Probe expected JSON (via ``expect_schema``) but Content-Type is not JSON-shaped."""

    code: ClassVar[str] = "contract.content-type.json-expected"
    severity_default: ClassVar[Severity] = Severity.WARN
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_CONTRACT

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if probe.expected_schema is None:
            return  # Probe doesn't ask for JSON shape.
        ct = response.header("Content-Type")
        if ct is None:
            return  # ContentTypeMissing handles this case.
        # Accept any JSON-shaped media type: application/json,
        # application/vnd.api+json, application/problem+json, etc.
        if "json" in ct.lower():
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message=(
                f"Probe expected a JSON body schema but Content-Type is "
                f"{ct!r}; the schema check will likely fail to parse."
            ),
            evidence={"content_type": ct, "schema": probe.expected_schema.describe()},
            remediation=(
                "Provider should return a JSON-shaped Content-Type "
                "(application/json or +json suffix), or the probe should "
                "drop expect_schema(...) and use a body matcher."
            ),
        )


__all__ = [
    "ContentTypeJsonExpected",
    "ContentTypeMissing",
    "HeaderMissing",
    "Server5xx",
    "StatusUnexpected",
]
