"""Tests for ``mgf.common.pytest_plugin``.

Strategy: most fixtures are exercised inside a pytester sub-session so
the auto-registration via entry point is tested end-to-end. Direct
unit tests cover the helpers (``decode_json_logs``) that don't need a
nested pytest run.
"""

from __future__ import annotations

import logging

import pytest

from mgf.common.pytest_plugin import decode_json_logs

# Enable pytester for sub-session tests.
pytest_plugins = ["pytester"]


@pytest.fixture
def configured_pytester(pytester: pytest.Pytester) -> pytest.Pytester:
    """``pytester`` with config that pytest-asyncio + pytest-cov accept.

    Sub-pytest sessions inherit the parent's loaded plugins (asyncio,
    cov, etc.), but those plugins demand config in the sub-session's
    own pyproject.toml. We seed a minimal one here.
    """
    pytester.makepyprojecttoml(
        """
        [tool.pytest.ini_options]
        asyncio_mode = "strict"
        asyncio_default_fixture_loop_scope = "function"
        """
    )
    return pytester


# ---------------------------------------------------------------------------
# decode_json_logs — direct unit tests.
# ---------------------------------------------------------------------------


class TestDecodeJsonLogs:
    def test_returns_empty_list_on_empty_caplog(self, caplog: pytest.LogCaptureFixture) -> None:
        assert decode_json_logs(caplog) == []

    def test_decodes_json_records(self, caplog: pytest.LogCaptureFixture) -> None:
        with caplog.at_level(logging.WARNING):
            logging.warning('{"event": "first", "level": "warn"}')
            logging.warning('{"event": "second", "level": "warn"}')
        decoded = decode_json_logs(caplog)
        assert len(decoded) == 2
        assert decoded[0]["event"] == "first"
        assert decoded[1]["event"] == "second"

    def test_skips_non_json_records(self, caplog: pytest.LogCaptureFixture) -> None:
        with caplog.at_level(logging.WARNING):
            logging.warning("plain text")
            logging.warning('{"valid": "json"}')
            logging.warning("more plain text")
        decoded = decode_json_logs(caplog)
        assert len(decoded) == 1
        assert decoded[0] == {"valid": "json"}


# ---------------------------------------------------------------------------
# mgf_settings — exercised via pytester sub-session.
# ---------------------------------------------------------------------------


class TestPluginAutoRegistration:
    """Confirm fixtures auto-register without explicit pytest_plugins."""

    def test_fixtures_visible_via_entry_point(self, configured_pytester: pytest.Pytester) -> None:
        # Tiny test file using mgf_settings — pytester runs it in a
        # sub-pytest session that auto-discovers the entry-point plugin
        # (the parent process has installed mgf-common, so the plugin
        # is registered).
        configured_pytester.makepyfile(
            """
            from mgf.common.settings import MgfSettings

            def test_mgf_settings_fixture(mgf_settings):
                assert isinstance(mgf_settings, MgfSettings)
                # testing() preset has WARNING-level logging.
                assert mgf_settings.logging.level == "WARNING"
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=1)

    def test_each_test_gets_fresh_settings(self, configured_pytester: pytest.Pytester) -> None:
        configured_pytester.makepyfile(
            """
            def test_one(mgf_settings):
                mgf_settings.logging.level = "DEBUG"

            def test_two(mgf_settings):
                # Fresh instance; previous test's mutation isn't visible.
                assert mgf_settings.logging.level == "WARNING"
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=2)


class TestPerfCapture:
    """TS-28 — the pytest plugin writes per-test perf artifacts under
    ``--mgf-perf`` / ``MGF_PERF`` and is silent otherwise."""

    def test_artifact_written_with_functions_and_spans(
        self, configured_pytester: pytest.Pytester
    ) -> None:
        import json

        perf_dir = configured_pytester.path / "perfout"
        configured_pytester.makepyfile(
            """
            from mgf.common.testing import perf_span

            def _work():
                total = 0
                for _ in range(50000):
                    total += 1
                return total

            def test_with_spans():
                with perf_span("phase-a"):
                    _work()
                with perf_span("phase-b"):
                    _work()
                assert True
            """
        )
        result = configured_pytester.runpytest("-q", "--mgf-perf", f"--mgf-perf-dir={perf_dir}")
        result.assert_outcomes(passed=1)
        artifacts = list(perf_dir.glob("*.json"))
        assert len(artifacts) == 1
        data = json.loads(artifacts[0].read_text(encoding="utf-8"))
        assert data["test_id"].endswith("::test_with_spans")
        assert [s["label"] for s in data["spans"]] == ["phase-a", "phase-b"]
        assert data["functions"], "cProfile should attribute some frames"
        assert data["total_ms"] >= 0.0

    def test_silent_by_default(self, configured_pytester: pytest.Pytester) -> None:
        perf_dir = configured_pytester.path / "perfdefault"
        configured_pytester.makepyfile(
            """
            def test_noop():
                assert True
            """
        )
        result = configured_pytester.runpytest("-q")  # no --mgf-perf
        result.assert_outcomes(passed=1)
        assert not perf_dir.exists()

    def test_env_var_enables_capture(
        self, configured_pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        perf_dir = configured_pytester.path / "perfenv"
        monkeypatch.setenv("MGF_PERF", "1")
        monkeypatch.setenv("MGF_PERF_DIR", str(perf_dir))
        configured_pytester.makepyfile(
            """
            def test_env():
                assert True
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=1)
        assert list(perf_dir.glob("*.json"))

    def test_perf_capture_does_not_change_failing_outcome(
        self, configured_pytester: pytest.Pytester
    ) -> None:
        # A failing test under --mgf-perf still fails (perf is fail-open
        # and out-of-band).
        perf_dir = configured_pytester.path / "perffail"
        configured_pytester.makepyfile(
            """
            def test_boom():
                assert 1 == 2
            """
        )
        result = configured_pytester.runpytest("-q", "--mgf-perf", f"--mgf-perf-dir={perf_dir}")
        result.assert_outcomes(failed=1)


class TestMgfContextFixture:
    def test_yields_bootstrapped_context(self, configured_pytester: pytest.Pytester) -> None:
        configured_pytester.makepyfile(
            """
            from mgf.common._lifecycle import AppContext

            def test_ctx(mgf_context):
                assert isinstance(mgf_context, AppContext)
                # Identity is set.
                assert mgf_context.app_name == "test-app"
                assert mgf_context.app_version == "0.0.1"
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=1)

    def test_shutdown_runs_at_test_end(self, configured_pytester: pytest.Pytester) -> None:
        # Two tests; if the first didn't shut down, the second's
        # bootstrap would either no-op or warn about double-bootstrap.
        # Confirm both tests run cleanly.
        configured_pytester.makepyfile(
            """
            def test_first(mgf_context):
                assert mgf_context is not None

            def test_second(mgf_context):
                assert mgf_context is not None
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=2)


class TestRedactionRecorder:
    def test_records_redaction_calls(self, configured_pytester: pytest.Pytester) -> None:
        configured_pytester.makepyfile(
            """
            from mgf.common.observability import Redactor

            def test_redaction_recorded(redaction_recorder):
                r = Redactor()
                payload = {"Authorization": "Bearer secret-xyz", "user": "alice"}
                r.redact(payload)
                # The recorder captured the call.
                assert len(redaction_recorder) == 1
                assert "Authorization" in redaction_recorder[0]["input_keys"]
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=1)

    def test_recorder_resets_between_tests(self, configured_pytester: pytest.Pytester) -> None:
        configured_pytester.makepyfile(
            """
            from mgf.common.observability import Redactor

            def test_one(redaction_recorder):
                Redactor().redact({"k": "v"})
                assert len(redaction_recorder) == 1

            def test_two(redaction_recorder):
                # Fresh recorder for second test.
                assert len(redaction_recorder) == 0
            """
        )
        result = configured_pytester.runpytest("-q")
        result.assert_outcomes(passed=2)


class TestOtelProvider:
    """Session-scoped provider fixture — skips if [observability] missing."""

    def test_skips_when_observability_not_installed(
        self, configured_pytester: pytest.Pytester
    ) -> None:
        # Verify the skip path: simulate by mocking the import inside
        # a sub-test. If [observability] IS installed in the dev env,
        # this test simply confirms the fixture yields None.
        configured_pytester.makepyfile(
            """
            def test_provider_or_skipped(mgf_otel_provider):
                # Either the fixture skipped, or it yielded None — both
                # are valid; the test's job is to NOT crash.
                assert mgf_otel_provider is None
            """
        )
        result = configured_pytester.runpytest("-q")
        # Outcome is either passed or skipped depending on extras.
        passed = result.parseoutcomes().get("passed", 0)
        skipped = result.parseoutcomes().get("skipped", 0)
        assert passed + skipped == 1

    def test_provider_persists_across_tests_in_session(
        self, configured_pytester: pytest.Pytester
    ) -> None:
        configured_pytester.makepyfile(
            """
            def test_one(mgf_otel_provider):
                pass

            def test_two(mgf_otel_provider):
                # Same session → same provider (set_tracer_provider is
                # one-shot, so the second call must defer to the first).
                pass
            """
        )
        result = configured_pytester.runpytest("-q")
        passed = result.parseoutcomes().get("passed", 0)
        skipped = result.parseoutcomes().get("skipped", 0)
        assert passed + skipped == 2


class TestMgfContextWithCapture:
    """Combined log + span capture; depends on ``mgf_otel_provider``.

    NOTE: ``mgf_context_with_capture`` cannot be unit-tested via
    pytester from within mgf-common's own test suite, because the
    parent suite has already claimed the OTel ``TracerProvider`` slot
    (one-shot per process). The sub-session sees a ``ProxyTracerProvider``
    and ``_wire_span_capture`` correctly rejects it. A real consumer
    that installs mgf-common into a fresh process gets the working
    flow. Validated end-to-end via the consumer-side integration once
    a consumer adopts the plugin.
    """


# ---------------------------------------------------------------------------
# Smoke: can pytest discover the plugin via its entry point?
# ---------------------------------------------------------------------------


def test_plugin_module_imports() -> None:
    """The plugin module must import without error."""
    import mgf.common.pytest_plugin as plugin

    expected = {
        "decode_json_logs",
        "mgf_context",
        "mgf_context_with_capture",
        "mgf_otel_provider",
        "mgf_settings",
        "redaction_recorder",
    }
    assert expected.issubset(set(plugin.__all__))


def test_plugin_in_pytest_entry_points() -> None:
    """The plugin is declared as a pytest11 entry point."""
    from importlib.metadata import entry_points

    eps = entry_points(group="pytest11")
    names = {ep.name for ep in eps}
    assert "mgf_common" in names, (
        f"mgf_common not in pytest11 entry points; install with `pip install -e .`. "
        f"Found: {sorted(names)}"
    )
