"""Phase 6a of the v0.3 closed-box redesign — preset wiring tests.

Phase 1 shipped :data:`mgf.common.settings.MgfSettings.preset` as a
typed seam. Phase 6a wires the semantics: each preset is a
``{sub_model: {field: value}}`` map applied to fields the caller did
NOT explicitly set. Per the design (CLOSED_BOX_REDESIGN.md §6),
explicit overrides win — preset values fill in unset fields only.

Coverage:

  - Each of the five presets (``explicit``, ``dev``, ``systemd``,
    ``docker``, ``auto``) applies its expected defaults.
  - Explicit field overrides win over preset defaults.
  - Env-supplied fields (``MGF_LOGGING__LEVEL=...``) win over
    preset defaults.
  - The ``"auto"`` preset reads runtime state at construction time
    (TTY / ``JOURNAL_STREAM`` / ``OTEL_EXPORTER_OTLP_ENDPOINT``).
  - ``model_fields_set`` introspection reports preset-applied
    fields as set (so Phase 6b's ``mgf-common explain`` can show
    provenance).
  - The validator is idempotent (re-validating an instance is a
    no-op).
  - Bootstrap default (no ``settings=`` arg) lands on
    ``preset="auto"`` per FEEDBACK §14.15.
"""

from __future__ import annotations

import os

import pytest

from mgf.common.settings import (
    LoggingSettings,
    MgfSettings,
    OtelSettings,
    _resolve_preset_overrides,
)

# ---------------------------------------------------------------------------
# Per-preset defaults
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
    """Strip every env var that could perturb preset evaluation.

    The ``"auto"`` preset reads ``JOURNAL_STREAM`` and
    ``OTEL_EXPORTER_OTLP_ENDPOINT``; pydantic-settings reads
    ``MGF_*``. Wipe them all so per-test assertions are
    deterministic regardless of the host environment.
    """
    for key in (
        "JOURNAL_STREAM",
        "OTEL_EXPORTER_OTLP_ENDPOINT",
    ):
        monkeypatch.delenv(key, raising=False)
    for key in list(os.environ):
        if key.startswith("MGF_"):
            monkeypatch.delenv(key, raising=False)


class TestExplicitPreset:
    """``explicit`` is a no-op — every field stays at its declared default."""

    def test_default_preset_is_explicit(self) -> None:
        assert MgfSettings().preset == "explicit"

    def test_explicit_does_not_change_logging_defaults(self) -> None:
        s = MgfSettings()
        assert s.logging.level == "INFO"
        assert s.logging.format == "auto"
        assert s.logging.journald is False

    def test_explicit_does_not_change_otel_defaults(self) -> None:
        s = MgfSettings()
        assert s.otel.enabled is False


class TestDevPreset:
    """``dev`` — text format, DEBUG level, no remote sinks."""

    def test_dev_logging_level_is_debug(self) -> None:
        s = MgfSettings(preset="dev")
        assert s.logging.level == "DEBUG"

    def test_dev_logging_format_is_text(self) -> None:
        s = MgfSettings(preset="dev")
        assert s.logging.format == "text"

    def test_dev_journald_is_off(self) -> None:
        s = MgfSettings(preset="dev")
        assert s.logging.journald is False

    def test_dev_otel_disabled(self) -> None:
        s = MgfSettings(preset="dev")
        assert s.otel.enabled is False


class TestSystemdPreset:
    """``systemd`` — journald + JSON, no console-formatted output."""

    def test_systemd_format_is_json(self) -> None:
        s = MgfSettings(preset="systemd")
        assert s.logging.format == "json"

    def test_systemd_journald_is_on(self) -> None:
        s = MgfSettings(preset="systemd")
        assert s.logging.journald is True

    def test_systemd_does_not_force_level(self) -> None:
        # The systemd preset deliberately leaves level at the declared
        # default so consumers can pick INFO vs DEBUG independently.
        s = MgfSettings(preset="systemd")
        assert s.logging.level == "INFO"


class TestDockerPreset:
    """``docker`` — JSON to stdout; no file sink (container captures stdout)."""

    def test_docker_format_is_json(self) -> None:
        s = MgfSettings(preset="docker")
        assert s.logging.format == "json"

    def test_docker_journald_off(self) -> None:
        s = MgfSettings(preset="docker")
        assert s.logging.journald is False

    def test_docker_file_remains_none(self) -> None:
        # Container runtime captures stdout; rotating-file sink off
        # by default is fine.
        s = MgfSettings(preset="docker")
        assert s.logging.file is None


class TestAutoPreset:
    """``auto`` — legacy v0.1 detection. Runtime state, NOT module-load state."""

    def test_auto_no_tty_no_env_yields_json_no_journald_no_otel(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        # Force is-a-TTY to False so we can pin the format value.
        monkeypatch.setattr("sys.stdout.isatty", lambda: False)
        s = MgfSettings(preset="auto")
        assert s.logging.format == "json"
        assert s.logging.journald is False
        assert s.otel.enabled is False

    def test_auto_with_tty_yields_text_format(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setattr("sys.stdout.isatty", lambda: True)
        s = MgfSettings(preset="auto")
        assert s.logging.format == "text"

    def test_auto_with_journal_stream_attaches_journald(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setattr("sys.stdout.isatty", lambda: False)
        monkeypatch.setenv("JOURNAL_STREAM", "8:12345")
        s = MgfSettings(preset="auto")
        assert s.logging.journald is True

    def test_auto_with_otel_endpoint_enables_otel(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setattr("sys.stdout.isatty", lambda: False)
        monkeypatch.setenv(
            "OTEL_EXPORTER_OTLP_ENDPOINT",
            "https://otel.example.com",
        )
        s = MgfSettings(preset="auto")
        assert s.otel.enabled is True

    def test_auto_evaluation_is_per_construction_not_module_load(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Regression guard against caching at module-import time."""
        monkeypatch.setattr("sys.stdout.isatty", lambda: False)

        s1 = MgfSettings(preset="auto")
        assert s1.otel.enabled is False

        monkeypatch.setenv(
            "OTEL_EXPORTER_OTLP_ENDPOINT",
            "https://otel.example.com",
        )
        s2 = MgfSettings(preset="auto")
        assert s2.otel.enabled is True


# ---------------------------------------------------------------------------
# Override precedence — explicit + env + file each beat the preset
# ---------------------------------------------------------------------------


class TestExplicitOverridesWin:
    """Per §6: explicit overrides win over preset defaults."""

    def test_explicit_kwarg_overrides_systemd_format(self) -> None:
        s = MgfSettings(
            preset="systemd",
            logging=LoggingSettings(format="text"),
        )
        assert s.logging.format == "text"
        # Other systemd defaults still applied.
        assert s.logging.journald is True

    def test_explicit_otel_kwarg_overrides_dev_disabled(self) -> None:
        s = MgfSettings(
            preset="dev",
            otel=OtelSettings(enabled=True),
        )
        assert s.otel.enabled is True
        # dev preset's logging defaults still applied.
        assert s.logging.format == "text"

    def test_partial_logging_kwarg_keeps_preset_for_unset_fields(self) -> None:
        # User sets level only; preset fills in format + journald.
        s = MgfSettings(
            preset="systemd",
            logging=LoggingSettings(level="DEBUG"),
        )
        assert s.logging.level == "DEBUG"  # explicit
        assert s.logging.format == "json"  # preset
        assert s.logging.journald is True  # preset


class TestEnvOverridesWin:
    """Env-supplied fields beat preset defaults (env entries land in
    ``model_fields_set`` BEFORE the validator runs)."""

    def test_env_logging_level_beats_dev_preset(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setenv("MGF_LOGGING__LEVEL", "WARNING")
        s = MgfSettings(preset="dev")
        # Env wins over dev's "DEBUG".
        assert s.logging.level == "WARNING"
        # Other dev preset defaults still applied.
        assert s.logging.format == "text"

    def test_env_format_beats_systemd_preset(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setenv("MGF_LOGGING__FORMAT", "text")
        s = MgfSettings(preset="systemd")
        assert s.logging.format == "text"
        # systemd preset's other defaults still applied.
        assert s.logging.journald is True


# ---------------------------------------------------------------------------
# Provenance — preset-applied fields appear as "set"
# ---------------------------------------------------------------------------


class TestProvenance:
    """Phase 6b's ``mgf-common explain`` needs to see preset-applied
    fields as ``set`` so it can attribute the value to the preset."""

    def test_systemd_marks_format_and_journald_as_set(self) -> None:
        s = MgfSettings(preset="systemd")
        assert "format" in s.logging.model_fields_set
        assert "journald" in s.logging.model_fields_set

    def test_explicit_does_not_mutate_fields_set(self) -> None:
        s = MgfSettings()
        assert s.logging.model_fields_set == set()


# ---------------------------------------------------------------------------
# Idempotency
# ---------------------------------------------------------------------------


class TestIdempotency:
    """Re-validating an instance does not double-apply or change values."""

    def test_revalidate_is_noop(self) -> None:
        s1 = MgfSettings(preset="systemd")
        before = s1.model_dump()

        # model_validate on the dump round-trips through the validator
        # again (every field shows up as explicitly set on the way in,
        # so the preset has nothing to add).
        s2 = MgfSettings.model_validate(before)
        assert s2.logging.format == "json"
        assert s2.logging.journald is True
        assert s2.logging.level == "INFO"


# ---------------------------------------------------------------------------
# _resolve_preset_overrides — the lookup helper
# ---------------------------------------------------------------------------


class TestResolvePresetOverrides:
    """Direct unit tests for the helper that the validator uses."""

    def test_explicit_returns_empty_dict(self) -> None:
        assert _resolve_preset_overrides("explicit") == {}

    def test_dev_returns_logging_and_otel(self) -> None:
        out = _resolve_preset_overrides("dev")
        assert "logging" in out
        assert out["logging"]["level"] == "DEBUG"
        assert out["otel"]["enabled"] is False

    def test_systemd_returns_logging_only(self) -> None:
        out = _resolve_preset_overrides("systemd")
        assert "logging" in out
        assert out["logging"]["format"] == "json"
        assert out["logging"]["journald"] is True
        # systemd intentionally leaves OTel + crash + redaction alone.
        assert "otel" not in out

    def test_auto_returns_runtime_detected_values(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setattr("sys.stdout.isatty", lambda: True)
        monkeypatch.setenv("JOURNAL_STREAM", "8:42")
        out = _resolve_preset_overrides("auto")
        assert out["logging"]["format"] == "text"
        assert out["logging"]["journald"] is True


# ---------------------------------------------------------------------------
# Bootstrap integration — default preset is "auto" per FEEDBACK §14.15
# ---------------------------------------------------------------------------


class TestBootstrapDefaultPreset:
    """Bootstrap without a ``settings=`` arg should construct an
    auto-preset MgfSettings to preserve v0.1 behaviour."""

    def test_bootstrap_default_settings_is_auto_preset(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        # Force a known TTY shape so the assertion is stable.
        monkeypatch.setattr("sys.stdout.isatty", lambda: False)

        from mgf.common import bootstrap, current_context

        ctx = bootstrap(app_name="presettest", app_version="0.0.0")
        try:
            assert ctx.settings.preset == "auto"
            # auto with no TTY → format should resolve to "json"
            assert ctx.settings.logging.format == "json"
            # current_context() unwraps to the AppContext; the
            # bootstrap return is a dual-mode wrapper. Compare by
            # identity of the settings, which is unique per call.
            active = current_context()
            assert active is not None
            assert active.settings is ctx.settings
        finally:
            ctx.shutdown()
