"""Tests for the mgf-fed central-logging bootstrap (Logging L2, item e).

``mgf.fed._logging.bootstrap_logging`` attaches mgf-logship's ``LogShipper`` to
the root logger when the env configures it, and is a silent no-op otherwise. The
real ``mgf-logship`` need not be installed: we inject a stub module so the lazy
``from mgf.logship import LogShipper`` resolves to a fake handler.
"""

from __future__ import annotations

import logging
import sys
import types
from collections.abc import Iterator

import pytest

from mgf.fed import _logging


@pytest.fixture
def clean_root() -> Iterator[logging.Logger]:
    """Snapshot/restore the root logger so a test's handler+level mutations
    don't leak into the rest of the session."""
    root = logging.getLogger()
    saved_handlers = root.handlers[:]
    saved_level = root.level
    try:
        yield root
    finally:
        root.handlers[:] = saved_handlers
        root.setLevel(saved_level)


@pytest.fixture
def fake_logship(monkeypatch: pytest.MonkeyPatch) -> type[logging.Handler]:
    """Inject a stub ``mgf.logship`` exposing ``LogShipper(logging.Handler)``."""

    class FakeLogShipper(logging.Handler):
        created: dict[str, object] = {}  # noqa: RUF012 — shared probe across instances

        def __init__(
            self,
            endpoint: str,
            *,
            token: str | None = None,
            app: str | None = None,
            env: str | None = None,
        ) -> None:
            super().__init__()
            FakeLogShipper.created = {
                "endpoint": endpoint,
                "token": token,
                "app": app,
                "env": env,
            }

        def emit(self, record: logging.LogRecord) -> None:  # pragma: no cover
            pass

    module = types.ModuleType("mgf.logship")
    module.LogShipper = FakeLogShipper  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "mgf.logship", module)
    # Keep unit tests hermetic: don't run mgf-common's real bootstrap (file/console
    # handlers, atexit hooks). A dedicated live check covers the context wiring.
    monkeypatch.setattr(_logging, "_establish_app_context", lambda: None)
    return FakeLogShipper


def test_noop_when_unconfigured(
    monkeypatch: pytest.MonkeyPatch, clean_root: logging.Logger
) -> None:
    monkeypatch.delenv("MGF_LOGSHIP_URL", raising=False)
    monkeypatch.delenv("MGF_LOGSHIP_INGEST_TOKEN", raising=False)
    before = clean_root.handlers[:]
    assert _logging.bootstrap_logging() is False
    assert clean_root.handlers == before  # nothing attached


def test_noop_when_only_url_set(
    monkeypatch: pytest.MonkeyPatch,
    clean_root: logging.Logger,
    fake_logship: type[logging.Handler],
) -> None:
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example")
    monkeypatch.delenv("MGF_LOGSHIP_INGEST_TOKEN", raising=False)
    assert _logging.bootstrap_logging() is False
    assert not any(isinstance(h, fake_logship) for h in clean_root.handlers)


def test_attaches_shipper_when_configured(
    monkeypatch: pytest.MonkeyPatch,
    clean_root: logging.Logger,
    fake_logship: type[logging.Handler],
) -> None:
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example/")
    monkeypatch.setenv("MGF_LOGSHIP_INGEST_TOKEN", "tok-123")
    monkeypatch.setenv("MGF_LOGSHIP_ENV", "staging")
    monkeypatch.delenv("MGF_LOG_LEVEL", raising=False)
    clean_root.setLevel(logging.WARNING)  # above INFO → bootstrap should lower it

    assert _logging.bootstrap_logging() is True
    attached = [h for h in clean_root.handlers if isinstance(h, fake_logship)]
    assert len(attached) == 1
    assert fake_logship.created == {  # type: ignore[attr-defined]
        "endpoint": "https://logs.example/",
        "token": "tok-123",
        "app": "mgf-fed",
        "env": "staging",
    }
    assert clean_root.level == logging.INFO


def test_env_defaults(
    monkeypatch: pytest.MonkeyPatch,
    clean_root: logging.Logger,
    fake_logship: type[logging.Handler],
) -> None:
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example")
    monkeypatch.setenv("MGF_LOGSHIP_INGEST_TOKEN", "tok")
    monkeypatch.delenv("MGF_LOGSHIP_ENV", raising=False)
    assert _logging.bootstrap_logging() is True
    assert fake_logship.created["env"] == "prod"  # type: ignore[attr-defined]
    assert fake_logship.created["app"] == "mgf-fed"  # type: ignore[attr-defined]


def test_idempotent(
    monkeypatch: pytest.MonkeyPatch,
    clean_root: logging.Logger,
    fake_logship: type[logging.Handler],
) -> None:
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example")
    monkeypatch.setenv("MGF_LOGSHIP_INGEST_TOKEN", "tok")
    assert _logging.bootstrap_logging() is True
    assert _logging.bootstrap_logging() is False  # second call no-ops
    attached = [h for h in clean_root.handlers if isinstance(h, fake_logship)]
    assert len(attached) == 1


def test_establishes_app_context_when_attaching(
    monkeypatch: pytest.MonkeyPatch,
    clean_root: logging.Logger,
    fake_logship: type[logging.Handler],
) -> None:
    calls: list[int] = []
    monkeypatch.setattr(_logging, "_establish_app_context", lambda: calls.append(1))
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example")
    monkeypatch.setenv("MGF_LOGSHIP_INGEST_TOKEN", "tok")
    assert _logging.bootstrap_logging() is True
    assert calls == [1]  # context established exactly once, before the shipper attach


def test_noop_when_logship_not_installed(
    monkeypatch: pytest.MonkeyPatch, clean_root: logging.Logger
) -> None:
    monkeypatch.setenv("MGF_LOGSHIP_URL", "https://logs.example")
    monkeypatch.setenv("MGF_LOGSHIP_INGEST_TOKEN", "tok")
    # None in sys.modules makes `import mgf.logship` raise ImportError.
    monkeypatch.setitem(sys.modules, "mgf.logship", None)
    before = clean_root.handlers[:]
    assert _logging.bootstrap_logging() is False
    assert clean_root.handlers == before


# --- log_operation: INFO start/end records + ERROR-with-traceback (LG-11 / LG-05) ---


def test_log_operation_emits_start_and_end_records(
    caplog: pytest.LogCaptureFixture,
) -> None:
    with caplog.at_level(logging.INFO, logger="mgf.fed"):
        with _logging.log_operation("release.remote", repos=3):
            pass
    events = [(r.operation, r.event) for r in caplog.records]  # type: ignore[attr-defined]
    assert ("release.remote", "release.remote.start") in events
    assert ("release.remote", "release.remote.end") in events
    end = next(r for r in caplog.records if r.event == "release.remote.end")  # type: ignore[attr-defined]
    assert isinstance(end.duration_ms, float)  # type: ignore[attr-defined]
    assert end.repos == 3  # type: ignore[attr-defined]


def test_log_operation_logs_traceback_and_reraises(
    caplog: pytest.LogCaptureFixture,
) -> None:
    with caplog.at_level(logging.INFO, logger="mgf.fed"):
        with pytest.raises(ValueError, match="boom"):
            with _logging.log_operation("local_ci.exec", repos=1):
                raise ValueError("boom")
    err = next(r for r in caplog.records if r.levelno == logging.ERROR)
    assert err.operation == "local_ci.exec"  # type: ignore[attr-defined]
    assert err.event == "local_ci.exec.error"  # type: ignore[attr-defined]
    assert err.exc_info is not None  # traceback captured for the central log
    # No end record on the failure path.
    assert not any(getattr(r, "event", "") == "local_ci.exec.end" for r in caplog.records)
