import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
from mutation_profile_fixture import shared_mutation_env  # noqa: E402
"""Negative tests for LLM-Agent-SHA attribution (#86, Phase 0).

``LLM-Agent-SHA`` (docs/llm-agent-sha.md) is attribution metadata ONLY. These
tests prove it can never bypass the review/merge safety gates:

1. Same Gitea user + different LLM-Agent-SHA still fails self-review/approval.
2. Same Gitea user + different LLM-Agent-SHA still fails self-merge.
3. The eligibility logic does not consult SHA metadata at all — results are
   identical with no SHA, one SHA, or a different SHA in the environment, and
   no gate accepts an agent-SHA input.

Phase 0 adds no SHA support to any MCP tool; the environment variables set
here (``LLM_AGENT_SHA`` / ``LLM_AGENT_ROLE``) simulate a future launcher and
must be ignored by every gate.
"""
import inspect
import os
import re
import sys
import unittest
from unittest.mock import patch

sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))

import mcp_server  # noqa: E402
from mcp_server import (  # noqa: E402
    gitea_check_pr_eligibility,
    gitea_merge_pr,
    gitea_review_pr,
    gitea_submit_pr_review,
)

FAKE_AUTH = "Basic dGVzdDp0ZXN0"
SHA_PATTERN = re.compile(r"^llm-[0-9a-f]{12}$")
# Two distinct, well-formed agent SHAs "belonging" to the same Gitea user.
SHA_IMPLEMENTER = "llm-8f3a9c2d6b41"
SHA_WOULD_BE_REVIEWER = "llm-41d0e7aa9f2c"


def _pr(author, state="open", sha="abc123", mergeable=True):
    return {
        "user": {"login": author},
        "state": state,
        "head": {"sha": sha},
        "mergeable": mergeable,
    }


class TestShaFormatConvention(unittest.TestCase):
    """The documented format: llm-<12 lowercase hex>, nothing identifying."""

    def test_documented_examples_are_valid(self):
        for value in (SHA_IMPLEMENTER, SHA_WOULD_BE_REVIEWER, "llm-b7c93d441a08"):
            self.assertRegex(value, SHA_PATTERN)

    def test_identifying_or_malformed_values_are_rejected(self):
        for bad in (
            "llm-8F3A9C2D6B41",          # uppercase
            "llm-8f3a9c2d6b4",           # too short
            "llm-8f3a9c2d6b411",         # too long
            "llm-opus4",                 # model name, not hex
            "claude-8f3a9c2d6b41",       # provider prefix
            "jcwalker3",                 # username
            "llm-user@example.com",      # email
            "8f3a9c2d6b41",              # missing prefix
            "",
        ):
            self.assertNotRegex(bad, SHA_PATTERN)


class TestShaCannotBypassSelfReview(unittest.TestCase):
    """Scenario: user A (SHA 1) authored the PR; user A (SHA 2) tries to act."""

    def _env(self, agent_sha, role):
        # Reviewer-capable profile + a simulated launcher-injected agent SHA.
        return {
            "GITEA_PROFILE_NAME": "gitea-reviewer",
            "GITEA_ALLOWED_OPERATIONS": "read,review,approve,merge",
            "LLM_AGENT_SHA": agent_sha,
            "LLM_AGENT_ROLE": role,
        }

    @patch("mcp_server.api_request")
    @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
    def test_same_user_different_sha_cannot_approve(self, _auth, mock_api):
        mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
        env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
        with patch.dict(os.environ, env, clear=True):
            r = gitea_check_pr_eligibility(pr_number=9, action="approve", remote="prgs")
        self.assertFalse(r["eligible"])
        self.assertIn("authenticated user is PR author", r["reasons"])

    @patch("mcp_server.api_request")
    @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
    def test_same_user_different_sha_cannot_merge(self, _auth, mock_api):
        mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
        env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
        with patch.dict(os.environ, env, clear=True):
            r = gitea_check_pr_eligibility(pr_number=9, action="merge", remote="prgs")
        self.assertFalse(r["eligible"])
        self.assertIn("authenticated user is PR author", r["reasons"])

    @patch("mcp_server.api_request")
    @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
    def test_gated_merge_tool_refuses_self_merge_despite_sha(self, _auth, mock_api):
        # Even the fully-confirmed gated merge path must refuse: correct
        # confirmation string, mergeable PR, merge-capable profile — but the
        # authenticated user is the PR author, whatever the agent SHA says.
        mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
        env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
        with patch.dict(os.environ, env, clear=True):
            r = gitea_merge_pr(
                pr_number=9, confirmation="MERGE PR 9", remote="prgs")
        self.assertFalse(r.get("performed"))
        for call in mock_api.call_args_list:
            method, url = call.args[0], call.args[1]
            self.assertFalse(
                method == "POST" and url.endswith("/merge"),
                f"self-merge mutation reached the API: {method} {url}",
            )

    @patch("mcp_server.api_get_all")
    @patch("mcp_server.api_request")
    @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
    def test_review_tool_refuses_self_approval_despite_sha(self, _auth, mock_api, mock_get_all):
        from mcp_server import init_review_decision_lock
        from tests.test_mcp_server import FULL_HEAD_SHA, _seed_ready_review_decision

        head_sha = FULL_HEAD_SHA
        mock_get_all.return_value = [{"number": 9, "title": "PR 9", "state": "open", "head": {"ref": "branch9", "sha": head_sha}, "base": {"ref": "master"}, "mergeable": True, "user": {"login": "jcwalker3"}}]
        mock_api.side_effect = [
            {"login": "jcwalker3"}, # /user (inventory)
            {"login": "jcwalker3"}, # /user (submit eligibility)
            {"user": {"login": "jcwalker3"}, "state": "open", "head": {"sha": head_sha}, "mergeable": True}, # /pulls/9
        ]
        init_review_decision_lock("prgs", "review_pr")
        with patch("mcp_server._list_pr_lease_comments", return_value=[]):
            _seed_ready_review_decision(9, "approve", sha=head_sha, remote="prgs")
        env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
        with patch.dict(os.environ, env, clear=True):
            r = gitea_review_pr(
                pr_number=9, event="APPROVE", body="self approve", merge=False,
                remote="prgs", final_review_decision_ready=True)
        self.assertFalse(r["success"])
        self.assertIn("authenticated user is PR author", r["message"])
        for call in mock_api.call_args_list:
            method, url = call.args[0], call.args[1]
            self.assertFalse(
                method == "POST" and url.endswith("/reviews"),
                f"self-review mutation reached the API: {method} {url}",
            )


class TestEligibilityNeverConsultsSha(unittest.TestCase):
    """The gates have no SHA input: not via env, not via parameters."""

    def _run_eligibility(self, extra_env):
        env = {
            "GITEA_PROFILE_NAME": "gitea-reviewer",
            "GITEA_ALLOWED_OPERATIONS": "read,review,approve",
        }
        env.update(extra_env)
        with patch("mcp_server.get_auth_header", return_value=FAKE_AUTH), \
                patch("mcp_server.api_request") as mock_api:
            mock_api.side_effect = [{"login": "reviewer-bot"}, _pr("author-bot")]
            with patch.dict(os.environ, env, clear=True):
                return gitea_check_pr_eligibility(
                    pr_number=5, action="approve", remote="prgs")

    def test_result_identical_with_without_and_across_shas(self):
        baseline = self._run_eligibility({})
        with_one = self._run_eligibility(
            {"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
        with_other = self._run_eligibility(
            {"LLM_AGENT_SHA": SHA_WOULD_BE_REVIEWER, "LLM_AGENT_ROLE": "reviewer"})
        self.assertEqual(baseline, with_one)
        self.assertEqual(baseline, with_other)
        self.assertTrue(baseline["eligible"])  # sanity: a real decision ran

    def test_eligibility_result_carries_no_agent_sha(self):
        r = self._run_eligibility(
            {"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
        blob = repr(r)
        self.assertNotIn(SHA_IMPLEMENTER, blob)
        self.assertNotIn("LLM_AGENT", blob)

    def test_gate_functions_accept_no_agent_sha_parameter(self):
        for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
                   gitea_review_pr, gitea_submit_pr_review):
            for param in inspect.signature(fn).parameters:
                lowered = param.lower()
                self.assertNotIn("agent", lowered,
                                 f"{fn.__name__} accepts agent param {param!r}")
                self.assertNotIn("llm", lowered,
                                 f"{fn.__name__} accepts llm param {param!r}")

    def test_gate_sources_never_read_agent_sha(self):
        # Phase 0 guarantee: no gate reads LLM_AGENT_* metadata at all.
        for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
                   gitea_review_pr, gitea_submit_pr_review):
            src = inspect.getsource(fn)
            self.assertNotIn("LLM_AGENT", src,
                             f"{fn.__name__} reads LLM_AGENT metadata")
            self.assertNotIn("agent_sha", src,
                             f"{fn.__name__} consults an agent SHA")


if __name__ == "__main__":
    unittest.main()
