"""Tests for ``mgf.apiprobe.core.check``."""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

import pytest

from mgf.apiprobe.core.check import Check
from mgf.apiprobe.core.context import Context
from mgf.apiprobe.core.finding import RootCause, Severity
from mgf.apiprobe.core.probe import Probe
from mgf.apiprobe.core.response import Response

if TYPE_CHECKING:
    from collections.abc import Iterable

    from mgf.apiprobe.core.finding import Finding


class _StatusCheck(Check):
    """Concrete check used to exercise the Check base class."""

    code: ClassVar[str] = "test.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 probe.expected_statuses and response.status not in probe.expected_statuses:
            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="Check provider service health.",
            )


def _resp(status: int = 200) -> Response:
    return Response(
        url="https://x.test/y",
        status=status,
        headers={"Content-Type": "application/json"},
        body=b"{}",
        elapsed_ms=10.0,
    )


class TestCheckABC:
    def test_cannot_instantiate_directly(self) -> None:
        with pytest.raises(TypeError):
            Check()  # type: ignore[abstract]


class TestStatusCheck:
    def test_emits_no_finding_on_match(self) -> None:
        probe = Probe.get("https://x.test/y").id("p1").expect_status(200)
        findings = list(_StatusCheck().run(probe, _resp(200), Context.empty()))
        assert findings == []

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

    def test_emits_no_finding_when_no_expectations(self) -> None:
        # No expected_statuses → check is silent.
        probe = Probe.get("https://x.test/y")
        findings = list(_StatusCheck().run(probe, _resp(500), Context.empty()))
        assert findings == []


class TestFindingHelper:
    def test_severity_override(self) -> None:
        check = _StatusCheck()
        f = check.finding(probe_id="p1", message="x.", severity=Severity.WARN)
        assert f.severity is Severity.WARN

    def test_root_cause_override(self) -> None:
        check = _StatusCheck()
        f = check.finding(
            probe_id="p1",
            message="x.",
            root_cause=RootCause.NETWORK,
        )
        assert f.root_cause is RootCause.NETWORK

    def test_defaults_used_when_unset(self) -> None:
        check = _StatusCheck()
        f = check.finding(probe_id="p1", message="x.")
        assert f.severity is Severity.CRITICAL
        assert f.root_cause is RootCause.PROVIDER_CONTRACT

    def test_threat_refs_and_tags(self) -> None:
        check = _StatusCheck()
        f = check.finding(
            probe_id="p1",
            message="x.",
            threat_refs=("OWASP-API-9",),
            tags=("read",),
        )
        assert f.threat_refs == ("OWASP-API-9",)
        assert f.tags == ("read",)

    def test_evidence_default_empty_dict(self) -> None:
        check = _StatusCheck()
        f = check.finding(probe_id="p1", message="x.")
        assert f.evidence == {}
