"""Unit tests for :mod:`mgf.common.observability.logging_setup`.

Verifies the standard sinks (console + rotating file), the auto
format heuristic, idempotent re-call, optional sinks (env-driven),
and that the file handler degrades gracefully when its destination
cannot be created.

The conftest's ``_configure_test_identity`` fixture sets the test
app name to ``mgftest`` for every test in this directory — so
default log paths land under ``<state>/mgftest/logs/mgftest.log``
and env vars are read with the ``MGFTEST_`` prefix.
"""

from __future__ import annotations

import contextlib
import json
import logging
from logging.handlers import RotatingFileHandler
from typing import TYPE_CHECKING

import pytest

from mgf.common.observability.json_formatter import JsonFormatter
from mgf.common.observability.logging_setup import _wire_logging
from mgf.common.observability.text_formatter import TextFormatter

if TYPE_CHECKING:
    from collections.abc import Iterator
    from pathlib import Path

# Matches conftest.TEST_APP_NAME — kept local to avoid a cross-file
# import of an internal constant.
_APP = "mgftest"


@pytest.fixture(autouse=True)
def _isolate_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
    """Redirect HOME + XDG_STATE_HOME so every test gets its own log dir.

    Also explicitly closes any handlers attached during the test so the
    rotating file handler's underlying file object is closed BEFORE
    pytest's teardown observes the unraised "ResourceWarning" from
    GC. ``filterwarnings = ["error"]`` in pyproject would otherwise
    turn the warning into a test error.
    """
    monkeypatch.setenv("HOME", str(tmp_path / "home"))
    monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
    yield
    # Close + remove every handler before the next test. RotatingFileHandler
    # holds an open file; close() is required for `filterwarnings=error`
    # to not fire on the deferred GC of the file object.
    for handler in logging.root.handlers[:]:
        with contextlib.suppress(Exception):
            handler.close()
        logging.root.removeHandler(handler)


# ---------------------------------------------------------------------------
# Sinks
# ---------------------------------------------------------------------------


def test_console_handler_always_attached(tmp_path: Path) -> None:
    _wire_logging(level="INFO", fmt="json", log_file=tmp_path / "x.log")
    streams = [h for h in logging.root.handlers if isinstance(h, logging.StreamHandler)]
    # Both StreamHandler and RotatingFileHandler subclass StreamHandler;
    # filter for the bare StreamHandler instance (not the file one).
    bare_streams = [h for h in streams if not isinstance(h, RotatingFileHandler)]
    assert len(bare_streams) >= 1


def test_rotating_file_handler_always_attached(tmp_path: Path) -> None:
    log_file = tmp_path / f"{_APP}.log"
    _wire_logging(level="INFO", fmt="json", log_file=log_file)
    file_handlers = [h for h in logging.root.handlers if isinstance(h, RotatingFileHandler)]
    assert len(file_handlers) == 1
    assert file_handlers[0].baseFilename == str(log_file)


def test_file_handler_writes_json(tmp_path: Path) -> None:
    log_file = tmp_path / f"{_APP}.log"
    _wire_logging(level="DEBUG", fmt="text", log_file=log_file)
    log = logging.getLogger("mgftest.test_logging_setup")
    log.warning("smoke", extra={"trace_field": "value-1"})
    # Force flush by closing all handlers (they auto-reopen on next write).
    for handler in logging.root.handlers:
        handler.flush()
    raw = log_file.read_text(encoding="utf-8").strip().splitlines()
    assert raw, "no records written to file sink"
    obj = json.loads(raw[-1])
    assert obj["msg"] == "smoke"
    assert obj["trace_field"] == "value-1"
    assert obj["level"] == "WARNING"


# ---------------------------------------------------------------------------
# Format selection
# ---------------------------------------------------------------------------


def test_auto_format_picks_text_for_tty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("sys.stderr.isatty", lambda: True)
    _wire_logging(level="INFO", fmt="auto", log_file=tmp_path / "x.log")
    streams = [
        h
        for h in logging.root.handlers
        if isinstance(h, logging.StreamHandler) and not isinstance(h, RotatingFileHandler)
    ]
    assert isinstance(streams[0].formatter, TextFormatter)


def test_auto_format_picks_json_for_non_tty(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr("sys.stderr.isatty", lambda: False)
    _wire_logging(level="INFO", fmt="auto", log_file=tmp_path / "x.log")
    streams = [
        h
        for h in logging.root.handlers
        if isinstance(h, logging.StreamHandler) and not isinstance(h, RotatingFileHandler)
    ]
    assert isinstance(streams[0].formatter, JsonFormatter)


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


def test_re_call_replaces_handlers_not_duplicates(tmp_path: Path) -> None:
    log_file = tmp_path / f"{_APP}.log"
    _wire_logging(level="INFO", fmt="json", log_file=log_file)
    first_count = len(logging.root.handlers)
    _wire_logging(level="INFO", fmt="json", log_file=log_file)
    assert len(logging.root.handlers) == first_count


# ---------------------------------------------------------------------------
# Graceful degradation when the file sink cannot be opened
# ---------------------------------------------------------------------------


def test_unwritable_log_file_does_not_raise(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Point at a path inside a file (not a directory) — mkdir will fail
    # with NotADirectoryError, which is an OSError subclass.
    a_file = tmp_path / "a_file"
    a_file.write_text("content")
    log_file = a_file / "subdir" / "log"  # cannot create — parent is a file
    _wire_logging(level="INFO", fmt="json", log_file=log_file)
    # Console handler still attached; no file handler.
    file_handlers = [h for h in logging.root.handlers if isinstance(h, RotatingFileHandler)]
    assert file_handlers == []


# ---------------------------------------------------------------------------
# Optional sinks (env-driven)
# ---------------------------------------------------------------------------


def test_journald_skipped_without_systemd_env(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # The dev session may itself be running under systemd; clear the
    # auto-detect env vars so the test is deterministic.
    monkeypatch.delenv("JOURNAL_STREAM", raising=False)
    monkeypatch.delenv("INVOCATION_ID", raising=False)
    _wire_logging(level="INFO", fmt="json", log_file=tmp_path / "x.log")
    journald = [h for h in logging.root.handlers if type(h).__name__ == "JournalHandler"]
    assert journald == []


def test_unknown_http_sink_url_is_logged_not_raised(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setenv(f"{_APP.upper()}_LOG_HTTP_SINK", "not a url")
    # Should not raise.
    _wire_logging(level="INFO", fmt="json", log_file=tmp_path / "x.log")


# ---------------------------------------------------------------------------
# Default log path resolution — covered by
# ``tests/public_surface/test_observability_as_user.py::TestPaths``
# (closes FEEDBACK PAPER-07; the v0.13 path helpers are public).
# ---------------------------------------------------------------------------
