"""V04-03 — bootstrap_for_test capture_logs / capture_spans flags.

Closes FEEDBACK V04-03. Optional capture fields make the test
helper save more than a few lines vs. the manual
``bootstrap(settings=MgfSettings.testing())`` shape — the
captured records / spans are accessible via
``ctx.captured_logs`` / ``ctx.captured_spans`` for direct
assertions.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import pytest

import mgf.common
from mgf.common._lifecycle import AppContext

if TYPE_CHECKING:
    from pathlib import Path


@pytest.fixture
def _isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
    monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
    monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
    monkeypatch.setenv("MGF_DISABLE_PYPROJECT", "1")
    return tmp_path


@pytest.fixture
def _isolated_logging() -> None:
    import logging as _logging

    from mgf.common import _identity

    prior_handlers = list(_logging.root.handlers)
    prior_level = _logging.root.level
    prior_app = _identity._APP_NAME
    prior_ver = _identity._APP_VERSION

    yield

    for h in list(_logging.root.handlers):
        _logging.root.removeHandler(h)
    for h in prior_handlers:
        _logging.root.addHandler(h)
    _logging.root.setLevel(prior_level)
    _identity._APP_NAME = prior_app
    _identity._APP_VERSION = prior_ver

    from mgf.common import _lifecycle

    _lifecycle._GLOBAL_CONTEXT = None


@pytest.fixture
def _isolated_excepthook() -> None:
    import sys
    import threading

    prior_sys = sys.excepthook
    prior_thread = threading.excepthook
    yield
    sys.excepthook = prior_sys
    threading.excepthook = prior_thread


# ---------------------------------------------------------------------------
# Default behaviour — flags off, capture fields are None / empty
# ---------------------------------------------------------------------------


class TestDefaultsNoCapture:
    """V04-03 defaults preserve v0.4.1 ergonomics — capture fields
    are None / empty when the flags aren't set."""

    def test_default_captured_logs_is_none(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap_for_test() as ctx:
            assert ctx.captured_logs is None

    def test_default_captured_spans_is_empty_list(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap_for_test() as ctx:
            # Property returns [] when no exporter wired.
            assert ctx.captured_spans == []


# ---------------------------------------------------------------------------
# Log capture
# ---------------------------------------------------------------------------


class TestLogCapture:
    """``capture_logs=True`` populates ``ctx.captured_logs`` with
    every emitted ``LogRecord``."""

    def test_capture_logs_populates_buffer(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap_for_test(capture_logs=True) as ctx:
            assert ctx.captured_logs is not None
            assert ctx.captured_logs == []

            log = logging.getLogger("test.v04_03")
            log.warning("hello from test")
            log.error("error path")

            assert len(ctx.captured_logs) >= 2
            messages = [r.getMessage() for r in ctx.captured_logs]
            assert "hello from test" in messages
            assert "error path" in messages

    def test_capture_logs_includes_debug_records(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """The capture handler installs at NOTSET so DEBUG records
        reach it even though ``MgfSettings.testing()`` defaults to
        WARNING."""
        with mgf.common.bootstrap_for_test(capture_logs=True) as ctx:
            log = logging.getLogger("test.v04_03_debug")
            log.setLevel(logging.DEBUG)
            log.debug("debug message")

            messages = [r.getMessage() for r in ctx.captured_logs]
            assert "debug message" in messages

    def test_capture_log_records_carry_logger_name(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        with mgf.common.bootstrap_for_test(capture_logs=True) as ctx:
            logging.getLogger("alpha.module").warning("from alpha")
            logging.getLogger("beta.module").warning("from beta")

            names = {r.name for r in ctx.captured_logs}
            assert "alpha.module" in names
            assert "beta.module" in names

    def test_capture_log_handler_removed_on_shutdown(
        self,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """After context shutdown, the in-memory handler is
        removed from the root logger."""
        ctx = mgf.common.bootstrap_for_test(capture_logs=True)
        try:
            from mgf.common._test_helpers import _InMemoryLogHandler

            installed = [h for h in logging.root.handlers if isinstance(h, _InMemoryLogHandler)]
            assert len(installed) == 1
        finally:
            ctx.shutdown()

        from mgf.common._test_helpers import _InMemoryLogHandler

        installed_after = [h for h in logging.root.handlers if isinstance(h, _InMemoryLogHandler)]
        assert installed_after == []


# ---------------------------------------------------------------------------
# Span capture
# ---------------------------------------------------------------------------


# Skip span tests if the [observability] extra isn't installed.
pytest.importorskip("opentelemetry.sdk.trace")


@pytest.fixture(scope="module")
def _otel_provider() -> object:
    """Install an SDK TracerProvider for the span-capture tests.

    ``bootstrap_for_test(capture_spans=True)`` requires an SDK
    TracerProvider to ALREADY be active — see the docstring on
    ``mgf.common._test_helpers._wire_span_capture`` for the
    rationale (OTel's set_tracer_provider is one-shot per
    process; auto-installing inside the helper would pollute
    other tests).

    Module-scoped so the install happens once for this file.
    Other test files / directories install their own (e.g.
    tests/unit/observability/conftest.py's session-scoped
    fixture).
    """
    from opentelemetry import trace
    from opentelemetry.sdk.trace import TracerProvider

    provider = trace.get_tracer_provider()
    if not isinstance(provider, TracerProvider):
        provider = TracerProvider()
        trace.set_tracer_provider(provider)
    return provider


class TestSpanCapture:
    """``capture_spans=True`` populates ``ctx.captured_spans``
    with finished OTel spans."""

    def test_capture_spans_populates_via_property(
        self,
        _otel_provider: object,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        from opentelemetry import trace

        with mgf.common.bootstrap_for_test(capture_spans=True) as ctx:
            tracer = trace.get_tracer("test.v04_03")
            with tracer.start_as_current_span("operation.test"):
                pass

            spans = ctx.captured_spans
            assert len(spans) >= 1
            names = [s.name for s in spans]
            assert "operation.test" in names

    def test_capture_spans_property_snapshots(
        self,
        _otel_provider: object,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        """``captured_spans`` is a property — each call returns a
        fresh snapshot. Spans accumulate as more are produced."""
        from opentelemetry import trace

        with mgf.common.bootstrap_for_test(capture_spans=True) as ctx:
            tracer = trace.get_tracer("test.v04_03_snap")

            with tracer.start_as_current_span("first"):
                pass
            first_count = len(ctx.captured_spans)

            with tracer.start_as_current_span("second"):
                pass
            second_count = len(ctx.captured_spans)

            assert second_count > first_count


# ---------------------------------------------------------------------------
# Combined capture
# ---------------------------------------------------------------------------


class TestCombinedCapture:
    """Both flags can be set in the same call."""

    def test_both_flags_populate_both_fields(
        self,
        _otel_provider: object,
        _isolated_state: Path,
        _isolated_logging: None,
        _isolated_excepthook: None,
    ) -> None:
        from opentelemetry import trace

        with mgf.common.bootstrap_for_test(
            capture_logs=True,
            capture_spans=True,
        ) as ctx:
            logging.getLogger("test.both").warning("log message")

            tracer = trace.get_tracer("test.both")
            with tracer.start_as_current_span("op.both"):
                pass

            assert len(ctx.captured_logs) >= 1
            assert len(ctx.captured_spans) >= 1


# ---------------------------------------------------------------------------
# AppContext field-shape pin
# ---------------------------------------------------------------------------


class TestAppContextCaptureFields:
    """Pin the shape of the new fields on AppContext."""

    def test_scoped_appcontext_default_capture_fields(self) -> None:
        """A scoped (non-bootstrap) AppContext also has the fields
        — they default to None / empty (consistent with bootstrap-
        owned contexts that didn't enable capture)."""
        ctx = AppContext("plugin", "1.0")
        assert ctx.captured_logs is None
        assert ctx.captured_spans == []
