"""End-to-end: bootstrap + run() + child output redaction.

Verifies the FULL chain documented in PUBLIC_API.md and the recipe:

  1. ``mgf.common.bootstrap()`` is wired (real handlers on root).
  2. A child process emits a known secret (Bearer token) on stdout.
  3. ``mgf.common.process.run()`` captures + streams it.
  4. The :class:`Redactor` wired by bootstrap scrubs the secret
     BEFORE the line lands in the captured ``stdout`` AND before
     the corresponding log record reaches handlers.

Failure modes this catches:

  - Streamer doesn't actually consult the redactor.
  - Streamer consults the redactor but applies it to the wrong
    field.
  - get_active_redactor() can't find the bootstrap-wired Redactor
    (a regression of the v0.23.0 public-property fix).
  - Redaction-after-capture: the scrubbed line reaches the logger
    but the captured string keeps the secret (would be a
    contract break: callers who feed result.stdout into other
    pipelines would leak).
"""

from __future__ import annotations

import logging
import sys

import pytest

pytestmark = pytest.mark.e2e


# A Bearer token is one of the built-in redaction patterns
# (_BEARER_RE in mgf.common.observability.redaction). When the
# Redactor is active, this string MUST be scrubbed in any log
# record / captured output.
_FAKE_SECRET_LINE = "Authorization: Bearer abcdef0123456789ABCDEF"
_REDACTED_TOKEN = "[REDACTED]"


@pytest.fixture
def bootstrapped_test_app(tmp_path, monkeypatch):
    """Bootstrap a real (test-shaped) AppContext for the duration of the test.

    Uses :func:`mgf.common.bootstrap_for_test` which wires the
    full observability stack with deterministic test defaults
    (WARNING-level logs, OTel disabled). Tears down on exit.
    """
    monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
    monkeypatch.setenv("HOME", str(tmp_path))
    from mgf.common import bootstrap_for_test

    ctx = bootstrap_for_test()
    try:
        yield ctx
    finally:
        ctx.shutdown()


def test_run_redacts_bearer_in_captured_stdout(
    bootstrapped_test_app,
) -> None:
    """The captured `result.stdout` MUST be redacted."""
    from mgf.common.process import run

    result = run(
        [
            sys.executable,
            "-c",
            f"print({_FAKE_SECRET_LINE!r})",
        ],
        check=True,
    )
    # The secret must NOT appear in captured output.
    assert "abcdef0123456789" not in result.stdout, (
        f"Secret leaked into ProcessResult.stdout: {result.stdout!r}"
    )
    # The redaction marker should be present.
    assert _REDACTED_TOKEN in result.stdout, (
        f"Expected redaction marker in stdout; got: {result.stdout!r}"
    )


def test_run_redacts_bearer_in_log_records(
    bootstrapped_test_app,
    caplog,
) -> None:
    """The streamed log record MUST also be redacted."""
    from mgf.common.process import run

    # Capture at INFO so we see the stdout line emitted by the
    # streamer (it logs at INFO for stdout).
    with caplog.at_level(logging.INFO, logger="mgf.process"):
        run(
            [
                sys.executable,
                "-c",
                f"print({_FAKE_SECRET_LINE!r})",
            ],
        )

    # Find the record emitted by the streamer for our secret line.
    matching = [
        r
        for r in caplog.records
        if r.name.startswith("mgf.process.") and ("Bearer" in r.message or "REDACTED" in r.message)
    ]
    assert matching, (
        "Expected at least one mgf.process.* record carrying the "
        f"Bearer line. Got records: {[r.message for r in caplog.records]}"
    )
    # Every matching record must NOT contain the raw secret.
    for r in matching:
        assert "abcdef0123456789" not in r.message, f"Secret leaked into log record: {r.message!r}"
        assert _REDACTED_TOKEN in r.message, (
            f"Expected redaction marker in log record; got: {r.message!r}"
        )


def test_run_redact_false_preserves_secret(
    bootstrapped_test_app,
) -> None:
    """``redact=False`` is the explicit opt-out (debug only).

    Pinning this is important: callers that pass ``redact=False``
    intentionally are accepting the risk; if the flag silently
    redacted anyway, they'd be confused about why their debug
    output is missing tokens.
    """
    from mgf.common.process import run

    result = run(
        [
            sys.executable,
            "-c",
            f"print({_FAKE_SECRET_LINE!r})",
        ],
        redact=False,
    )
    # With redact=False, the raw secret should reach captured stdout.
    assert "abcdef0123456789" in result.stdout


def test_redact_true_without_bootstrap_raises() -> None:
    """``redact=True`` without an active bootstrap raises RuntimeError.

    Pre-condition: NO bootstrapped_test_app fixture in this test —
    the global context is None. ``redact=True`` must surface a
    clear error rather than silently no-op (which would leak).
    """
    from mgf.common.process import run

    with pytest.raises(RuntimeError, match="bootstrap"):
        run(
            [sys.executable, "-c", "print('x')"],
            redact=True,
        )


def test_run_no_bootstrap_is_silent_no_redaction(
    monkeypatch,
    tmp_path,
) -> None:
    """Without bootstrap and with redact=None, no redaction happens — silently.

    This is the documented ergonomic: redaction is "default-on
    when bootstrap is wired; default-off otherwise." Test scripts
    that haven't bootstrapped MUST NOT raise just for using run().
    """
    # No bootstrap fixture; verify no AppContext active.
    import warnings

    from mgf.common import current_context

    # current_context() emits UnconfiguredWarning when no bootstrap;
    # squelch it for the precondition check.
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        assert current_context() is None

    from mgf.common.process import run

    result = run(
        [
            sys.executable,
            "-c",
            f"print({_FAKE_SECRET_LINE!r})",
        ],
    )
    # Secret reaches the result unredacted (no Redactor wired).
    assert "abcdef0123456789" in result.stdout
