"""User-perspective tests for ``mgf.common.observability``.

Surfaces exercised:
  - :func:`get_logger`
  - :func:`operation_span` (no-op safe when OTel disabled)
  - :class:`Redactor` + :data:`DEFAULT_REDACTION_KEYS`
  - Span helpers: :func:`get_current_span`, :func:`set_attribute`,
    :func:`add_event`, :func:`set_status`
  - Path helpers: :func:`default_log_path`, :func:`default_crash_dir`,
    :func:`default_state_dir`
  - In-memory log capture via ``bootstrap_for_test(capture_logs=True)``
"""

from __future__ import annotations

import logging
import sys
from pathlib import Path

import pytest

import mgf.common  # noqa: TC001 — imported as a user would, not hidden under TYPE_CHECKING
from mgf.common.observability import (
    DEFAULT_REDACTION_KEYS,
    Redactor,
    add_event,
    default_crash_dir,
    default_log_path,
    default_state_dir,
    get_current_span,
    get_logger,
    operation_span,
    set_attribute,
)


class TestLogger:
    def test_get_logger_returns_stdlib_logger(self) -> None:
        log = get_logger("my_app.test")
        assert isinstance(log, logging.Logger)
        assert log.name == "my_app.test"

    def test_get_logger_no_name_returns_root(self) -> None:
        log = get_logger()
        assert isinstance(log, logging.Logger)


class TestOperationSpan:
    def test_no_op_safe_when_otel_disabled(
        self,
        app_ctx: mgf.common.AppContext,
    ) -> None:
        """``operation_span`` works when OTel is disabled (the
        testing-preset default). It yields a usable context
        manager that does nothing."""
        with operation_span("test.op", attr="value"):
            # Inside: span helpers are no-op safe too
            set_attribute("key", "v")
            add_event("checkpoint")
        # No exception; this is the contract.

    def test_get_current_span_returns_something_or_none(
        self,
        app_ctx: mgf.common.AppContext,
    ) -> None:
        # Outside any span, it's the no-op span (or None when OTel off).
        # Either way, no exception.
        result = get_current_span()
        # Don't assert on the value — the contract is "doesn't raise".
        _ = result


class TestRedactor:
    def test_default_redaction_keys_set_is_a_frozenset(self) -> None:
        # ``DEFAULT_REDACTION_KEYS`` is the canonical built-in set
        # consumers can inspect / extend. Frozenset shape pinned.
        assert isinstance(DEFAULT_REDACTION_KEYS, frozenset)
        assert len(DEFAULT_REDACTION_KEYS) > 0
        # API observation: Redactor itself does NOT expose the
        # combined (default + extra) key set as a public attribute.
        # Consumers wanting to introspect must compose
        # `DEFAULT_REDACTION_KEYS | redactor.extra_keys` manually.
        # Worth filing as a 🟢 if a consumer asks for it.

    def test_redact_scrubs_default_key_value(self) -> None:
        """Behavioural pin — every key in the default set scrubs
        its value when the Redactor walks a dict."""
        r = Redactor.default()
        for key in DEFAULT_REDACTION_KEYS:
            payload = {key: "real-secret-do-not-leak"}
            r.redact(payload)
            assert payload[key] != "real-secret-do-not-leak", (
                f"default key {key!r} did not scrub the value"
            )

    def test_redact_string_scrubs_openai_key(self) -> None:
        """OpenAI-style key pattern: ``\\bsk-[A-Za-z0-9]{32,}\\b``
        — needs ≥32 alphanumeric chars after ``sk-`` to match."""
        r = Redactor.default()
        # 36 chars after sk- — comfortably above the regex floor.
        secret = "sk-" + "a" * 36
        scrubbed = r.redact_string(f"token={secret}")
        assert secret not in scrubbed


class TestLogCapture:
    """Proves the user-facing log-capture flow works end-to-end."""

    def test_captures_warning_level(
        self,
        captured_app_ctx: mgf.common.AppContext,
    ) -> None:
        get_logger("test.observability").warning("captured")
        assert captured_app_ctx.captured_logs is not None
        messages = [r.getMessage() for r in captured_app_ctx.captured_logs]
        assert "captured" in messages


class TestPaths:
    """Public on-disk path helpers (closes FEEDBACK PAPER-07).

    The library writes logs and crash reports to well-known
    locations under ``$XDG_STATE_HOME`` (or ``~/.local/state``).
    Consumers building observability-adjacent UI need a public
    way to ask "where do my logs live?" without reaching into
    private helpers.
    """

    def test_default_state_dir_honours_xdg_state_home(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
        path = default_state_dir()
        # ``app_ctx`` bootstraps under app_name="test-app".
        assert path == tmp_path / "test-app"

    def test_default_state_dir_falls_back_to_home_local_state(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.delenv("XDG_STATE_HOME", raising=False)
        monkeypatch.setenv("HOME", str(tmp_path))
        path = default_state_dir()
        assert path == tmp_path / ".local" / "state" / "test-app"

    def test_default_log_path_lives_under_state_dir_logs(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
        path = default_log_path()
        assert path == tmp_path / "test-app" / "logs" / "test-app.log"

    def test_default_crash_dir_lives_under_state_dir_crashes(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
        path = default_crash_dir()
        assert path == tmp_path / "test-app" / "crashes"

    def test_paths_are_lazy_no_resolution_at_import_time(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Re-resolving after monkeypatching XDG_STATE_HOME picks
        up the new value — proves the helpers don't cache (rule
        CF-04). Load-bearing for per-test isolation."""
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "first"))
        first = default_log_path()
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "second"))
        second = default_log_path()
        assert first != second
        assert second.is_relative_to(tmp_path / "second")


class TestPathsCrossPlatform:
    """RA-29: ``default_state_dir`` composes with
    :func:`mgf.common.config.platform_dir` so logs and crash
    reports land at the per-OS idiomatic location. Previously the
    module hardcoded ``~/.local/state`` for every platform —
    Windows and macOS consumers would write to a Linux-shaped
    path that doesn't match their host conventions.

    The Linux behaviour is covered by the ``TestPaths`` tests
    above (XDG_STATE_HOME + ~/.local/state fallback). These tests
    pin the macOS and Windows branches via ``os.name`` /
    ``sys.platform`` monkeypatching, same pattern as
    ``tests/unit/config/test_paths.py``.
    """

    def test_macos_state_dir_under_application_support(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """On macOS, state lives under
        ``~/Library/Application Support/<app>``."""
        monkeypatch.setattr("os.name", "posix")
        monkeypatch.setattr("sys.platform", "darwin")
        monkeypatch.delenv("XDG_STATE_HOME", raising=False)
        monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path))
        path = default_state_dir()
        assert path == tmp_path / "Library" / "Application Support" / "test-app"

    def test_windows_state_dir_falls_back_to_userprofile(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """On Windows without LOCALAPPDATA, state falls back to
        ``<home>/AppData/Local/<app>``.

        We exercise the ``Path.home`` fallback branch (not the
        LOCALAPPDATA-set branch) because monkeypatching
        ``os.name='nt'`` makes ``Path(env_value)`` return a
        ``WindowsPath``, which Python's pathlib refuses to
        instantiate on a Linux host — the LOCALAPPDATA branch is
        already covered at the unit level by
        ``tests/unit/config/test_paths.py::test_windows_state_uses_localappdata``.
        This test pins the COMPOSITION through ``default_state_dir``:
        ``platform_dir('state') / app_name()`` produces the right
        Windows-shaped path."""
        if sys.version_info < (3, 12):
            pytest.skip("WindowsPath cannot be instantiated on POSIX before Python 3.12")
        monkeypatch.setattr("os.name", "nt")
        monkeypatch.delenv("XDG_STATE_HOME", raising=False)
        monkeypatch.delenv("LOCALAPPDATA", raising=False)
        monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "user"))
        path = default_state_dir()
        # str/replace normalisation for the same Windows-path-on-
        # Linux reason as tests/unit/config/test_paths.py.
        assert str(path).replace("\\", "/") == str(
            tmp_path / "user" / "AppData" / "Local" / "test-app"
        ).replace("\\", "/")

    def test_xdg_state_home_wins_on_macos(
        self,
        app_ctx: mgf.common.AppContext,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Rule CF-09: XDG_STATE_HOME is the universal override.
        Power users on macOS can opt into XDG layout. (Windows-side
        XDG override is unit-tested in test_paths.py — we don't
        re-test through default_state_dir here because the
        WindowsPath instantiation rule rears up.)"""
        monkeypatch.setattr("os.name", "posix")
        monkeypatch.setattr("sys.platform", "darwin")
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "xdg"))
        path = default_state_dir()
        assert path == tmp_path / "xdg" / "test-app"
