"""Manual MCP daemon-kill contamination guard (#630).

Covers the four scenarios the acceptance criteria name — manual process kill,
sanctioned reconnect, stale-runtime restart, and a contaminated post-restart
mutation — across the pure guard, the durable marker, the MCP tools, the
pre-flight enforcement gate, and the final-report rules.
"""

from __future__ import annotations

import os
from unittest.mock import patch

import final_report_validator
import mcp_session_state
import runtime_recovery_guard as guard
import gitea_mcp_server as srv


AUTH_ENV = guard.OPERATOR_AUTHORIZATION_ENV


def _clear_marker(remote="prgs"):
    srv._clear_runtime_recovery_marker(remote=remote)


def teardown_function():
    _clear_marker()


def _marker(reason_class=guard.REASON_MANUAL_DAEMON_KILL, **overrides):
    record = guard.build_contamination_record(
        reason_class=reason_class,
        command_redacted="pkill -f mcp_server.py",
        session_id="prgs-author-1234-abcd",
        remote="prgs",
        role="author",
        detail="manual daemon kill",
    )
    record.update(overrides)
    return record


# ── AC1/AC2: manual process kill is detected and classified ──────────────────

def test_pkill_mcp_server_py_is_contamination():
    result = guard.classify_recovery_command("pkill -f mcp_server.py")
    assert result["process_kill"] is True
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    assert result["ambiguous"] is False


def test_equivalent_kill_forms_are_contamination():
    for command in (
        "pkill -f gitea_mcp_server",
        "pkill -f mcp",
        "pkill -9 -f mcp_server.py",
        "killall mcp_server",
        "sudo pkill -f mcp_server.py",
        "killall -9 mcp-server",
    ):
        result = guard.classify_recovery_command(command)
        assert result["contamination"] is True, command
        assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL, command


def test_broad_pattern_is_collateral_damage_contamination():
    result = guard.classify_recovery_command("pkill -f python")
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_BROAD_PROCESS_KILL
    assert "collateral" in " ".join(result["reasons"])


def test_kill_of_known_mcp_pid_is_contamination():
    result = guard.classify_recovery_command("kill -9 4242", mcp_pids=[4242, 99])
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    assert "4242" in " ".join(result["reasons"])


def test_kill_resolved_from_mcp_lookup_is_contamination():
    result = guard.classify_recovery_command("kill $(pgrep -f mcp_server.py)")
    assert result["contamination"] is True


def test_compound_command_detects_the_kill_half():
    result = guard.classify_recovery_command(
        "ps aux | grep mcp_server && pkill -f mcp_server.py"
    )
    assert result["contamination"] is True


# ── #787: background separator and subshell forms reach the classifier ───────

def test_background_separator_kill_is_contamination():
    result = guard.classify_recovery_command("sleep 1 & pkill -f mcp_server.py")
    assert result["process_kill"] is True
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    assert result["ambiguous"] is False


def test_subshell_wrapped_kill_is_contamination():
    result = guard.classify_recovery_command("(pkill -f mcp_server.py)")
    assert result["process_kill"] is True
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    assert result["ambiguous"] is False


def test_further_background_and_subshell_forms_are_contamination():
    for command in (
        "pkill -f mcp_server.py &",
        "( sudo pkill -f mcp_server.py )",
        "((pkill -f gitea_mcp_server))",
        "sleep 1 & killall mcp_server",
        "(ps aux | grep mcp_server) & pkill -f mcp_server.py",
    ):
        result = guard.classify_recovery_command(command)
        assert result["contamination"] is True, command
        assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL, command


def test_logical_operators_are_not_split_into_single_characters():
    # ``&&``/``||`` must still be consumed whole by the separator scan.
    assert guard._split_segments("a && b || c") == ["a", "b", "c"]
    assert guard._split_segments("a & b") == ["a", "b"]
    assert guard._split_segments("(a)") == ["a"]
    assert guard._split_segments("a; b\nc | d") == ["a", "b", "c", "d"]


# ── #789 F1: separators only separate outside quoted or escaped text ─────────

# The three commands the PR #789 review measured as regressions at head
# 6b58f04: each merely *mentions* the canonical kill string inside quotes.
F1_QUOTED_COMMANDS = (
    'git commit -m "block sleep 1 & pkill -f mcp_server.py as recovery"',
    'echo "docs: sleep 1 & pkill -f mcp_server.py is now detected"',
    'grep -rn "sleep 1 & pkill -f mcp_server.py" docs/',
)


def test_quoted_ampersand_examples_from_review_f1_are_not_kills():
    for command in F1_QUOTED_COMMANDS:
        result = guard.classify_recovery_command(command)
        assert result["process_kill"] is False, command
        assert result["contamination"] is False, command
        assert result["reason_class"] is None, command


def test_ampersand_inside_double_quotes_is_not_a_separator():
    assert guard._split_segments('echo "a & b"') == ['echo "a & b"']
    result = guard.classify_recovery_command(
        'echo "restart it: sleep 1 & pkill -f mcp_server.py"'
    )
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_ampersand_inside_single_quotes_is_not_a_separator():
    assert guard._split_segments("echo 'a & b'") == ["echo 'a & b'"]
    result = guard.classify_recovery_command(
        "git commit -m 'sleep 1 & pkill -f mcp_server.py stays quoted'"
    )
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_backslash_escaped_ampersand_is_not_a_separator():
    command = r"echo a \& pkill -f mcp_server.py"
    assert guard._split_segments(command) == [command]
    result = guard.classify_recovery_command(command)
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_backslash_does_not_escape_inside_single_quotes():
    # POSIX: a backslash is literal inside single quotes, so the closing quote
    # still closes and the following ``&`` is a genuinely active separator.
    command = r"echo 'a\' & pkill -f mcp_server.py"
    assert guard._split_segments(command) == [r"echo 'a\'", "pkill -f mcp_server.py"]
    assert guard.classify_recovery_command(command)["contamination"] is True


def test_quote_awareness_also_retires_the_pre_existing_semicolon_and_pipe_cases():
    # ``;`` and ``|`` misclassified quoted text before #787 as well. The fix is
    # the quote-unawareness, not the ``&`` instance the issue happens to name.
    for command in (
        'git commit -m "fix; pkill -f mcp_server.py"',
        'git commit -m "fix | pkill -f mcp_server.py"',
    ):
        result = guard.classify_recovery_command(command)
        assert result["process_kill"] is False, command
        assert result["contamination"] is False, command


# ── #789 F3: subshell stripping and redirection stay syntactically honest ────

def test_command_substitution_is_not_mangled_by_subshell_stripping():
    # Only a wrapper this call opened may be unwrapped; a ``)`` closing ``$(``
    # must survive intact.
    assert guard._strip_subshell("kill $(pgrep -f myapp)") == "kill $(pgrep -f myapp)"
    result = guard.classify_recovery_command("kill $(pgrep -f myapp)")
    assert result["contamination"] is False
    assert result["ambiguous"] is True


def test_redirection_is_not_treated_as_a_background_separator():
    assert guard._split_segments("a 2>&1") == ["a 2>&1"]
    assert guard._split_segments("a &> log") == ["a &> log"]
    assert guard._split_segments("pkill -f mcp_server.py 2>&1") == [
        "pkill -f mcp_server.py 2>&1"
    ]
    result = guard.classify_recovery_command("pkill -f mcp_server.py 2>&1")
    assert result["contamination"] is True
    assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL


# ── no false positives ───────────────────────────────────────────────────────

def test_read_only_inspection_is_not_a_kill():
    result = guard.classify_recovery_command("ps aux | grep mcp_server")
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_grepping_for_pkill_is_not_a_kill():
    result = guard.classify_recovery_command('grep -rn "pkill" native_mcp_preference.py')
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_unrelated_pkill_target_is_not_contamination():
    result = guard.classify_recovery_command("pkill -f my-dev-server")
    assert result["process_kill"] is True
    assert result["contamination"] is False
    assert result["ambiguous"] is False


def test_user_scoped_pkill_of_unrelated_app_is_not_contamination():
    # ``-u`` consumes ``mcpuser``; the surviving operand names no daemon (#787).
    result = guard.classify_recovery_command("pkill -u mcpuser -f myapp")
    assert result["process_kill"] is True
    assert result["contamination"] is False
    assert result["ambiguous"] is False


def test_commit_message_quoting_the_kill_string_is_not_a_kill():
    result = guard.classify_recovery_command(
        'git commit -m "block pkill -f mcp_server.py as workflow recovery"'
    )
    assert result["process_kill"] is False
    assert result["contamination"] is False


def test_bare_kill_of_unknown_pid_is_ambiguous_not_contamination():
    result = guard.classify_recovery_command("kill 31337")
    assert result["contamination"] is False
    assert result["ambiguous"] is True
    assert "not known MCP" in " ".join(result["reasons"])


def test_kill_without_pid_is_ambiguous():
    result = guard.classify_recovery_command("kill")
    assert result["contamination"] is False
    assert result["ambiguous"] is True


def test_empty_command_is_inert():
    result = guard.classify_recovery_command(None)
    assert result["command_present"] is False
    assert result["process_kill"] is False
    assert result["contamination"] is False


# ── sanctioned reconnect / restart ───────────────────────────────────────────

def test_sanctioned_reconnect_is_not_contamination():
    result = guard.classify_recovery_command(
        "/mcp reconnect then re-run gitea_whoami"
    )
    assert result["sanctioned_recovery"] is True
    assert result["contamination"] is False
    assert result["process_kill"] is False


def test_stale_runtime_restart_language_is_not_contamination():
    result = guard.classify_recovery_command(
        "runtime is stale against master; relaunch the IDE client so the "
        "namespaces restart"
    )
    assert result["sanctioned_recovery"] is True
    assert result["contamination"] is False


def test_sanctioned_language_never_excuses_an_actual_kill():
    result = guard.classify_recovery_command(
        "client reconnect did not help; pkill -f mcp_server.py"
    )
    assert result["sanctioned_recovery"] is True
    assert result["contamination"] is True


# ── operator authorization (env-only, never self-assertable) ─────────────────

def test_operator_authorization_absent_by_default():
    auth = guard.operator_authorization(env={})
    assert auth["authorized"] is False
    assert auth["reference"] is None
    assert auth["self_assertable"] is False


def test_operator_authorization_read_from_env_only():
    auth = guard.operator_authorization(env={AUTH_ENV: "CHG-4471 host maintenance"})
    assert auth["authorized"] is True
    assert auth["reference"] == "CHG-4471 host maintenance"
    assert auth["source"] == AUTH_ENV


def test_authorized_maintenance_is_not_contamination():
    assessment = guard.assess_recovery_command(
        "pkill -f mcp_server.py",
        env={AUTH_ENV: "CHG-4471"},
    )
    assert assessment["classification"]["contamination"] is True
    assert assessment["contaminated"] is False
    assert assessment["authorized_bypass"] is True
    assert assessment["remediation"] is None


def test_unauthorized_kill_is_contamination():
    assessment = guard.assess_recovery_command("pkill -f mcp_server.py", env={})
    assert assessment["contaminated"] is True
    assert assessment["authorized_bypass"] is False
    assert assessment["remediation"]


# ── redaction ────────────────────────────────────────────────────────────────

def test_marker_and_classification_redact_secrets():
    command = "GITEA_TOKEN=supersecretvalue pkill -f mcp_server.py"
    result = guard.classify_recovery_command(command)
    assert "supersecretvalue" not in result["redacted_command"]
    assert "GITEA_TOKEN=***" in result["redacted_command"]
    record = guard.build_contamination_record(
        reason_class=guard.REASON_MANUAL_DAEMON_KILL,
        command_redacted=result["redacted_command"],
    )
    assert "supersecretvalue" not in record["command_summary"]
    assert record["cleared_by_reconciler"] is False


# ── AC3: gate over the gated mutation set ────────────────────────────────────

def test_gate_blocks_gated_tasks():
    marker = _marker()
    for task in ("merge_pr", "review_pr", "close_issue", "create_pr", "submit_pr_review"):
        gate = guard.assess_contamination_gate(marker, task=task, actual_role="author")
        assert gate["block"] is True, task


def test_gate_allows_handoff_tasks():
    marker = _marker()
    for task in ("comment_issue", "lock_issue"):
        gate = guard.assess_contamination_gate(marker, task=task, actual_role="author")
        assert gate["block"] is False, task


def test_gate_exempts_reconciler():
    gate = guard.assess_contamination_gate(
        _marker(), task="merge_pr", actual_role="reconciler"
    )
    assert gate["block"] is False


def test_gate_allows_when_no_marker_or_cleared():
    assert guard.assess_contamination_gate(
        None, task="merge_pr", actual_role="author"
    )["block"] is False
    cleared = _marker(cleared_by_reconciler=True)
    assert guard.assess_contamination_gate(
        cleared, task="merge_pr", actual_role="author"
    )["block"] is False


def test_gate_error_message_names_the_issue():
    gate = guard.assess_contamination_gate(
        _marker(), task="merge_pr", actual_role="author"
    )
    assert "#630" in guard.format_contamination_gate_error(gate)


# ── scope item 4: final-report rules ─────────────────────────────────────────

def test_final_report_clean_claim_is_rejected():
    result = guard.assess_final_report_claim(
        "Runtime recovery: manual daemon kill occurred. Otherwise a clean session.",
        _marker(),
    )
    assert result["block"] is True
    assert result["clean_claim"] is True


def test_final_report_must_surface_the_contamination():
    result = guard.assess_final_report_claim(
        "All acceptance criteria met; tests pass.", _marker()
    )
    assert result["block"] is True
    assert result["surfaced"] is False


def test_final_report_that_surfaces_and_claims_nothing_clean_passes():
    result = guard.assess_final_report_claim(
        "This session performed a manual daemon kill of the MCP processes and "
        "is workflow-contaminated pending a reconciler audit.",
        _marker(),
    )
    assert result["block"] is False
    assert result["surfaced"] is True


def test_final_report_unconstrained_without_marker():
    result = guard.assess_final_report_claim("clean session", None)
    assert result["block"] is False
    assert result["contaminated"] is False


def test_validator_blocks_clean_claim_while_contaminated():
    out = final_report_validator.assess_final_report_validator(
        "Merged the PR. No contamination in this session.",
        "merge_pr",
        runtime_recovery_marker=_marker(),
    )
    assert out["blocked"] is True
    assert any(
        finding["rule_id"] == "shared.runtime_recovery_contamination"
        for finding in out["findings"]
    )


def test_validator_default_is_unchanged_without_marker():
    out = final_report_validator.assess_final_report_validator(
        "Merged the PR. No contamination in this session.",
        "merge_pr",
    )
    assert "runtime_recovery_contamination" not in out["checks"]
    assert not any(
        finding["rule_id"] == "shared.runtime_recovery_contamination"
        for finding in out["findings"]
    )


# ── durable marker must outlive the session TTL ──────────────────────────────

def test_contamination_marker_is_recovery_critical():
    assert (
        mcp_session_state.KIND_RUNTIME_RECOVERY_CONTAMINATION
        in mcp_session_state.RECOVERY_CRITICAL_KINDS
    )


# ── server wiring: record tool ───────────────────────────────────────────────

def test_record_tool_marks_manual_daemon_kill():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    assert res["contaminated"] is True
    assert res["marked"] is True
    assert res["marker"]["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    loaded = srv._load_runtime_recovery_marker("prgs")
    assert loaded is not None
    assert "mcp_server.py" in loaded["command_summary"]


def test_record_tool_marks_background_separator_kill():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="sleep 1 & pkill -f mcp_server.py", remote="prgs"
    )
    assert res["contaminated"] is True
    assert res["marked"] is True
    assert res["marker"]["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    loaded = srv._load_runtime_recovery_marker("prgs")
    assert loaded is not None
    assert "mcp_server.py" in loaded["command_summary"]


def test_record_tool_marks_subshell_wrapped_kill():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="(pkill -f mcp_server.py)", remote="prgs"
    )
    assert res["contaminated"] is True
    assert res["marked"] is True
    assert res["marker"]["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
    loaded = srv._load_runtime_recovery_marker("prgs")
    assert loaded is not None
    assert "mcp_server.py" in loaded["command_summary"]


def test_record_tool_does_not_mark_a_quoted_mention_of_the_kill_string():
    # The marker is what fails review/merge/close closed and only a reconciler
    # may clear it, so a quoted mention must never create one (PR #789 F1).
    for command in F1_QUOTED_COMMANDS:
        _clear_marker()
        res = srv.gitea_record_daemon_process_kill_attempt(command=command, remote="prgs")
        assert res["contaminated"] is False, command
        assert res["marked"] is False, command
        assert srv._load_runtime_recovery_marker("prgs") is None, command


def test_record_tool_marks_broad_sweep():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f python", remote="prgs"
    )
    assert res["contaminated"] is True
    assert res["marker"]["reason_class"] == guard.REASON_BROAD_PROCESS_KILL


def test_record_tool_marks_known_pid_kill():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="kill -9 4242", mcp_pids=["4242"], remote="prgs"
    )
    assert res["contaminated"] is True
    assert res["marked"] is True


def test_record_tool_does_not_mark_inspection():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="ps aux | grep mcp_server", remote="prgs"
    )
    assert res["contaminated"] is False
    assert res["marked"] is False
    assert srv._load_runtime_recovery_marker("prgs") is None


def test_record_tool_does_not_mark_sanctioned_reconnect():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="/mcp reconnect", remote="prgs"
    )
    assert res["contaminated"] is False
    assert res["marked"] is False
    assert srv._load_runtime_recovery_marker("prgs") is None


def test_record_tool_mark_false_is_read_only():
    _clear_marker()
    res = srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs", mark=False
    )
    assert res["contaminated"] is True
    assert res["marked"] is False
    assert srv._load_runtime_recovery_marker("prgs") is None


def test_record_tool_honours_operator_authorization():
    _clear_marker()
    with patch.dict(os.environ, {AUTH_ENV: "CHG-4471"}):
        res = srv.gitea_record_daemon_process_kill_attempt(
            command="pkill -f mcp_server.py", remote="prgs"
        )
    assert res["authorized_bypass"] is True
    assert res["contaminated"] is False
    assert res["marked"] is False
    assert srv._load_runtime_recovery_marker("prgs") is None


# ── server wiring: audit tool ────────────────────────────────────────────────

def test_audit_inspect_reports_marker():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    out = srv.gitea_audit_runtime_recovery_contamination(action="inspect", remote="prgs")
    assert out["contaminated"] is True
    assert out["read_only"] is True


def test_audit_clear_refused_for_non_reconciler():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    with patch.object(srv, "_actual_profile_role", return_value="author"):
        out = srv.gitea_audit_runtime_recovery_contamination(
            action="clear", remote="prgs"
        )
    assert out["success"] is False
    assert out["reasons"]
    assert srv._load_runtime_recovery_marker("prgs") is not None


def test_audit_clear_allowed_for_reconciler():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    identity = srv._runtime_recovery_profile_identity()
    with patch.object(srv, "_actual_profile_role", return_value="reconciler"):
        out = srv.gitea_audit_runtime_recovery_contamination(
            action="clear", remote="prgs", profile_identity=identity
        )
    assert out["success"] is True
    assert srv._load_runtime_recovery_marker("prgs") is None


def test_audit_unknown_action_fails_closed():
    out = srv.gitea_audit_runtime_recovery_contamination(action="nuke", remote="prgs")
    assert out["success"] is False
    assert out["performed"] is False


# ── AC3/AC4: contaminated post-restart mutation fails closed ─────────────────

def _force_gate_env():
    return patch.dict(os.environ, {"GITEA_TEST_FORCE_RUNTIME_CONTAMINATION": "1"})


def test_gate_blocks_mutations_after_manual_kill_and_restart():
    _clear_marker()
    # The session kills the daemons, the IDE respawns them, and the session then
    # attempts the mutations #601 was closed with.
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    with _force_gate_env(), patch.object(srv, "_actual_profile_role", return_value="author"):
        for task in ("merge_pr", "review_pr", "close_issue", "create_pr"):
            try:
                srv._enforce_runtime_recovery_contamination_gate(task, "prgs")
                raised = False
            except RuntimeError as exc:
                raised = True
                assert "#630" in str(exc)
            assert raised, task


def test_gate_allows_handoff_comment_when_contaminated():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    with _force_gate_env(), patch.object(srv, "_actual_profile_role", return_value="author"):
        srv._enforce_runtime_recovery_contamination_gate("comment_issue", "prgs")
        srv._enforce_runtime_recovery_contamination_gate("lock_issue", "prgs")


def test_gate_exempts_reconciler_audit():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="pkill -f mcp_server.py", remote="prgs"
    )
    with _force_gate_env(), patch.object(srv, "_actual_profile_role", return_value="reconciler"):
        srv._enforce_runtime_recovery_contamination_gate("merge_pr", "prgs")


def test_gate_noop_after_sanctioned_restart_only():
    _clear_marker()
    srv.gitea_record_daemon_process_kill_attempt(
        command="/mcp reconnect", remote="prgs"
    )
    with _force_gate_env(), patch.object(srv, "_actual_profile_role", return_value="author"):
        srv._enforce_runtime_recovery_contamination_gate("merge_pr", "prgs")
