"""Phase 6b of the v0.3 closed-box redesign — diagnostic CLI tests.

Drives ``mgf-common explain / validate / doctor`` through the
in-process ``main(argv)`` entry so the CLI is exercised without
spawning a subprocess. Stdout / stderr are captured and asserted
against — the CLI's contract is "what the operator sees", so the
tests are textual-shape pins rather than internal-API asserts.

Coverage:

  - ``main([])`` (no subcommand) returns ``EXIT_UNKNOWN`` and
    prints the help text.
  - ``main(["explain"])`` renders every section header + every
    field + the env-vars footer.
  - Provenance attribution is correct for: declared default,
    preset-applied, env-supplied, init-supplied (via
    ``--preset``).
  - Secret-shaped fields (``sentry_dsn``, ``http_url``,
    ``http_sink``, ``syslog_address``) are redacted in the
    explain output AND in the env-var footer.
  - ``main(["validate", <good.yaml>])`` returns ``EXIT_SUCCESS``
    and emits the ``✓`` lines.
  - ``main(["validate", <bad.yaml>])`` returns
    ``EXIT_CONFIG_ERROR`` and lists per-field errors on stderr.
  - ``main(["validate", <missing>])`` returns
    ``EXIT_OPERATION_ERROR``.
  - ``main(["app-doctor"])`` runs every check + emits the
    "Recommended actions" footer.
"""

from __future__ import annotations

import os
import textwrap
from typing import TYPE_CHECKING

import pytest
from mgf.common.cli import build_cli
from mgf.common.cli._exit_codes import (
    EXIT_CONFIG_ERROR,
    EXIT_OPERATION_ERROR,
    EXIT_SUCCESS,
    EXIT_UNKNOWN,
)

from mgf.fed.__main__ import main

if TYPE_CHECKING:
    from pathlib import Path


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
    """Strip MGF_* + auto-detect env vars so per-test assertions are stable."""
    for k in (
        "JOURNAL_STREAM",
        "OTEL_EXPORTER_OTLP_ENDPOINT",
    ):
        monkeypatch.delenv(k, raising=False)
    for k in list(os.environ):
        if k.startswith("MGF_"):
            monkeypatch.delenv(k, raising=False)


# ---------------------------------------------------------------------------
# Top-level dispatch
# ---------------------------------------------------------------------------


class TestDispatch:
    """``main(argv)`` + the argparse parser shape."""

    def test_no_subcommand_returns_unknown_and_prints_help(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # The no-subcommand → EXIT_UNKNOWN + help-on-stderr fallback is a
        # property of the registry CLI (build_cli has required=False). The
        # mgf-fed __main__ parser uses required=True (argparse SystemExit(2)),
        # so this one case drives the vestigial _mgfcli.main which preserves
        # the documented fallback.
        from mgf.fed.cli._mgfcli import main as mgfcli_main

        rc = mgfcli_main([])
        captured = capsys.readouterr()
        assert rc == EXIT_UNKNOWN
        # Help text goes to stderr per the build_cli default.
        assert "subcommands" in captured.err
        assert "explain" in captured.err
        assert "validate" in captured.err
        # Registered as "app-doctor" after the federation reorg.
        assert "app-doctor" in captured.err

    def test_help_flag_exits_zero(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # argparse handles --help by raising SystemExit(0)
        with pytest.raises(SystemExit) as excinfo:
            main(["--help"])
        assert excinfo.value.code == 0

    def test_build_cli_lists_phase6b_subcommands(self) -> None:
        import argparse

        import mgf.fed.cli  # noqa: F401  (registers the moved subcommands)

        parser = build_cli()
        # The subparsers action carries the choices dict.
        subparsers_action = next(
            a for a in parser._actions if isinstance(a, argparse._SubParsersAction)
        )
        # Phase 6b wired explain / validate / doctor (registered as
        # "app-doctor" after the federation reorg); Phase 7 added
        # migrate. Assert the Phase 6b set is present (subset check
        # so adding a future subcommand doesn't break this pin).
        assert {"explain", "validate", "app-doctor"} <= set(
            subparsers_action.choices.keys(),
        )


# ---------------------------------------------------------------------------
# explain — provenance attribution + redaction
# ---------------------------------------------------------------------------


class TestExplain:
    """``mgf-common explain``."""

    def test_renders_all_sections(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        rc = main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        assert rc == EXIT_SUCCESS
        for section in ("[logging]", "[otel]", "[crash]", "[redaction]", "[vault]", "[meta]"):
            assert section in captured.out

    def test_header_includes_app_and_version(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["explain", "--app", "myapp", "--version", "2.5"])
        captured = capsys.readouterr()
        assert "myapp v2.5" in captured.out

    def test_default_attribution(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        # Out of the box: every field is (default).
        assert "level" in captured.out
        assert "(default)" in captured.out

    def test_env_attribution(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        monkeypatch.setenv("MGF_LOGGING__LEVEL", "WARNING")
        main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        # The level field's annotation is now (env: ...).
        assert "(env: MGF_LOGGING__LEVEL=WARNING)" in captured.out

    def test_preset_attribution_via_cli_flag(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(
            [
                "explain",
                "--preset",
                "systemd",
                "--app",
                "demo",
                "--version",
                "1.0",
            ]
        )
        captured = capsys.readouterr()
        # journald = true with preset attribution.
        assert "journald" in captured.out
        assert "(preset: systemd)" in captured.out
        # The preset itself in [meta] is (init) since we passed --preset.
        assert "(init)" in captured.out

    def test_env_beats_preset_in_attribution(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # systemd preset would set logging.format=json; env says text.
        monkeypatch.setenv("MGF_LOGGING__FORMAT", "text")
        main(
            [
                "explain",
                "--preset",
                "systemd",
                "--app",
                "demo",
                "--version",
                "1.0",
            ]
        )
        captured = capsys.readouterr()
        # The format line should be env-attributed, not preset.
        # Find the format line specifically.
        format_lines = [
            line for line in captured.out.splitlines() if line.lstrip().startswith("format")
        ]
        assert any("(env: MGF_LOGGING__FORMAT=text)" in line for line in format_lines)
        assert not any("(preset: systemd)" in line and "format" in line for line in format_lines)

    def test_secret_field_value_redacted_in_dump(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # Set sentry_dsn via env so it lands in the dump.
        monkeypatch.setenv(
            "MGF_CRASH__SENTRY_DSN",
            "https://abc123@sentry.io/4505",
        )
        main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        # The actual DSN value MUST NOT appear in the output.
        assert "abc123" not in captured.out
        assert "***REDACTED***" in captured.out

    def test_secret_env_var_value_redacted_in_footer(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        monkeypatch.setenv(
            "MGF_CRASH__SENTRY_DSN",
            "https://abc123@sentry.io/4505",
        )
        main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        # The footer shows the env var name with redacted value.
        # Look for the footer line specifically.
        footer_lines = [
            line
            for line in captured.out.splitlines()
            if "MGF_CRASH__SENTRY_DSN" in line and "(set:" in line
        ]
        assert footer_lines, "expected the footer line for SENTRY_DSN"
        assert "abc123" not in footer_lines[0]
        assert "***REDACTED***" in footer_lines[0]

    def test_env_vars_footer_lists_every_known_var(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["explain", "--app", "demo", "--version", "1.0"])
        captured = capsys.readouterr()
        # Spot-check a handful of known names.
        for name in (
            "MGF_LOGGING__LEVEL",
            "MGF_OTEL__ENABLED",
            "MGF_CRASH__SINKS",
            "MGF_VAULT__BACKEND",
            "MGF_PRESET",
        ):
            assert name in captured.out


# ---------------------------------------------------------------------------
# validate — schema check + provenance with file source
# ---------------------------------------------------------------------------


class TestValidate:
    """``mgf-common validate <config.yaml>``."""

    def test_valid_config_returns_success_and_dumps_resolved(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        config = tmp_path / "good.yaml"
        config.write_text(
            textwrap.dedent("""
            preset: systemd
            logging:
              level: WARNING
            otel:
              enabled: true
              sample_rate: 0.1
        """)
        )
        rc = main(["validate", str(config)])
        captured = capsys.readouterr()
        assert rc == EXIT_SUCCESS
        # Pass markers.
        assert "✓ Schema valid" in captured.out
        assert "✓ All field constraints pass" in captured.out
        assert "✓ No unknown keys" in captured.out
        # File-supplied fields show (file) provenance.
        assert "(file)" in captured.out
        # Preset-supplied fields show (preset: systemd).
        assert "(preset: systemd)" in captured.out

    def test_unknown_key_returns_config_error(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        config = tmp_path / "bad.yaml"
        config.write_text(
            textwrap.dedent("""
            logging:
              level: INFO
              unknown_field: oops
        """)
        )
        rc = main(["validate", str(config)])
        captured = capsys.readouterr()
        assert rc == EXIT_CONFIG_ERROR
        assert "✗ Schema validation FAILED" in captured.err
        assert "logging.unknown_field" in captured.err
        assert "Extra inputs are not permitted" in captured.err

    def test_invalid_enum_value_returns_config_error(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        config = tmp_path / "bad.yaml"
        config.write_text(
            textwrap.dedent("""
            logging:
              level: BANANA
        """)
        )
        rc = main(["validate", str(config)])
        captured = capsys.readouterr()
        assert rc == EXIT_CONFIG_ERROR
        assert "logging.level" in captured.err

    def test_missing_file_returns_operation_error(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        rc = main(["validate", "/nonexistent.yaml"])
        captured = capsys.readouterr()
        assert rc == EXIT_OPERATION_ERROR
        assert "✗ failed to read" in captured.err

    def test_yaml_parse_error_returns_operation_error(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        config = tmp_path / "broken.yaml"
        config.write_text("preset: systemd\n  bad: indent")
        rc = main(["validate", str(config)])
        captured = capsys.readouterr()
        assert rc == EXIT_OPERATION_ERROR
        assert "YAML parse error" in captured.err

    def test_top_level_must_be_mapping(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # A YAML list at the top level is not a mapping → CONFIG error.
        config = tmp_path / "list.yaml"
        config.write_text("- one\n- two\n")
        rc = main(["validate", str(config)])
        captured = capsys.readouterr()
        assert rc == EXIT_CONFIG_ERROR
        assert "MUST be a mapping" in captured.err

    def test_empty_yaml_is_valid(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # Empty file → loaded is None → coerced to empty dict → valid
        # (every field uses its declared default).
        config = tmp_path / "empty.yaml"
        config.write_text("")
        rc = main(["validate", str(config)])
        assert rc == EXIT_SUCCESS


# ---------------------------------------------------------------------------
# doctor — install-state + wiring checks
# ---------------------------------------------------------------------------


class TestDoctor:
    """``mgf-common doctor``."""

    def test_runs_and_emits_recommended_actions_footer(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        rc = main(["app-doctor"])
        captured = capsys.readouterr()
        # In this dev environment everything passes → exit 0.
        assert rc == EXIT_SUCCESS
        assert "Recommended actions:" in captured.out

    def test_install_check_present(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "mgf-common v" in captured.out
        assert "installed" in captured.out

    def test_py_typed_check_present(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "py.typed marker" in captured.out

    def test_extras_check_lists_observability_and_vault(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "[observability]" in captured.out
        assert "[vault]" in captured.out

    def test_paper_58_vault_missing_is_info_not_warn(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # PAPER-58 (v0.41.6+): when the [vault] extra is missing, the
        # marker MUST be ℹ (informational) not ⚠. Vault is genuinely
        # optional — consumers like vm-vmanager-app and iosRecovery ship
        # their own credential layer and never touch
        # mgf.common.vault. The ⚠ was misleading noise; the ℹ
        # communicates "you can install this if you want" without
        # implying anything is wrong.
        from mgf.fed.cli import _doctor

        # Force vault's modules to appear missing.
        def _fake_installed(name: str) -> bool:
            return name not in ("keyring", "cryptography")

        monkeypatch.setattr(_doctor, "_module_installed", _fake_installed)
        main(["app-doctor"])
        captured = capsys.readouterr()
        # Find the vault-missing line.
        vault_lines = [
            ln for ln in captured.out.splitlines() if "[vault] extra not installed" in ln
        ]
        assert len(vault_lines) == 1, f"expected one [vault] line; got: {vault_lines}"
        # PAPER-58 contract: ℹ marker, not ⚠.
        assert vault_lines[0].startswith("ℹ "), (
            f"PAPER-58 regression: vault-missing line should start with ℹ; got: {vault_lines[0]!r}"
        )

    def test_paper_58_observability_missing_stays_warn(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        # PAPER-58 (v0.41.6+): observability is NOT downgraded.
        # It's the federation default; a missing observability
        # extra is still a ⚠ so the operator notices.
        from mgf.fed.cli import _doctor

        def _fake_installed(name: str) -> bool:
            return name not in ("opentelemetry", "sentry_sdk")

        monkeypatch.setattr(_doctor, "_module_installed", _fake_installed)
        main(["app-doctor"])
        captured = capsys.readouterr()
        obs_lines = [
            ln for ln in captured.out.splitlines() if "[observability] extra not installed" in ln
        ]
        assert len(obs_lines) == 1
        assert obs_lines[0].startswith("⚠ ")

    def test_state_dir_check_present(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "Default crash dir" in captured.out
        assert "Default log dir" in captured.out

    def test_journal_stream_warning_when_set(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        monkeypatch.setenv("JOURNAL_STREAM", "8:42")
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "JOURNAL_STREAM" in captured.out
        assert "⚠" in captured.out

    def test_otel_endpoint_warning_when_set(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        monkeypatch.setenv(
            "OTEL_EXPORTER_OTLP_ENDPOINT",
            "https://otel.example.com",
        )
        main(["app-doctor"])
        captured = capsys.readouterr()
        assert "OTEL_EXPORTER_OTLP_ENDPOINT" in captured.out

    def test_registry_checks_list_known_extensions(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        main(["app-doctor"])
        captured = capsys.readouterr()
        # Built-in crash sinks are pre-registered.
        assert "Registered crash sinks:" in captured.out
        assert "sentry" in captured.out
        assert "otlp" in captured.out
        # Built-in vault backends are pre-registered.
        assert "Registered vault backends:" in captured.out
        assert "env" in captured.out
        assert "keyring" in captured.out
        assert "file" in captured.out
        # Built-in redaction pattern group "builtin" is pre-registered.
        assert "Registered redaction pattern groups:" in captured.out
        assert "builtin" in captured.out


# ---------------------------------------------------------------------------
# Provenance shim — direct unit tests
# ---------------------------------------------------------------------------


class TestProvenanceDirect:
    """Targeted tests for the field_provenance helper."""

    def test_default_attribution_when_no_overrides(self) -> None:
        from mgf.common.cli._provenance import field_provenance
        from mgf.common.settings import MgfSettings

        prov = field_provenance(MgfSettings())
        assert prov["logging"]["level"].kind == "default"
        assert prov["otel"]["enabled"].kind == "default"

    def test_env_detected(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        from mgf.common.cli._provenance import field_provenance
        from mgf.common.settings import MgfSettings

        monkeypatch.setenv("MGF_LOGGING__LEVEL", "DEBUG")
        prov = field_provenance(MgfSettings())
        assert prov["logging"]["level"].kind == "env"
        assert "MGF_LOGGING__LEVEL=DEBUG" in prov["logging"]["level"].detail

    def test_preset_detected(self) -> None:
        from mgf.common.cli._provenance import field_provenance
        from mgf.common.settings import MgfSettings

        prov = field_provenance(MgfSettings(preset="systemd"))
        assert prov["logging"]["journald"].kind == "preset"
        assert prov["logging"]["journald"].detail == "systemd"

    def test_init_overrides_attributed_as_init(self) -> None:
        from mgf.common.cli._provenance import field_provenance
        from mgf.common.settings import MgfSettings

        prov = field_provenance(
            MgfSettings(preset="dev"),
            init_overrides={"preset": "dev"},
        )
        assert prov["_root"]["preset"].kind == "init"

    def test_file_overrides_attributed_as_file(self) -> None:
        from mgf.common.cli._provenance import field_provenance
        from mgf.common.settings import MgfSettings

        prov = field_provenance(
            MgfSettings(logging={"level": "WARNING"}),  # type: ignore[arg-type]
            file_overrides={"logging": {"level": "WARNING"}},
        )
        assert prov["logging"]["level"].kind == "file"

    def test_env_vars_known_to_mgf_returns_sorted(self) -> None:
        from mgf.common.cli._provenance import env_vars_known_to_mgf

        out = env_vars_known_to_mgf()
        names = [name for name, _ in out]
        assert names == sorted(names)
        # MGF_PRESET should be in there.
        assert "MGF_PRESET" in names
