"""Tests for global deny rules and role-based tool access in ``.opencode/opencode.json``.

Covers issue #39 acceptance criteria:
- Global ``permission.bash`` deny rules for direct ``git commit``/``gh pr create``/
  ``gh pr merge``/``gh issue create`` (findLast — last wins, so they override earlier
  allows).
- ``agent.<name>.tools`` role-based access map per subagent.
- ``check-permissions.py`` exits 0 on the real repo configs.

Strategy:
- ``test_*_present`` load the real ``.opencode/opencode.json`` and assert structure.
- ``test_check_permissions_passes`` runs the validator as a subprocess (black-box).
"""

import json
import subprocess
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
OPENCODE_JSON = REPO_ROOT / ".opencode" / "opencode.json"
CHECK_PERM_SCRIPT = REPO_ROOT / ".opencode" / "scripts" / "check-permissions.py"
ENV_EXAMPLE = REPO_ROOT / ".env.example"


def _load_config() -> dict:
    with open(OPENCODE_JSON) as f:
        return json.load(f)


# ── CONTEXT7_API_KEY typo fix (issue #43) ────────────────────────────────────


def test_context7_api_key_present_in_env_example():
    """CONTEXT7_API_KEY (correct) is present in .env.example."""
    content = ENV_EXAMPLE.read_text()
    assert "CONTEXT7_API_KEY=" in content, "CONTEXT7_API_KEY missing from .env.example"


def test_context7_api_key_absent_in_env_example():
    """CONTEX7_API_KEY (typo) is absent from .env.example."""
    content = ENV_EXAMPLE.read_text()
    assert "CONTEX7_API_KEY=" not in content, "typo CONTEX7_API_KEY still in .env.example"


def test_context7_api_key_present_in_opencode_json():
    """CONTEXT7_API_KEY (correct) is referenced in opencode.json (context7 MCP)."""
    content = OPENCODE_JSON.read_text()
    assert "{env:CONTEXT7_API_KEY}" in content, "CONTEXT7_API_KEY missing from opencode.json"


def test_context7_api_key_absent_in_opencode_json():
    """CONTEX7_API_KEY (typo) is absent from opencode.json."""
    content = OPENCODE_JSON.read_text()
    assert "{env:CONTEX7_API_KEY}" not in content, "typo CONTEX7_API_KEY still in opencode.json"


# ── global deny rules ───────────────────────────────────────────────────────


# ── OPENCODE_SERVER_USERNAME in .env.example (issue #117) ───────────────────


def test_opencode_server_username_present_in_env_example():
    """OPENCODE_SERVER_USERNAME is present in .env.example."""
    content = ENV_EXAMPLE.read_text()
    assert "OPENCODE_SERVER_USERNAME" in content, (
        "OPENCODE_SERVER_USERNAME missing from .env.example"
    )


# ── global deny rules ───────────────────────────────────────────────────────


def test_global_deny_rules_present():
    """The 4 deny rules exist in permission.bash and are set to 'deny'."""
    bash = _load_config()["permission"]["bash"]
    expected = {
        "git commit *": "deny",
        "gh pr create *": "deny",
        "gh pr merge *": "deny",
        "gh issue create *": "deny",
    }
    for pattern, action in expected.items():
        assert pattern in bash, f"missing deny rule: {pattern}"
        assert bash[pattern] == action, f"{pattern}: expected {action}, got {bash[pattern]}"


def test_git_push_remains_allowed():
    """git push must NOT be denied (issue constraint)."""
    bash = _load_config()["permission"]["bash"]
    assert bash.get("git push *") == "allow", "git push must remain allowed"


def test_deny_rules_override_earlier_allows():
    """findLast semantics: deny rules come AFTER earlier allows, so deny wins.

    For ``git commit *`` the allow and deny use the same pattern — JSON dedupes
    keys, so ``json.load`` keeps the last value (deny). For the other 3 the deny
    pattern is narrower (e.g. ``gh pr create *`` vs ``gh pr create*``) and appears
    later in insertion order. This test verifies ordering for the differing
    patterns and final value for the duplicate-key case.
    """
    bash = _load_config()["permission"]["bash"]
    keys = list(bash.keys())

    # git commit *: same pattern for allow+deny, json.load keeps last (deny).
    assert bash["git commit *"] == "deny"

    # The other 3: deny pattern must appear after the broader allow pattern.
    ordering_checks = [
        ("gh pr create*", "gh pr create *"),
        ("gh pr merge*", "gh pr merge *"),
        ("gh issue*", "gh issue create *"),
    ]
    for allow_pattern, deny_pattern in ordering_checks:
        if allow_pattern in keys and deny_pattern in keys:
            assert keys.index(deny_pattern) > keys.index(allow_pattern), (
                f"deny '{deny_pattern}' must come after allow '{allow_pattern}' "
                f"(findLast: last wins)"
            )


def test_bash_destructive_deny_ask_rules_present():
    """Harden rules (issue #217) exist in permission.bash with correct actions.

    Deny covers gh api DELETE variants and repo transfer; ask covers force
    push, refspec deletion and tag deletion, including ``git -C *`` mirrors.
    Also asserts findLast ordering: all new rules come after the last
    pre-existing rule (``gh issue create *``), so no earlier allow shadows them.
    """
    bash = _load_config()["permission"]["bash"]
    expected = {
        "gh api * -X DELETE *": "deny",
        "gh api -X DELETE *": "deny",
        "gh api * --method DELETE *": "deny",
        "gh api * --method delete *": "deny",
        "gh repo transfer *": "deny",
        "git push --force*": "ask",
        "git push -f*": "ask",
        "git push * --force*": "ask",
        "git push * -f*": "ask",
        "git push * :*": "ask",
        "git tag -d *": "ask",
        "git -C * push --force*": "ask",
        "git -C * push -f*": "ask",
        "git -C * push * --force*": "ask",
        "git -C * push * -f*": "ask",
        "git -C * push * :*": "ask",
        "git -C * tag -d *": "ask",
    }
    for pattern, action in expected.items():
        assert pattern in bash, f"missing rule: {pattern}"
        assert bash[pattern] == action, f"{pattern}: expected {action}, got {bash[pattern]}"

    keys = list(bash.keys())
    last_old_rule = "gh issue create *"
    assert keys.index(last_old_rule) < min(keys.index(p) for p in expected), (
        f"new rules must come after '{last_old_rule}' (findLast: last wins)"
    )


# ── agent.general.tools ────────────────────────────────────────────────────


def test_general_tools():
    """general: commit/create_pr/create_issue=true, merge_pr=false."""
    tools = _load_config()["agent"]["general"]["tools"]
    assert tools["commit"] is True
    assert tools["create_pr"] is True
    assert tools["create_issue"] is True
    assert tools["merge_pr"] is False
    assert _load_config()["agent"]["general"]["steps"] == 150


# ── agent.reviewer.tools ────────────────────────────────────────────────────


def test_reviewer_tools_readonly():
    """reviewer is read-only for repo ops; can create issues for discovered bugs."""
    tools = _load_config()["agent"]["reviewer"]["tools"]
    assert tools["commit"] is False
    assert tools["create_pr"] is False
    assert tools["create_issue"] is True
    assert tools["merge_pr"] is False


# ── agent.memory-syncer.tools ───────────────────────────────────────────────


def test_memory_syncer_tools_readonly():
    """memory-syncer is read-only for repo ops; can create issues."""
    tools = _load_config()["agent"]["memory-syncer"]["tools"]
    assert tools["commit"] is False
    assert tools["create_pr"] is False
    assert tools["create_issue"] is True
    assert tools["merge_pr"] is False


# ── check-permissions.py passes ─────────────────────────────────────────────


def test_check_permissions_passes():
    """The validator exits 0 with OK message on current configs."""
    result = subprocess.run(
        ["python3", str(CHECK_PERM_SCRIPT)],
        capture_output=True,
        text=True,
        check=False,
    )
    assert result.returncode == 0, result.stderr
    assert "OK: No dangerous permission rules found." in result.stdout


if __name__ == "__main__":
    import pytest

    pytest.main([__file__, "-v"])
