"""Axis 3 restricted canary smoke test — SP1..SP7.

chair_authorization_id = CHAIR-AUTH-AXIS-3-CANARY-20260524-JJONGS-RESTRICTED-001

Each SP simulates a single PreToolUse hook invocation by piping a synthetic
event payload into the live hook script. Asserts on exit code + audit log.
PYTHONPATH-agnostic: classifier is imported via explicit sys.path bootstrap.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
HOOK_PATH = Path("/home/jay/.claude/hooks/pre_tool_use_runtime_guard_canary.py")
WORKSPACE_LIVE = Path("/home/jay/workspace")

# ---- helpers ---------------------------------------------------------------


def _run_hook(payload: dict, extra_env: dict | None = None, workspace_root: Path | None = None) -> subprocess.CompletedProcess:
    env = os.environ.copy()
    env["ANU_CANARY_AXIS_3"] = "true"
    if workspace_root is not None:
        env["ANU_WORKSPACE_ROOT"] = str(workspace_root)
    if extra_env:
        env.update(extra_env)
    proc = subprocess.run(
        [sys.executable, str(HOOK_PATH)],
        input=json.dumps(payload),
        capture_output=True,
        text=True,
        env=env,
        timeout=10,
    )
    return proc


@pytest.fixture
def isolated_audit_dir(tmp_path: Path) -> Path:
    """Create an isolated workspace root with a memory/system subtree so the
    hook writes audit logs into the sandbox instead of the live tree.
    """
    (tmp_path / "memory" / "system").mkdir(parents=True, exist_ok=True)
    # ensure classifier import path resolves: copy utils/ or symlink it.
    utils_src = REPO_ROOT / "utils"
    utils_dst = tmp_path / "utils"
    if not utils_dst.exists():
        utils_dst.mkdir(parents=True, exist_ok=True)
        for fname in ("runtime_guard_classifier.py", "runtime_guard_policy_map.py"):
            shutil.copy2(utils_src / fname, utils_dst / fname)
        # add an empty __init__ so import works under sandbox root.
        (utils_dst / "__init__.py").write_text("", encoding="utf-8")
    return tmp_path


def _read_jsonl(path: Path) -> list[dict]:
    if not path.exists():
        return []
    out = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            out.append(json.loads(line))
        except json.JSONDecodeError:
            pass
    return out


# ---- SP1: normal noop ------------------------------------------------------


def test_SP1_normal_noop(isolated_audit_dir: Path):
    """Plain Read tool — must allow (exit 0) and record AUDIT_ONLY."""
    payload = {
        "session_id": "dev5-canary-session-test",
        "tool_name": "Read",
        "tool_input": {"file_path": "/home/jay/workspace/README.md"},
    }
    proc = _run_hook(payload, workspace_root=isolated_audit_dir)
    assert proc.returncode == 0, f"SP1 expected exit 0, got {proc.returncode}: {proc.stderr}"
    log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_decision_audit.jsonl")
    assert any(r.get("decision") == "AUDIT_ONLY" for r in log), f"SP1 decision log missing AUDIT_ONLY: {log}"


# ---- SP2: forbidden path WARN ---------------------------------------------


def test_SP2_forbidden_path_warn(isolated_audit_dir: Path):
    """Edit on dispatch.py forbidden_path — must allow (WARN, not BLOCK)."""
    payload = {
        "session_id": "dev5-canary-session-test",
        "tool_name": "Edit",
        "tool_input": {
            "file_path": "/home/jay/workspace/dispatch.py",
            "old_string": "x",
            "new_string": "y",
        },
    }
    proc = _run_hook(payload, workspace_root=isolated_audit_dir)
    assert proc.returncode == 0, f"SP2 expected allow (forbidden=WARN), got {proc.returncode}"
    log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_decision_audit.jsonl")
    assert any(r.get("decision") == "WARN" and r.get("category") == "forbidden_path" for r in log), (
        f"SP2 expected WARN forbidden_path in log: {log}"
    )


# ---- SP3: credential pattern WARN -----------------------------------------


def test_SP3_credential_pattern_warn(isolated_audit_dir: Path):
    """Write containing AWS key pattern — WARN, allow."""
    payload = {
        "session_id": "dev5-canary-session-test",
        "tool_name": "Write",
        "tool_input": {
            "file_path": "/tmp/nope.txt",
            "content": "leaked AKIAABCDEFGHIJKLMNOP credential",
        },
    }
    proc = _run_hook(payload, workspace_root=isolated_audit_dir)
    assert proc.returncode == 0, f"SP3 expected allow (credential=WARN), got {proc.returncode}"
    log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_decision_audit.jsonl")
    assert any(r.get("decision") == "WARN" and r.get("category") == "credential_pattern" for r in log), (
        f"SP3 expected WARN credential_pattern in log: {log}"
    )


# ---- SP4: destructive 5 BLOCK ---------------------------------------------


@pytest.mark.parametrize(
    "command,expected_rule",
    [
        ("rm -rf /", "destructive.rm_rf_root"),
        ("git push --force origin main", "destructive.git_push_force_main"),
        ("git reset --hard some_other_branch", "destructive.git_reset_hard_other_branch"),
        ("git branch -D main", "destructive.git_branch_delete_main"),
        ("cokacdir --cron-remove abc123", "destructive.cokacdir_cron_remove"),
    ],
)
def test_SP4_destructive_block(isolated_audit_dir: Path, command: str, expected_rule: str):
    """Each destructive command — BLOCK (exit 2) with matching rule id."""
    payload = {
        "session_id": "dev5-canary-session-test",
        "tool_name": "Bash",
        "tool_input": {"command": command},
    }
    proc = _run_hook(payload, workspace_root=isolated_audit_dir)
    assert proc.returncode == 2, f"SP4 [{command}] expected exit 2 (BLOCK), got {proc.returncode}: {proc.stdout}"
    body = proc.stdout
    assert expected_rule in body, f"SP4 [{command}] expected rule {expected_rule} in stdout: {body}"


# ---- SP5: Axis 1/2 hook 충돌 0 ---------------------------------------------


def test_SP5_axis_1_2_no_interference(isolated_audit_dir: Path):
    """Callback ledger append + callback inbox write must NOT be blocked."""
    cases = [
        {
            "tool_name": "Write",
            "tool_input": {
                "file_path": "/home/jay/workspace/memory/system/.callback_ledger.jsonl",
                "content": '{"foo":1}\n',
            },
        },
        {
            "tool_name": "Write",
            "tool_input": {
                "file_path": "/home/jay/workspace/memory/.callback_inbox/sample.json",
                "content": "{}",
            },
        },
        {
            "tool_name": "Read",
            "tool_input": {
                "file_path": "/home/jay/workspace/memory/system/.callback_ledger.jsonl",
            },
        },
        # Callback helper modules — allowed_resources excludes them as targets,
        # but reading them must never BLOCK (callback flow protection).
        {
            "tool_name": "Read",
            "tool_input": {
                "file_path": "/home/jay/workspace/utils/callback_registration.py",
            },
        },
    ]
    for c in cases:
        payload = {"session_id": "dev5-canary-session-test", **c}
        proc = _run_hook(payload, workspace_root=isolated_audit_dir)
        assert proc.returncode == 0, (
            f"SP5 Axis 1/2 interference: tool={c['tool_name']} input={c['tool_input']} exit={proc.returncode}"
        )


# ---- SP6: dispatch 통과 ----------------------------------------------------


def test_SP6_dispatch_bypass(isolated_audit_dir: Path):
    """ANU_DISPATCH_RUNNING=1 → AUDIT_ONLY exclusion regardless of payload."""
    payload = {
        "session_id": "dispatch-session",
        "tool_name": "Bash",
        # would have been BLOCK without exclusion
        "tool_input": {"command": "rm -rf /"},
    }
    proc = _run_hook(
        payload,
        extra_env={"ANU_DISPATCH_RUNNING": "1"},
        workspace_root=isolated_audit_dir,
    )
    assert proc.returncode == 0, f"SP6 dispatch must pass through, got {proc.returncode}: {proc.stdout}"
    log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_decision_audit.jsonl")
    assert any(r.get("rule_id") == "session.dispatch_excluded" for r in log), (
        f"SP6 expected session.dispatch_excluded in log: {log}"
    )


# ---- SP7: hook crash fail-safe --------------------------------------------


def test_SP7_hook_crash_fail_safe(isolated_audit_dir: Path):
    """Forced crash via ANU_AXIS_3_CANARY_FORCE_CRASH=1 — must still allow."""
    payload = {
        "session_id": "dev5-canary-session-test",
        "tool_name": "Bash",
        "tool_input": {"command": "rm -rf /"},  # would BLOCK without crash path
    }
    proc = _run_hook(
        payload,
        extra_env={"ANU_AXIS_3_CANARY_FORCE_CRASH": "1"},
        workspace_root=isolated_audit_dir,
    )
    assert proc.returncode == 0, f"SP7 fail-safe broke: exit {proc.returncode}: {proc.stdout}"
    crash_log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_hook_crash_audit.jsonl")
    assert any(r.get("stage") == "forced_simulation" for r in crash_log), (
        f"SP7 expected forced_simulation crash log: {crash_log}"
    )


# ---- BONUS: canary env-flag off → noop fast path -------------------------


def test_canary_env_flag_off_noop(isolated_audit_dir: Path):
    """Without ANU_CANARY_AXIS_3=true, hook must noop and NOT write any logs."""
    env = os.environ.copy()
    env.pop("ANU_CANARY_AXIS_3", None)
    env["ANU_WORKSPACE_ROOT"] = str(isolated_audit_dir)
    payload = {
        "session_id": "non-canary-session",
        "tool_name": "Bash",
        "tool_input": {"command": "rm -rf /"},
    }
    proc = subprocess.run(
        [sys.executable, str(HOOK_PATH)],
        input=json.dumps(payload),
        capture_output=True,
        text=True,
        env=env,
        timeout=10,
    )
    assert proc.returncode == 0, f"non-canary must noop allow, got {proc.returncode}"
    log = _read_jsonl(isolated_audit_dir / "memory/system/.axis_3_canary_decision_audit.jsonl")
    assert log == [], f"non-canary must not write decision log: {log}"
