"""Tests for ``mgf.apiprobe.cli``.

Uses respx to mock httpx for the ``run`` subcommand — no live network.
"""

from __future__ import annotations

import json

import httpx
import pytest
import respx

from mgf.apiprobe.cli.__main__ import build_parser, main
from mgf.apiprobe.cli.checks import handle_checks
from mgf.apiprobe.cli.doctor import handle_doctor

# ---------------------------------------------------------------------------
# Argparse parser
# ---------------------------------------------------------------------------


class TestParser:
    def test_subcommand_required(self) -> None:
        parser = build_parser()
        with pytest.raises(SystemExit):
            parser.parse_args([])

    def test_run_default_format(self) -> None:
        parser = build_parser()
        args = parser.parse_args(["run"])
        assert args.subcommand == "run"
        assert args.format == "human"
        assert args.suite is None

    def test_run_with_format_json(self) -> None:
        parser = build_parser()
        args = parser.parse_args(["run", "--format", "json"])
        assert args.format == "json"

    def test_run_invalid_format_rejected(self) -> None:
        parser = build_parser()
        with pytest.raises(SystemExit):
            parser.parse_args(["run", "--format", "xml"])

    def test_checks_subcommand(self) -> None:
        args = build_parser().parse_args(["checks"])
        assert args.subcommand == "checks"

    def test_doctor_subcommand(self) -> None:
        args = build_parser().parse_args(["doctor"])
        assert args.subcommand == "doctor"


# ---------------------------------------------------------------------------
# checks subcommand
# ---------------------------------------------------------------------------


class TestChecksCommand:
    def test_returns_zero(self, capsys: pytest.CaptureFixture[str]) -> None:
        rc = handle_checks()
        assert rc == 0

    def test_lists_all_18_codes(self, capsys: pytest.CaptureFixture[str]) -> None:
        handle_checks()
        out = capsys.readouterr().out
        # Spot-check codes from each category.
        assert "contract.status.unexpected" in out
        assert "schema.body.invalid-json" in out
        assert "quality.latency.over-budget" in out
        assert "security.body.echoes-secret" in out
        assert "observability.headers.missing-request-id" in out
        assert "22 checks total" in out

    def test_severity_in_uppercase(self, capsys: pytest.CaptureFixture[str]) -> None:
        handle_checks()
        out = capsys.readouterr().out
        assert "CRITICAL" in out
        assert "WARN" in out
        assert "INFO" in out


# ---------------------------------------------------------------------------
# doctor subcommand
# ---------------------------------------------------------------------------


class TestDoctorCommand:
    def test_returns_zero_on_clean_install(self, capsys: pytest.CaptureFixture[str]) -> None:
        rc = handle_doctor()
        assert rc == 0
        out = capsys.readouterr().out
        assert "DOCTOR: ok." in out

    def test_includes_version(self, capsys: pytest.CaptureFixture[str]) -> None:
        from mgf.apiprobe import __version__

        handle_doctor()
        out = capsys.readouterr().out
        # Read the live __version__ rather than hardcoding so this
        # test doesn't drift on every release.
        assert __version__ in out

    def test_includes_check_count(self, capsys: pytest.CaptureFixture[str]) -> None:
        handle_doctor()
        out = capsys.readouterr().out
        assert "default checks    22" in out


# ---------------------------------------------------------------------------
# run subcommand
# ---------------------------------------------------------------------------


class TestRunCommand:
    @respx.mock
    def test_run_with_default_suite_succeeds_against_mocked_httpbin(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # Mock all httpbin URLs the default suite hits.
        respx.get("https://httpbin.org/status/200").mock(
            return_value=httpx.Response(
                200,
                headers={
                    "Content-Type": "application/json",
                    "Strict-Transport-Security": "max-age=31536000",
                    "X-Content-Type-Options": "nosniff",
                    "X-Request-Id": "test-001",
                    "Server": "test",
                },
            )
        )
        respx.get("https://httpbin.org/status/404").mock(return_value=httpx.Response(404))
        respx.get("https://httpbin.org/headers").mock(
            return_value=httpx.Response(200, headers={"Content-Type": "application/json"})
        )
        respx.get("https://httpbin.org/json").mock(
            return_value=httpx.Response(200, headers={"Content-Type": "application/json"})
        )
        respx.post("https://httpbin.org/anything").mock(
            return_value=httpx.Response(200, headers={"Content-Type": "application/json"})
        )

        # Direct invocation of the subcommand handler (skip argparse).
        from mgf.apiprobe.cli.run import handle_run

        rc = handle_run(suite_path=None, format="human")
        # 0 OR 1 depending on the mocked findings; the smoke is that
        # it runs end-to-end and produces output.
        assert rc in (0, 1)
        out = capsys.readouterr().out
        assert "probe(s)" in out

    @respx.mock
    def test_run_json_format_outputs_valid_json(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # Mock minimal: just the ones the suite needs to not transport-error.
        respx.get("https://httpbin.org/status/200").mock(return_value=httpx.Response(200))
        respx.get("https://httpbin.org/status/404").mock(return_value=httpx.Response(404))
        respx.get("https://httpbin.org/headers").mock(return_value=httpx.Response(200))
        respx.get("https://httpbin.org/json").mock(return_value=httpx.Response(200))
        respx.post("https://httpbin.org/anything").mock(return_value=httpx.Response(200))

        from mgf.apiprobe.cli.run import handle_run

        handle_run(suite_path=None, format="json")
        out = capsys.readouterr().out
        # Output must be valid JSON.
        parsed = json.loads(out)
        assert "probe_count" in parsed
        assert parsed["probe_count"] == 5

    def test_run_with_invalid_module_raises_config_error(self) -> None:
        from mgf.apiprobe.cli.run import _load_suite
        from mgf.apiprobe.exceptions import ApiprobeConfigError

        with pytest.raises(ApiprobeConfigError, match="cannot import"):
            _load_suite("nonexistent.module.path")

    def test_run_with_missing_attr_raises_config_error(self) -> None:
        from mgf.apiprobe.cli.run import _load_suite
        from mgf.apiprobe.exceptions import ApiprobeConfigError

        with pytest.raises(ApiprobeConfigError, match="has no attribute"):
            # Use this test module — it doesn't have a 'suite' attribute.
            _load_suite("tests.unit.cli.test_cli")

    def test_run_with_non_suite_attr_raises_config_error(self, monkeypatch) -> None:
        import sys
        import types

        from mgf.apiprobe.cli.run import _load_suite
        from mgf.apiprobe.exceptions import ApiprobeConfigError

        fake = types.ModuleType("_apiprobe_test_fake")
        fake.suite = "not a suite"  # type: ignore[attr-defined]
        sys.modules["_apiprobe_test_fake"] = fake
        try:
            with pytest.raises(ApiprobeConfigError, match="not a Suite"):
                _load_suite("_apiprobe_test_fake")
        finally:
            del sys.modules["_apiprobe_test_fake"]


# ---------------------------------------------------------------------------
# main entry point
# ---------------------------------------------------------------------------


class TestMainEntryPoint:
    def test_main_calls_dispatch(self, capsys: pytest.CaptureFixture[str]) -> None:
        rc = main(["doctor"])
        assert rc == 0
        out = capsys.readouterr().out
        assert "DOCTOR: ok." in out

    def test_main_with_no_argv_uses_sys_argv(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        monkeypatch.setattr("sys.argv", ["apiprobe", "doctor"])
        rc = main()
        assert rc == 0
