"""task-2736-r1 — PR #209 micro-fix regression tests (Gemini unresolved 3).

Scope = exactly the 3 real defects approved for THIS_PR micro-fix:
  ① guard.py  _HIGH_RISK_CMD_PATTERNS — `.*` must cross newlines (re.DOTALL) so
     a multiline / line-continuation high-risk command cannot bypass the block.
  ② rules.py  rule_4 — must extract the command via _get_command so the `cmd`
     input key (supported by every other rule) cannot bypass the check.
  ③ rules.py  rule_5 git-push branch — must be selected by rule identity, not by
     re.search over `pat.pattern` (the raw regex source, where `\\s+` is literal
     text that never matches → the push-specific message branch was dead).

Required chair test matrix (instruction §4):
  1. multiline high-risk command DENY
  2. cmd-key input also triggers rule_4
  3. git push dedicated message branch actually runs
  4. existing high-risk DENY / benign ALLOW regression
  5. degraded fail-closed regression
  6. expected_files outside diff == 0
  7. existing regression unaffected

Isolation: ANU_V36_HARNESS_TEST_MODE=1 so JSONL writes go to /tmp; no real tool
is ever executed — only mock payloads / string evaluation.
"""
from __future__ import annotations

import os
import subprocess
from pathlib import Path

import pytest

_REPO_ROOT = Path(__file__).resolve().parents[2]


@pytest.fixture(autouse=True)
def _test_mode_env(monkeypatch):
    monkeypatch.setenv("ANU_V36_HARNESS_TEST_MODE", "1")
    # rule_4 keys off this env; ensure the "no collector" branch is exercised.
    monkeypatch.delenv("ANU_CALLBACK_COLLECTOR_RUNNING", raising=False)
    yield


# ---------------------------------------------------------------------------
# §4.1 — fix ① multiline high-risk command DENY (re.DOTALL)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
    "cmd",
    [
        # line-continuation rm -rf of a protected root
        "rm -rf \\\n  /home/jay/workspace",
        "rm -fr \\\n\t/home/jay/.claude",
        # force-push split across lines
        "git push origin \\\n  --force main",
        "git push \\\n  --force-with-lease origin feature",
        # delete main via push, multiline
        "git push origin \\\n  --delete \\\n  main",
        # no-ff merge of main, multiline
        "git merge feature \\\n  --no-ff \\\n  main",
    ],
)
def test_fix1_multiline_high_risk_is_blocked(cmd):
    from scripts.harness.v36.guard import is_high_risk

    assert is_high_risk("Bash", {"command": cmd}) is True


def test_fix1_multiline_high_risk_degraded_denies():
    """End-to-end: on the degraded path a multiline high-risk payload must DENY,
    never silently ALLOW."""
    from scripts.harness.v36.guard import _degraded_decision

    cmd = "rm -rf \\\n  /home/jay/workspace"
    dec = _degraded_decision("Bash", {"command": cmd}, {}, stage="evaluate_core", exc=RuntimeError("x"))
    assert dec["decision"] == "DENY"
    assert dec["degraded"] is True
    assert dec["matched_rule"] == "degraded.fail_closed_high_risk"


def test_fix1_singleline_high_risk_still_blocked():
    """re.DOTALL must not regress the original single-line detection."""
    from scripts.harness.v36.guard import is_high_risk

    assert is_high_risk("Bash", {"command": "rm -rf /home/jay/workspace"}) is True
    assert is_high_risk("Bash", {"command": "git push --force origin main"}) is True


# ---------------------------------------------------------------------------
# §4.2 — fix ② cmd-key input also triggers rule_4
# ---------------------------------------------------------------------------
def test_fix2_rule4_cmd_key_triggers():
    from scripts.harness.v36.rules import rule_4_self_status_confirm

    payload = {"cmd": "touch /home/jay/workspace/memory/events/x.done"}
    result = rule_4_self_status_confirm("Bash", payload, {})
    assert result is not None
    assert result[0] == "DENY"
    assert result[1] == "pattern.self_status_confirm_no_collector"


def test_fix2_rule4_command_and_cmd_key_parity():
    """command and cmd keys must produce identical decisions (no bypass)."""
    from scripts.harness.v36.rules import rule_4_self_status_confirm

    base = "echo done > /home/jay/workspace/memory/events/x.done"
    via_command = rule_4_self_status_confirm("Bash", {"command": base}, {})
    via_cmd = rule_4_self_status_confirm("Bash", {"cmd": base}, {})
    assert via_command is not None and via_cmd is not None
    assert via_command[0] == via_cmd[0] == "DENY"
    assert via_command[1] == via_cmd[1]


def test_fix2_rule4_collector_running_allows_cmd_key():
    """Parity must also hold for the allow path: collector running → no block."""
    from scripts.harness.v36.rules import rule_4_self_status_confirm

    os.environ["ANU_CALLBACK_COLLECTOR_RUNNING"] = "1"
    try:
        payload = {"cmd": "touch /home/jay/workspace/memory/events/x.done"}
        assert rule_4_self_status_confirm("Bash", payload, {}) is None
    finally:
        os.environ.pop("ANU_CALLBACK_COLLECTOR_RUNNING", None)


# ---------------------------------------------------------------------------
# §4.3 — fix ③ git push dedicated message branch actually runs
# ---------------------------------------------------------------------------
def test_fix3_git_push_plain_branch_message():
    from scripts.harness.v36.rules import rule_5_forbidden_tool

    result = rule_5_forbidden_tool("Bash", {"command": "git push origin feature-branch"}, {})
    assert result is not None
    assert result[0] == "DENY"
    assert result[1] == "pattern.forbidden_tool_or_shell"
    # Push-specific branch reached → its dedicated message, NOT the generic
    # "(rule: ...)" fallthrough that quoted pat.pattern.
    assert "git push" in result[2]
    assert "허가된 workflow" in result[2]
    assert "rule:" not in result[2]


def test_fix3_git_push_force_main_branch_message():
    from scripts.harness.v36.rules import rule_5_forbidden_tool

    result = rule_5_forbidden_tool("Bash", {"command": "git push --force origin main"}, {})
    assert result is not None
    assert result[0] == "DENY"
    assert "git push force/main" in result[2]


def test_fix3_non_push_forbidden_unaffected():
    """Non-push forbidden commands must still hit the generic branch."""
    from scripts.harness.v36.rules import rule_5_forbidden_tool

    result = rule_5_forbidden_tool("Bash", {"command": "gh pr merge 209"}, {})
    assert result is not None
    assert result[0] == "DENY"
    assert result[1] == "pattern.forbidden_tool_or_shell"
    # generic branch still surfaces the matched rule pattern
    assert "rule:" in result[2]


# ---------------------------------------------------------------------------
# §4.4 — existing high-risk DENY / benign ALLOW regression
# ---------------------------------------------------------------------------
def test_regress_benign_allows():
    from scripts.harness.v36.guard import evaluate

    dec = evaluate("Bash", {"command": "ls -la"}, {"task_id": "t"})
    assert dec["decision"] == "ALLOW"
    assert dec.get("degraded") is not True


def test_regress_existing_forbidden_denied():
    from scripts.harness.v36.guard import evaluate

    dec = evaluate("Bash", {"command": "git reset --hard HEAD~1"}, {"task_id": "t"})
    assert dec["decision"] == "DENY"


# ---------------------------------------------------------------------------
# §4.5 — degraded fail-closed regression (benign degraded still ALLOW-marked)
# ---------------------------------------------------------------------------
def test_regress_degraded_benign_allow_marked():
    from scripts.harness.v36.guard import _degraded_decision

    dec = _degraded_decision("Bash", {"command": "ls"}, {}, stage="evaluate_core", exc=RuntimeError("x"))
    assert dec["decision"] == "ALLOW"
    assert dec["degraded"] is True


def test_regress_degraded_highrisk_denies():
    from scripts.harness.v36.guard import _degraded_decision

    dec = _degraded_decision(
        "Bash", {"command": "git push --force origin main"}, {}, stage="evaluate_core", exc=RuntimeError("x")
    )
    assert dec["decision"] == "DENY"
    assert dec["degraded"] is True


# ---------------------------------------------------------------------------
# §4.6 — expected_files outside diff == 0
# ---------------------------------------------------------------------------
def test_diff_within_expected_files_only():
    """The micro-fix commit(s) on top of base c5eb7ee2 must touch only the
    expected_files set: guard.py, rules.py and this test file."""
    base = "c5eb7ee2"
    out = subprocess.run(
        ["git", "-C", str(_REPO_ROOT), "diff", "--name-only", base, "HEAD"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout
    changed = {line.strip() for line in out.splitlines() if line.strip()}
    allowed = {
        "scripts/harness/v36/guard.py",
        "scripts/harness/v36/rules.py",
        "tests/harness/test_v36_microfix_task2736_r1.py",
    }
    outside = changed - allowed
    assert not outside, f"diff escaped expected_files: {sorted(outside)}"
