"""Phase 1 of the v0.3 closed-box redesign — lifecycle tests.

Covers the new ``bootstrap()`` + ``AppContext`` + ``current_context()``
surface plus the round-2 additions:

  - §14.2 Bootstrap atomicity — transactional with rollback.
  - §14.3 Bootstrap-time logging — deferred replay buffer.
  - §14.10 Bootstrap idempotency — replace + AUDIT log.

The existing v0.1 APIs (configure / setup_logging / setup_otel /
install_excepthook / install_threading_excepthook) are NOT tested
here — their existing tests in ``tests/unit/test_identity.py``
and ``tests/unit/observability/`` continue to pass unchanged.

Each test isolates side-effects via tempfile + monkeypatch +
explicit ``shutdown()`` so no test leaves the process in a wired
state.
"""

from __future__ import annotations

import contextlib
import logging
import sys
from typing import TYPE_CHECKING, Any

import pytest
from pydantic import ValidationError

import mgf.common
from mgf.common import _identity, _lifecycle
from mgf.common.exceptions import BootstrapError
from mgf.common.settings import (
    CrashSettings,
    LoggingSettings,
    MgfSettings,
    OtelSettings,
    RedactionSettings,
)

if TYPE_CHECKING:
    from pathlib import Path


# ---------------------------------------------------------------------------
# Fixtures — process-global state isolation
# ---------------------------------------------------------------------------


@pytest.fixture
def _isolated_state(monkeypatch: Any, tmp_path: Path) -> Path:
    """Redirect XDG state to tmp_path; restore identity + global ctx."""
    monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("JOURNAL_STREAM", raising=False)
    monkeypatch.delenv("INVOCATION_ID", raising=False)
    # Pristine identity so app_name() returns the placeholder.
    monkeypatch.setattr(_identity, "_APP_NAME", "app")
    monkeypatch.setattr(_identity, "_APP_VERSION", "unknown")
    monkeypatch.setattr(_identity, "_CONFIGURED", False)
    monkeypatch.setattr(_identity, "_UNCONFIGURED_WARNED", False)
    # Pristine global context so each test sees no prior bootstrap.
    monkeypatch.setattr(_lifecycle, "_GLOBAL_CONTEXT", None)
    return tmp_path


@pytest.fixture
def _isolated_logging() -> Any:
    """Save + restore root-logger handlers around each test."""
    yield
    # Close + remove every handler to satisfy filterwarnings = error
    # (RotatingFileHandler GC fires ResourceWarning otherwise).
    for h in list(logging.root.handlers):
        with contextlib.suppress(Exception):
            h.close()
        logging.root.removeHandler(h)


@pytest.fixture
def _isolated_excepthook() -> Any:
    """Save + restore sys.excepthook + threading.excepthook."""
    import threading

    prior_sys = sys.excepthook
    prior_threading = threading.excepthook
    # Replace sys.excepthook with no-op so pytest's prior hook
    # doesn't capture synthetic exceptions during tests.
    sys.excepthook = lambda *_args: None
    yield
    sys.excepthook = prior_sys
    threading.excepthook = prior_threading


# ---------------------------------------------------------------------------
# bootstrap() — basic shape
# ---------------------------------------------------------------------------


class TestBootstrapBasic:
    """The headline contract: one call, full setup, clean exit."""

    def test_bootstrap_as_context_manager(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap(app_name="test-app", app_version="0.0.1") as ctx:
            assert ctx.app_name == "test-app"
            assert ctx.app_version == "0.0.1"
            assert mgf.common.current_context() is ctx
        # After exit, context is cleared.
        assert mgf.common.current_context() is None

    def test_bootstrap_as_one_shot_returns_appcontext(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """The dual-mode return: ``ctx = bootstrap(...)`` works
        without ``with``. The returned wrapper delegates to the
        AppContext."""
        ctx = mgf.common.bootstrap(app_name="oneshot", app_version="1.0")
        try:
            assert ctx.app_name == "oneshot"
            assert ctx.app_version == "1.0"
            assert mgf.common.current_context() is not None
        finally:
            ctx.shutdown()
        assert mgf.common.current_context() is None

    def test_bootstrap_default_settings_use_auto_preset(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """FEEDBACK §14.15: omitted ``settings=`` → preset='auto'
        (preserves v0.1 behaviour for legacy consumers)."""
        with mgf.common.bootstrap(app_name="auto-default", app_version="0.0") as ctx:
            assert ctx.settings.preset == "auto"

    def test_bootstrap_explicit_settings_keep_preset(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """Explicit MgfSettings(...) defaults to preset='explicit'
        — the new-project default."""
        s = MgfSettings(logging=LoggingSettings(level="DEBUG"))
        with mgf.common.bootstrap(
            app_name="explicit",
            app_version="0.0",
            settings=s,
        ) as ctx:
            assert ctx.settings.preset == "explicit"
            assert ctx.settings.logging.level == "DEBUG"

    def test_app_name_resolves_via_current_context(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """``app_name()`` consults current_context() first; the
        value matches what bootstrap was called with."""
        with mgf.common.bootstrap(app_name="ctx-app", app_version="0.1"):
            assert mgf.common.app_name() == "ctx-app"
            assert mgf.common.app_version() == "0.1"

    def test_shutdown_is_idempotent(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """Two shutdowns do not raise; second is a no-op."""
        ctx = mgf.common.bootstrap(app_name="idem", app_version="0.0")
        ctx.shutdown()
        ctx.shutdown()  # MUST NOT raise


# ---------------------------------------------------------------------------
# Atomicity (FEEDBACK §14.2)
# ---------------------------------------------------------------------------


class TestBootstrapSetupLoggingFlag:
    """BOOTSTRAP-01 — ``bootstrap(setup_logging=False)``.

    When the consumer's framework owns the logging stack (Django
    LOGGING dictConfig, FastAPI/uvicorn logging, Flask app.logger),
    bootstrap() must NOT install StreamHandler / RotatingFileHandler
    on top — that produces double log emission and conflicts with
    framework-native carve-outs (LOG-01).
    """

    def test_setup_logging_false_skips_handler_install(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        # Snapshot whatever handlers are present BEFORE bootstrap
        # (the framework's dictConfig has run by now in real usage;
        # our isolation fixture just clears them, so the baseline is
        # 0 handlers).
        before = list(logging.root.handlers)

        ctx = mgf.common.bootstrap(
            app_name="django-style",
            app_version="0.0.1",
            setup_logging=False,
        )
        try:
            after = list(logging.root.handlers)
            # No handlers added — framework keeps full control.
            assert after == before
            # But identity + AppContext registration still happened.
            assert ctx.app_name == "django-style"
            assert mgf.common.current_context() is ctx
        finally:
            ctx.shutdown()

    def test_setup_logging_true_installs_handlers(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        # Default behaviour — handlers ARE installed.
        ctx = mgf.common.bootstrap(
            app_name="default-style",
            app_version="0.0.1",
        )
        try:
            assert len(logging.root.handlers) > 0
        finally:
            ctx.shutdown()

    def test_setup_logging_false_skips_level_overrides(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        # When skipped, level_overrides MUST not run either — they
        # presume the handlers exist + are owned by mgf-common.
        target = "test_bootstrap_skip_overrides_logger"
        prior_level = logging.getLogger(target).level
        s = MgfSettings(
            logging=LoggingSettings(
                level_overrides={target: "ERROR"},
            ),
        )
        ctx = mgf.common.bootstrap(
            app_name="skip-overrides",
            app_version="0.0.1",
            settings=s,
            setup_logging=False,
        )
        try:
            # Override should NOT have been applied.
            assert logging.getLogger(target).level == prior_level
        finally:
            ctx.shutdown()


class TestBootstrapAtomicity:
    """Bootstrap is transactional. On any failure, partial state
    is rolled back and ``BootstrapError`` is raised carrying the
    failed step + the original cause."""

    def test_bootstrap_failure_rolls_back_partial_state(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Inject a failure at the OTel step. Assert no log handlers
        left attached and BootstrapError carries the right metadata."""

        def _bad_setup_otel(*args: Any, **kwargs: Any) -> None:
            raise RuntimeError("synthetic OTel setup failure")

        # Patch where bootstrap looks it up (lazy import inside).
        # bootstrap calls the PRIVATE `_wire_otel` to bypass the
        # deprecation warning the public `setup_otel` shim emits.
        monkeypatch.setattr(
            "mgf.common.observability.otel_setup._wire_otel",
            _bad_setup_otel,
        )

        with pytest.raises(BootstrapError) as exc_info:
            mgf.common.bootstrap(
                app_name="rollback-test",
                app_version="0.0",
                settings=MgfSettings(otel=OtelSettings(enabled=True)),
            )

        assert exc_info.value.failed_step == "otel"
        assert "synthetic OTel setup failure" in str(exc_info.value.__cause__)
        # Identity rolled back to the placeholder defaults.
        assert _identity._APP_NAME == "app"
        # No global context left.
        assert _lifecycle._GLOBAL_CONTEXT is None

    def test_bootstrap_failure_message_carries_cause(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """The BootstrapError message includes the failed step name
        AND the original exception text, with `__cause__` populated."""

        def _bad_install(*a: Any, **kw: Any) -> None:
            raise OSError("permission denied: cannot install hook")

        # Patch the private `_wire_excepthook` (bootstrap bypasses
        # the deprecated public `install_excepthook` shim).
        monkeypatch.setattr(
            "mgf.common.observability.excepthook._wire_excepthook",
            _bad_install,
        )
        with pytest.raises(BootstrapError) as exc_info:
            mgf.common.bootstrap(app_name="hook-fail", app_version="0.0")

        assert exc_info.value.failed_step == "excepthooks"
        assert "permission denied" in str(exc_info.value.__cause__)

    def test_rollback_chain_completes_when_a_rollback_step_itself_raises(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """RA-02: the LIFO rollback chain MUST complete every step
        even when one of the rollback closures raises mid-unwind.

        Scenario: ``_wire_otel`` fails at step 7. The unwind runs
        the rollbacks LIFO — buffer_replay (no-op), level_overrides,
        logging, identity. We inject a failure in the ``logging``
        rollback (``_unwire_logging``); the chain MUST still call
        the ``identity`` rollback that follows so identity ends up
        at the placeholder default, NOT stuck mid-replace.

        Three invariants:
          1. ``BootstrapError`` carries the ORIGINAL cause
             (``_wire_otel`` failure), not the rollback failure.
          2. stderr received a "rollback 'logging' failed: …" line
             so an operator can correlate.
          3. Despite the rollback failure, identity rolled back
             (proves the loop continued past the failing rollback).
        """

        def _bad_setup_otel(*args: Any, **kwargs: Any) -> None:
            raise RuntimeError("synthetic OTel failure (RA-02)")

        # The original "_unwire_logging" lives in mgf.common._lifecycle.
        # Replace it with one that raises so we exercise the
        # in-rollback failure path.
        def _bad_unwire_logging() -> None:
            raise OSError("simulated unwire_logging failure (RA-02)")

        monkeypatch.setattr(
            "mgf.common.observability.otel_setup._wire_otel",
            _bad_setup_otel,
        )
        monkeypatch.setattr(
            "mgf.common._lifecycle._unwire_logging",
            _bad_unwire_logging,
        )

        with pytest.raises(BootstrapError) as exc_info:
            mgf.common.bootstrap(
                app_name="ra02-rollback-chain",
                app_version="0.0",
                settings=MgfSettings(otel=OtelSettings(enabled=True)),
            )

        # Invariant 1: BootstrapError carries the OTel cause, not
        # the rollback failure. The rollback failure is logged to
        # stderr but never propagates.
        assert exc_info.value.failed_step == "otel"
        assert "synthetic OTel failure (RA-02)" in str(exc_info.value.__cause__)
        assert "unwire_logging failure" not in str(exc_info.value.__cause__)

        # Invariant 2: stderr names the failed rollback so operators
        # can correlate the broken cleanup.
        captured = capsys.readouterr()
        assert "rollback 'logging' failed" in captured.err
        assert "simulated unwire_logging failure (RA-02)" in captured.err

        # Invariant 3: identity DID roll back despite the logging
        # rollback failure — the chain continued past the bad step.
        # The ``identity`` rollback runs AFTER ``logging`` in LIFO
        # order (identity was appended first, popped last).
        assert _identity._APP_NAME == "app"
        assert _identity._APP_VERSION == "unknown"
        # No global context left — bootstrap raised before setting it.
        assert _lifecycle._GLOBAL_CONTEXT is None


# ---------------------------------------------------------------------------
# Idempotency (FEEDBACK §14.10)
# ---------------------------------------------------------------------------


class TestBootstrapIdempotency:
    """Second bootstrap() call replaces; previous context gets
    shutdown; AUDIT log fires."""

    def test_second_bootstrap_replaces_previous(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        ctx1 = mgf.common.bootstrap(app_name="first", app_version="1.0")
        try:
            assert mgf.common.current_context().app_name == "first"
            ctx2 = mgf.common.bootstrap(app_name="second", app_version="2.0")
            try:
                assert mgf.common.current_context().app_name == "second"
                # Previous ctx was marked superseded by the second
                # call (RA-15: prior._shut_down=True, rollbacks
                # cleared so a later prior.shutdown() is a no-op
                # rather than running stale rollbacks against the
                # new bootstrap's wiring).
                assert ctx1._shut_down is True
                assert ctx1._wiring is not None
                assert ctx1._wiring.rollbacks == []
            finally:
                ctx2.shutdown()
        finally:
            with contextlib.suppress(Exception):
                ctx1.shutdown()  # idempotent; no-op

    def test_replace_chains_pre_bootstrap_state_forward(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """RA-15: when bootstrap replaces a prior context and the
        consumer eventually shuts down the new context, the global
        state restores to PRE-prior values (not in-between values
        that belonged to the now-superseded prior).

        Without the prior_* chain-forward this regresses: the new
        context's shutdown restores ``sys.excepthook`` to the
        PRIOR's hook (because that's what was current when the new
        bootstrap saved its 'prior' snapshot), and the prior's hook
        leaks into the rest of the process.

        (``_wire_excepthook`` is idempotent — the SAME hook object
        is installed by both bootstraps via the
        ``_INSTALLED_MARKER`` short-circuit. So we verify the
        chain-forward via the wiring snapshot rather than function
        identity at install time.)
        """
        pre_bootstrap_excepthook = sys.excepthook

        # ctx1's return value is unused — the test only needs the
        # bootstrap side effect (installing the intermediate hook).
        _ctx1 = mgf.common.bootstrap(app_name="first", app_version="1.0")
        ctx2 = mgf.common.bootstrap(app_name="second", app_version="2.0")

        # Chain-forward invariant: ctx2's wiring snapshot points at
        # the PRE-bootstrap hook, not at ctx1's hook (which is what
        # sys.excepthook actually was when ctx2's bootstrap ran).
        assert ctx2._wiring is not None
        assert ctx2._wiring.prior_excepthook is pre_bootstrap_excepthook
        assert ctx2._wiring.prior_app_name == "app"
        assert ctx2._wiring.prior_app_version == "unknown"

        ctx2.shutdown()
        # End-state: sys.excepthook restores to the PRE-bootstrap
        # value (not to ctx1's intermediate state).
        assert sys.excepthook is pre_bootstrap_excepthook

    def test_concurrent_bootstraps_serialize_cleanly(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """RA-03: two threads calling ``bootstrap()`` simultaneously
        must serialise on ``_WIRING_LOCK``. The end state is one of
        the two threads' inputs (last-call-wins), never a mix:

          - ``current_context()`` returns an AppContext whose
            ``app_name`` matches ONE of the two threads' inputs.
          - All four wiring surfaces (identity, logging, OTel
            provider, excepthook) match THAT context.
          - The losing thread's context exists but is marked
            ``_shut_down=True`` with empty rollbacks (superseded
            via the replace path).

        Without ``_WIRING_LOCK``, the threads race on
        ``logging.root.addHandler()`` / ``removeHandler()`` (the
        stdlib logging module is not thread-safe for handler-list
        mutations); the symptom is missing or duplicated log
        records, plus identity mismatches between
        ``_identity._APP_NAME`` and ``current_context().app_name``.
        """
        import threading

        results: list[mgf.common.AppContext] = []
        errors: list[BaseException] = []
        barrier = threading.Barrier(2)

        def boot(name: str) -> None:
            try:
                # Synchronise the call to maximise race surface.
                barrier.wait(timeout=5.0)
                ctx = mgf.common.bootstrap(
                    app_name=name,
                    app_version="1.0",
                )
                results.append(ctx)
            except BaseException as e:
                errors.append(e)

        # Run many iterations so any race is reliably exposed.
        n_iters = 25
        for _ in range(n_iters):
            results.clear()
            errors.clear()
            barrier.reset()
            t_a = threading.Thread(target=boot, args=("racer-a",), name="racer-a")
            t_b = threading.Thread(target=boot, args=("racer-b",), name="racer-b")
            t_a.start()
            t_b.start()
            t_a.join(timeout=10.0)
            t_b.join(timeout=10.0)

            assert not t_a.is_alive() and not t_b.is_alive(), "thread hang"
            assert not errors, f"concurrent bootstrap raised: {errors!r}"
            assert len(results) == 2

            # Identify which context won and which lost.
            global_ctx = mgf.common.current_context()
            assert global_ctx is not None
            assert global_ctx.app_name in {"racer-a", "racer-b"}

            winner = next(c for c in results if c is global_ctx)
            loser = next(c for c in results if c is not global_ctx)
            assert winner.app_name == global_ctx.app_name
            # Loser was superseded by the replace path (RA-15).
            assert loser._shut_down is True
            assert loser._wiring is not None
            assert loser._wiring.rollbacks == []

            # Identity cache + winner agree (no mixed wiring).
            assert winner.app_name == _identity._APP_NAME
            assert winner.app_version == _identity._APP_VERSION

            # Tear down for the next iteration.
            winner.shutdown()

    def test_failed_replace_leaves_prior_context_active(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """RA-15: when the SECOND bootstrap fails partway, the FIRST
        context stays the active global. The process is not left
        context-less; ``current_context()`` still returns ``ctx1``;
        ``ctx1.shutdown()`` still works (rollbacks not yet cleared)."""
        ctx1 = mgf.common.bootstrap(app_name="first", app_version="1.0")
        try:
            # Sanity: ctx1 is active.
            assert mgf.common.current_context() is ctx1

            # Inject a failure in the second bootstrap at the OTel
            # step. Same mechanism as
            # test_bootstrap_failure_rolls_back_partial_state but
            # with an active prior context.
            def _bad_setup_otel(*args: Any, **kwargs: Any) -> None:
                raise RuntimeError("synthetic OTel failure on replace")

            monkeypatch.setattr(
                "mgf.common.observability.otel_setup._wire_otel",
                _bad_setup_otel,
            )
            with pytest.raises(BootstrapError) as exc_info:
                mgf.common.bootstrap(
                    app_name="second",
                    app_version="2.0",
                    settings=MgfSettings(otel=OtelSettings(enabled=True)),
                )
            assert exc_info.value.failed_step == "otel"

            # RA-15 invariant: ctx1 is STILL the active global.
            assert mgf.common.current_context() is ctx1
            assert mgf.common.current_context().app_name == "first"
            # ctx1's wiring is intact — rollbacks were not cleared
            # because the replace did not succeed.
            assert ctx1._shut_down is False
            assert ctx1._wiring is not None
            assert ctx1._wiring.rollbacks  # non-empty
        finally:
            ctx1.shutdown()


# ---------------------------------------------------------------------------
# Bootstrap-time log buffer (FEEDBACK §14.3)
# ---------------------------------------------------------------------------


class TestBootstrapLogBuffer:
    """The deferred replay buffer captures records emitted from
    inside bootstrap and replays them through the wired handlers."""

    def test_buffer_captures_and_replays(self) -> None:
        """Direct unit test of the buffer mechanics."""
        buffer = _lifecycle._BootstrapLogBuffer()
        # Synthesise three records.
        for i in range(3):
            record = logging.LogRecord(
                name="test",
                level=logging.INFO,
                pathname=__file__,
                lineno=1,
                msg=f"msg-{i}",
                args=None,
                exc_info=None,
            )
            buffer.emit(record)
        assert len(buffer._records) == 3

        # Replay through a target handler that captures.
        target = logging.Handler(level=logging.INFO)
        seen: list[str] = []
        target.handle = lambda r: seen.append(r.getMessage())  # type: ignore[method-assign]
        buffer.replay_to([target])
        assert seen == ["msg-0", "msg-1", "msg-2"]

    def test_buffer_respects_target_handler_level(self) -> None:
        """Replay applies the target's level filter — DEBUG records
        get dropped if the target is set to INFO."""
        buffer = _lifecycle._BootstrapLogBuffer()
        for level, msg in [(logging.DEBUG, "debug-msg"), (logging.INFO, "info-msg")]:
            record = logging.LogRecord(
                name="test",
                level=level,
                pathname=__file__,
                lineno=1,
                msg=msg,
                args=None,
                exc_info=None,
            )
            buffer.emit(record)

        target = logging.Handler(level=logging.INFO)
        seen: list[str] = []
        target.handle = lambda r: seen.append(r.getMessage())  # type: ignore[method-assign]
        buffer.replay_to([target])
        assert seen == ["info-msg"]

    def test_flush_to_stderr_includes_level_and_logger(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """RA-17: the emergency flush MUST format records with at
        least level + logger name + message so an operator can
        diagnose a mid-bootstrap failure. Before the fix, the
        flush fell back to ``record.getMessage()`` (bare text only)
        when no formatter was attached.
        """
        buffer = _lifecycle._BootstrapLogBuffer()
        record_info = logging.LogRecord(
            name="mgf.boot.step1",
            level=logging.INFO,
            pathname=__file__,
            lineno=1,
            msg="wiring identity",
            args=None,
            exc_info=None,
        )
        record_error = logging.LogRecord(
            name="mgf.boot.step3",
            level=logging.ERROR,
            pathname=__file__,
            lineno=42,
            msg="logging step failed: permission denied",
            args=None,
            exc_info=None,
        )
        buffer.emit(record_info)
        buffer.emit(record_error)

        buffer.flush_to_stderr()
        captured = capsys.readouterr().err

        # Both records appear with level + logger + message.
        assert "INFO" in captured
        assert "ERROR" in captured
        assert "mgf.boot.step1" in captured
        assert "mgf.boot.step3" in captured
        assert "wiring identity" in captured
        assert "logging step failed: permission denied" in captured
        # The wrapper prefix is preserved so operators can grep
        # for "[mgf.common bootstrap]" to find the failed-bootstrap
        # block in noisy stderr.
        assert "[mgf.common bootstrap]" in captured


# ---------------------------------------------------------------------------
# AppContext (the data shape)
# ---------------------------------------------------------------------------


class TestAppContext:
    """The frozen-snapshot data class returned by bootstrap."""

    def test_appcontext_carries_settings(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        s = MgfSettings(
            logging=LoggingSettings(level="WARNING"),
            otel=OtelSettings(sample_rate=0.5),
        )
        with mgf.common.bootstrap(
            app_name="data",
            app_version="9.9",
            settings=s,
        ) as ctx:
            assert ctx.settings.logging.level == "WARNING"
            assert ctx.settings.otel.sample_rate == 0.5


# ---------------------------------------------------------------------------
# MgfSettings — the typed root
# ---------------------------------------------------------------------------


class TestMgfSettings:
    """The library's own settings root."""

    def test_defaults_construct_cleanly(self) -> None:
        s = MgfSettings()
        assert s.preset == "explicit"
        assert s.logging.level == "INFO"
        assert s.otel.enabled is False
        assert s.crash.sinks == ["local"]
        assert s.redaction.strict_mode is False

    def test_env_var_aliases_work(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """``MGF_*`` env vars override defaults via pydantic-settings
        nested-delimiter convention."""
        monkeypatch.setenv("MGF_LOGGING__LEVEL", "DEBUG")
        monkeypatch.setenv("MGF_OTEL__SAMPLE_RATE", "0.1")
        s = MgfSettings()
        assert s.logging.level == "DEBUG"
        assert s.otel.sample_rate == 0.1

    def test_unknown_field_rejected(self) -> None:
        """Rule CF-06: extra='forbid' on every sub-model."""
        with pytest.raises(ValidationError):
            LoggingSettings(unknown_field=42)  # type: ignore[call-arg]

    def test_level_overrides_field_typed(self) -> None:
        """FEEDBACK §14.6: per-component log levels first-class."""
        s = LoggingSettings(
            level_overrides={
                "mgf.common.observability": "DEBUG",
                "urllib3": "WARNING",
            }
        )
        assert s.level_overrides["mgf.common.observability"] == "DEBUG"

    def test_strict_mode_field_present(self) -> None:
        """FEEDBACK §14.12: GDPR strict mode field wired in Phase 1
        (patterns themselves wired in Phase 6)."""
        s = RedactionSettings(strict_mode=True)
        assert s.strict_mode is True

    def test_sample_rate_validated(self) -> None:
        """OtelSettings.sample_rate MUST be in [0.0, 1.0]."""
        with pytest.raises(ValidationError):
            OtelSettings(sample_rate=1.5)
        with pytest.raises(ValidationError):
            OtelSettings(sample_rate=-0.1)

    def test_recursive_serialisation(self) -> None:
        """``model_dump_for_yaml`` recurses into sub-models (works
        around the shallow ``to_yaml_dict`` known limitation)."""
        s = MgfSettings(
            logging=LoggingSettings(level="DEBUG"),
            crash=CrashSettings(sinks=["local", "sentry"]),
        )
        dumped = s.model_dump_for_yaml()
        assert dumped["logging"]["level"] == "DEBUG"
        assert dumped["crash"]["sinks"] == ["local", "sentry"]


# ---------------------------------------------------------------------------
# Per-component log level overrides applied at bootstrap (§14.6)
# ---------------------------------------------------------------------------


class TestLevelOverridesAppliedAtBootstrap:
    """The level_overrides dict is applied during _wire_logging
    so the named loggers see the override level."""

    def test_overrides_set_logger_levels(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        s = MgfSettings(
            logging=LoggingSettings(
                level="INFO",
                level_overrides={"some.deep.module": "DEBUG"},
            )
        )
        with mgf.common.bootstrap(
            app_name="lo",
            app_version="0.0",
            settings=s,
        ):
            assert logging.getLogger("some.deep.module").level == logging.DEBUG

    def test_overrides_rolled_back_on_shutdown(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        prior = logging.getLogger("rollback.test").level
        s = MgfSettings(
            logging=LoggingSettings(
                level_overrides={"rollback.test": "ERROR"},
            )
        )
        ctx = mgf.common.bootstrap(
            app_name="rb",
            app_version="0.0",
            settings=s,
        )
        assert logging.getLogger("rollback.test").level == logging.ERROR
        ctx.shutdown()
        # Restored to prior level.
        assert logging.getLogger("rollback.test").level == prior


# ---------------------------------------------------------------------------
# current_context() — resolution order
# ---------------------------------------------------------------------------


class TestCurrentContext:
    """The accessor consulted by every observability function."""

    def test_returns_none_before_bootstrap(self) -> None:
        # _isolated_state isn't a fixture here — we just verify the
        # module-level state without bootstrapping.
        # If a prior test left a context, skip via the global directly.
        assert _lifecycle._GLOBAL_CONTEXT is None or isinstance(
            _lifecycle._GLOBAL_CONTEXT,
            _lifecycle.AppContext,
        )

    def test_returns_active_context_after_bootstrap(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap(app_name="active", app_version="0.0") as ctx:
            assert mgf.common.current_context() is ctx

    def test_returns_none_after_shutdown(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        ctx = mgf.common.bootstrap(app_name="cleanup", app_version="0.0")
        ctx.shutdown()
        assert mgf.common.current_context() is None


# ---------------------------------------------------------------------------
# RA-22 — _maybe_emit_runtime_snapshot is best-effort
# ---------------------------------------------------------------------------


class TestRA22RuntimeSnapshotBestEffort:
    """RA-22: ``_maybe_emit_runtime_snapshot`` is wrapped in
    try/except so a snapshot failure does not propagate out of
    ``bootstrap()``.

    Why this matters: `bootstrap()` calls the snapshot helper
    AFTER it has set ``_GLOBAL_CONTEXT = new_context`` (line 864
    in `_lifecycle.py`). If the snapshot raises, the process is
    left with a fully-wired AppContext on the global AND a
    propagating ``BootstrapError`` — an inconsistent state that
    callers cannot recover from cleanly (no AppContext to
    `.shutdown()` from the caller's perspective, but the global
    is set so future bootstrap calls hit the "replace" path).

    Practical failure modes the wrap protects against:
      - `settings.model_dump()` raising on a consumer-supplied
        ``@computed_field`` that throws.
      - File-handle exhaustion / permissions errors on the
        snapshot atomic-write path.
      - An import-time error in the lazy-imported
        ``mgf.common.cli._doctor_live`` (e.g., a typo introduced
        in a refactor).
    """

    def test_snapshot_failure_does_not_propagate(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """The defining RA-22 guarantee: even when the snapshot
        helper raises mid-construction, ``bootstrap()`` returns
        a usable ``AppContext`` and the global is set.

        Capture path: ``bootstrap()`` rewires the root logger's
        handlers to the JSON-to-stderr sink, so pytest's ``caplog``
        fixture (which attaches its handler before bootstrap)
        doesn't see records emitted afterward. We capture stderr
        directly via ``capsys`` and parse the JSON line — that's
        what an operator would actually see.
        """
        import json as _json

        def boom(*_args: Any, **_kw: Any) -> None:
            raise RuntimeError("synthetic-snapshot-failure")

        # Patch the lazily-imported emitter so the construction
        # path runs (and we exercise the try/except boundary
        # AROUND it) but the actual write blows up.
        monkeypatch.setattr(
            "mgf.common._doctor_snapshot.emit_runtime_snapshot",
            boom,
        )
        monkeypatch.setenv("MGF_DOCTOR_RUNTIME_PATH", "/tmp/snap.json")

        ctx = mgf.common.bootstrap(
            app_name="ra22-test",
            app_version="0.0.0",
        )
        try:
            assert ctx is not None
            assert mgf.common.current_context() is ctx
            # The warning JSON line lands on stderr; parse it.
            stderr = capsys.readouterr().err
            audit_lines = [
                line for line in stderr.splitlines() if "bootstrap.runtime_snapshot_failed" in line
            ]
            assert len(audit_lines) == 1, (
                f"RA-22: expected exactly one runtime-snapshot-failed "
                f"audit record on stderr, got {len(audit_lines)}. "
                f"stderr:\n{stderr}"
            )
            parsed = _json.loads(audit_lines[0])
            assert parsed["event"] == "bootstrap.runtime_snapshot_failed"
            assert parsed["error_type"] == "RuntimeError"
            assert parsed["level"] == "WARNING"
        finally:
            ctx.shutdown()

    def test_snapshot_success_does_not_warn(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
        tmp_path: Path,
    ) -> None:
        """Sanity: the try/except wrap must NOT mask a successful
        snapshot path. When `MGF_DOCTOR_RUNTIME_PATH` points at a
        writable location, the snapshot lands and NO warning fires."""
        snapshot_path = tmp_path / "snap.json"
        monkeypatch.setenv("MGF_DOCTOR_RUNTIME_PATH", str(snapshot_path))

        ctx = mgf.common.bootstrap(
            app_name="ra22-ok",
            app_version="0.0.0",
        )
        try:
            assert ctx is not None
            assert snapshot_path.exists()
            stderr = capsys.readouterr().err
            failure_lines = [
                line for line in stderr.splitlines() if "bootstrap.runtime_snapshot_failed" in line
            ]
            assert failure_lines == [], (
                f"RA-22: snapshot succeeded but the failure-path "
                f"audit warning fired anyway. stderr:\n{stderr}"
            )
        finally:
            ctx.shutdown()
