"""v0.23.0 polish: regression tests for restart-after-failure + OTel pinning."""

from __future__ import annotations

import socket
import sys
import textwrap
from contextlib import nullcontext

import pytest

from mgf.common.exceptions import ServiceNotReadyError


def _free_port() -> int:
    s = socket.socket()
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]
    s.close()
    return port


_TINY_LISTENER = textwrap.dedent(
    """
    import socket, sys
    sock = socket.socket(); sock.bind(("127.0.0.1", PORT)); sock.listen(1)
    print("READY", flush=True)
    while True:
        try:
            conn, _ = sock.accept(); conn.close()
        except KeyboardInterrupt:
            break
    """,
).strip()


# ---------------------------------------------------------------------------
# Restart after failure
# ---------------------------------------------------------------------------


class TestRestartAfterFailure:
    def test_service_can_be_started_again_after_failed_start(self) -> None:
        """A Service whose first start() raised can have start() called again.

        This is the natural "fix the cause and retry" pattern for
        test fixtures that catch ServiceNotReadyError, log it, and try
        again with adjusted args.
        """
        from mgf.common.process import PortReady, Service

        port = _free_port()
        # First start: bad probe (port that won't bind in time).
        bad_probe = PortReady("127.0.0.1", 1, connect_timeout_s=0.05)
        svc = Service(
            [sys.executable, "-c", "import time; time.sleep(60)"],
            ready=bad_probe,
            timeout_s=0.2,
            poll_interval_s=0.05,
            kill_grace_s=0.5,
        )
        with pytest.raises(ServiceNotReadyError):
            svc.start()
        # First child is dead.
        assert not svc.is_running

        # Construct a fresh service with the same name but a
        # working command + good probe. (The test models the
        # consumer pattern of "try again with adjusted args.")
        svc2 = Service(
            [sys.executable, "-c", _TINY_LISTENER.replace("PORT", str(port))],
            ready=PortReady("127.0.0.1", port, connect_timeout_s=0.5),
            timeout_s=5.0,
            poll_interval_s=0.05,
            kill_grace_s=1.0,
        )
        try:
            svc2.start()
            assert svc2.is_running
            # And distinct command_id from the first.
            assert svc2.command_id != svc.command_id
        finally:
            svc2.stop()


# ---------------------------------------------------------------------------
# OTel span gating — bootstrap-aware no-op
# ---------------------------------------------------------------------------


class TestOtelSpanGating:
    def test_no_bootstrap_no_unconfigured_warning_from_run(self) -> None:
        """Without bootstrap, run() must NOT trigger UnconfiguredWarning.

        Pre-v0.23 path: ``operation_span("process.run.echo")`` calls
        ``app_name()`` to name the tracer, which raises
        UnconfiguredWarning when no bootstrap is active. v0.23 gates
        the span on bootstrap state — nullcontext when no AppContext.
        """
        import warnings

        from mgf.common.process import run

        # Strict warning capture: any UnconfiguredWarning would be
        # caught.
        with warnings.catch_warnings():
            warnings.simplefilter("error")
            r = run(["echo", "no-bootstrap"])
        assert r.returncode == 0
        assert r.stdout == "no-bootstrap"

    def test_maybe_operation_span_returns_nullcontext_without_bootstrap(
        self,
    ) -> None:
        """Direct unit test of the helper."""
        from mgf.common.process._run import _maybe_operation_span

        cm = _maybe_operation_span("process.run.x", command_id="abc")
        # nullcontext is the documented "no-bootstrap" path.
        assert isinstance(cm, type(nullcontext()))

    def test_maybe_operation_span_returns_real_span_when_bootstrapped(
        self,
        tmp_path,
        monkeypatch,
    ) -> None:
        """When bootstrap IS active, the real span is returned.

        We don't assert it's an OTel span object (it might be a
        contextlib._GeneratorContextManager wrapping the generator)
        — only that it's distinct from nullcontext, which proves
        the helper took the "real path."
        """
        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
        monkeypatch.setenv("HOME", str(tmp_path))
        from mgf.common import bootstrap_for_test
        from mgf.common.process._run import _maybe_operation_span

        ctx = bootstrap_for_test()
        try:
            cm = _maybe_operation_span("process.run.x", command_id="abc")
            assert not isinstance(cm, type(nullcontext()))
        finally:
            ctx.shutdown()


# ---------------------------------------------------------------------------
# v0.23.0 misc — public redactor accessor + Service repr
# ---------------------------------------------------------------------------


class TestPublicRedactorProperty:
    def test_json_formatter_exposes_public_redactor(self) -> None:
        from mgf.common.observability.json_formatter import JsonFormatter
        from mgf.common.observability.redaction import Redactor

        r = Redactor()
        f = JsonFormatter(redactor=r)
        # v0.23.0: public ``redactor`` property is the stable accessor.
        assert f.redactor is r
        # ``_redactor`` is still there for back-compat (and self-use).
        assert f._redactor is r

    def test_text_formatter_exposes_public_redactor(self) -> None:
        from mgf.common.observability.redaction import Redactor
        from mgf.common.observability.text_formatter import TextFormatter

        r = Redactor()
        f = TextFormatter(redactor=r)
        assert f.redactor is r


class TestServiceRepr:
    def test_repr_before_start(self) -> None:
        from mgf.common.process import PortReady, Service

        svc = Service(
            ["sleep", "1"],
            name="my-svc",
            ready=PortReady("127.0.0.1", 1),
        )
        rep = repr(svc)
        assert "my-svc" in rep
        assert "sleep" in rep
        assert "not-started" in rep
        assert "pid=-" in rep

    def test_repr_running_includes_pid(self) -> None:
        from mgf.common.process import LogLineReady, Service

        port = _free_port()
        with Service(
            [sys.executable, "-c", _TINY_LISTENER.replace("PORT", str(port))],
            name="repr-test",
            ready=LogLineReady(r"^READY$", stream="stdout"),
            timeout_s=5.0,
            poll_interval_s=0.05,
            kill_grace_s=1.0,
        ) as svc:
            rep = repr(svc)
            assert "running" in rep
            assert f"pid={svc.pid}" in rep
        # After stop:
        rep = repr(svc)
        assert "exited" in rep
