"""Tests for gitea_delete_branch capability + role gate (Issue #408, #729).

``gitea_delete_branch`` requires the exact ``gitea.branch.delete`` operation:
without it the delete fails closed (no preflight, no auth lookup, no API call,
structured permission report).

#729: delete_branch is reconciler-owned. ``gitea.branch.delete`` is granted only
to the reconciler profile, so the resolver classifies delete_branch as a
reconciler task. Raw ``gitea_delete_branch`` still redirects the reconciler to
the guarded ``gitea_cleanup_merged_pr_branch`` path (#514/#687); author,
reviewer, and merger remain denied by the permission gate and/or the required
role gate.
"""
import json
import os
import sys
import tempfile
import unittest
from unittest.mock import patch

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

import mcp_server
from mcp_server import gitea_delete_branch
import task_capability_map
import role_session_router

FAKE_AUTH = "token fake"

AUTHOR_NO_DELETE = {
    "profile_name": "prgs-author",
    "allowed_operations": [
        "gitea.read", "gitea.pr.create", "gitea.pr.comment",
        "gitea.branch.push", "gitea.issue.comment",
    ],
    "forbidden_operations": ["gitea.pr.approve", "gitea.pr.merge"],
    "audit_label": "prgs-author",
}

AUTHOR_WITH_DELETE = {
    "profile_name": "prgs-author-deleter",
    "allowed_operations": AUTHOR_NO_DELETE["allowed_operations"] + [
        "gitea.branch.delete",
    ],
    "forbidden_operations": ["gitea.pr.approve", "gitea.pr.merge"],
    "audit_label": "prgs-author-deleter",
}

# #729: delete_branch is reconciler-owned. The reconciler holds
# gitea.branch.delete but raw gitea_delete_branch redirects it to the guarded
# gitea_cleanup_merged_pr_branch path (#514/#687).
RECONCILER_WITH_DELETE = {
    "profile_name": "prgs-reconciler",
    "role": "reconciler",
    "allowed_operations": [
        "gitea.read", "gitea.issue.comment", "gitea.pr.comment",
        "gitea.branch.delete",
    ],
    "forbidden_operations": [
        "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.create",
    ],
    "audit_label": "prgs-reconciler",
}

CONFIG = {
    "version": 2,
    "contexts": {
        "ctx": {
            "enabled": True,
            "gitea": {"enabled": True, "base_url": "https://gitea.example.com"},
        }
    },
    "profiles": {
        "author-no-delete": {
            "enabled": True,
            "context": "ctx",
            "role": "author",
            "username": "author-user",
            "auth": {"type": "env", "name": "GITEA_TOKEN_AUTHOR"},
            "allowed_operations": AUTHOR_NO_DELETE["allowed_operations"],
            "forbidden_operations": [],
            "execution_profile": "author-no-delete",
        },
        "author-with-delete": {
            "enabled": True,
            "context": "ctx",
            "role": "author",
            "username": "deleter-user",
            "auth": {"type": "env", "name": "GITEA_TOKEN_AUTHOR"},
            "allowed_operations": AUTHOR_WITH_DELETE["allowed_operations"],
            "forbidden_operations": [],
            "execution_profile": "author-with-delete",
        },
        "reviewer-profile": {
            "enabled": True,
            "context": "ctx",
            "role": "reviewer",
            "username": "reviewer-user",
            "auth": {"type": "env", "name": "GITEA_TOKEN_REVIEWER"},
            "allowed_operations": [
                "gitea.read", "gitea.pr.review", "gitea.pr.merge",
            ],
            "forbidden_operations": [
                "gitea.branch.delete", "gitea.branch.push", "gitea.pr.create",
            ],
            "execution_profile": "reviewer-profile",
        },
        # #729: reconciler is the only delete-capable role.
        "reconciler-profile": {
            "enabled": True,
            "context": "ctx",
            "role": "reconciler",
            "username": "reconciler-user",
            "auth": {"type": "env", "name": "GITEA_TOKEN_RECONCILER"},
            "allowed_operations": [
                "gitea.read", "gitea.issue.comment", "gitea.pr.comment",
                "gitea.branch.delete",
            ],
            "forbidden_operations": ["gitea.pr.create", "gitea.pr.merge"],
            "execution_profile": "reconciler-profile",
        },
    },
    "rules": {"allow_runtime_switching": False},
}


class TestDeleteBranchToolGate(unittest.TestCase):

    def setUp(self):
        self._preflight_snapshot = (
            mcp_server._preflight_whoami_called,
            mcp_server._preflight_capability_called,
        )
        mcp_server._preflight_whoami_called = False
        mcp_server._preflight_capability_called = False
        self._remotes = patch.dict(mcp_server.REMOTES, {
            "prgs": {"host": "gitea.example.com", "org": "Example-Org",
                     "repo": "Example-Repo"},
        })
        self._remotes.start()
        mcp_server._IDENTITY_CACHE.clear()
        patch("gitea_config.load_config", return_value={}).start()
        patch(
            "gitea_config.is_runtime_switching_enabled", return_value=False
        ).start()
        patch("gitea_audit.audit_enabled", return_value=False).start()
        self.mock_api = patch("mcp_server.api_request").start()
        self.mock_auth = patch(
            "mcp_server.get_auth_header", return_value=FAKE_AUTH
        ).start()

    def tearDown(self):
        patch.stopall()
        mcp_server._IDENTITY_CACHE.clear()
        mcp_server._preflight_whoami_called, mcp_server._preflight_capability_called = (
            self._preflight_snapshot
        )

    def _set_profile(self, profile):
        patch("mcp_server.get_profile", return_value=profile).start()

    def _delete_calls(self):
        return [c for c in self.mock_api.call_args_list if c.args[0] == "DELETE"]

    def test_blocked_without_delete_capability(self):
        self._set_profile(AUTHOR_NO_DELETE)
        res = gitea_delete_branch(branch="feat/branch", remote="prgs")
        self.assertFalse(res["success"])
        self.assertFalse(res["performed"])
        self.assertEqual(res["required_permission"], "gitea.branch.delete")
        self.assertTrue(res["reasons"])
        self.assertEqual(
            res["permission_report"]["missing_permission"],
            "gitea.branch.delete",
        )
        self.mock_api.assert_not_called()
        self.mock_auth.assert_not_called()

    def test_author_with_delete_perm_denied_by_role_gate(self):
        """#729: even an author holding gitea.branch.delete is denied — the
        required role is now reconciler. Fail closed, no API DELETE."""
        self._set_profile(AUTHOR_WITH_DELETE)
        mcp_server.record_preflight_check("whoami")
        mcp_server.record_preflight_check("capability", resolved_role="author")
        res = gitea_delete_branch(branch="feat/branch", remote="prgs")
        self.assertFalse(res["success"])
        self.assertFalse(res["performed"])
        self.assertEqual(res["required_role_kind"], "reconciler")
        self.assertEqual(res["active_role_kind"], "author")
        self.assertTrue(res["reasons"])
        self.assertFalse(self._delete_calls())

    def test_reviewer_with_delete_perm_denied_by_role_gate(self):
        """A reviewer that somehow holds the permission is still denied."""
        reviewer_with_delete = {
            "profile_name": "prgs-reviewer",
            "role": "reviewer",
            "allowed_operations": [
                "gitea.read", "gitea.pr.review", "gitea.branch.delete",
            ],
            "forbidden_operations": [],
            "audit_label": "prgs-reviewer",
        }
        self._set_profile(reviewer_with_delete)
        mcp_server.record_preflight_check("whoami")
        mcp_server.record_preflight_check("capability", resolved_role="reviewer")
        res = gitea_delete_branch(branch="feat/branch", remote="prgs")
        self.assertFalse(res["success"])
        self.assertFalse(res["performed"])
        self.assertEqual(res["required_role_kind"], "reconciler")
        self.assertEqual(res["active_role_kind"], "reviewer")
        self.assertFalse(self._delete_calls())

    def test_reconciler_performs_raw_delete(self):
        """#729: the reconciler is the delete-capable role and performs the raw
        deletion (no audit phase active here); the API DELETE is issued."""
        self._set_profile(RECONCILER_WITH_DELETE)
        mcp_server.record_preflight_check("whoami")
        mcp_server.record_preflight_check(
            "capability", resolved_role="reconciler")
        self.mock_api.return_value = {}
        res = gitea_delete_branch(branch="feat/branch", remote="prgs")
        self.assertTrue(res["success"])
        self.assertIn("deleted", res["message"])
        self.assertTrue(self._delete_calls())


class TestDeleteBranchResolverParity(unittest.TestCase):
    def setUp(self):
        self._remotes = patch.dict(mcp_server.REMOTES, {
            "prgs": {"host": "gitea.example.com", "org": "Example-Org",
                     "repo": "Example-Repo"},
        })
        self._remotes.start()
        mcp_server._IDENTITY_CACHE.clear()
        self._dir = tempfile.TemporaryDirectory()
        self.config_path = os.path.join(self._dir.name, "profiles.json")
        with open(self.config_path, "w", encoding="utf-8") as fh:
            fh.write(json.dumps(CONFIG))
        patch(
            "gitea_config.is_runtime_switching_enabled", return_value=False
        ).start()
        patch("gitea_audit.audit_enabled", return_value=False).start()
        self.mock_api = patch("mcp_server.api_request").start()
        patch("mcp_server.get_auth_header", return_value=FAKE_AUTH).start()

    def tearDown(self):
        patch.stopall()
        mcp_server._IDENTITY_CACHE.clear()
        self._dir.cleanup()

    def _env(self, profile: str) -> dict:
        return {
            "GITEA_MCP_CONFIG": self.config_path,
            "GITEA_MCP_PROFILE": profile,
            "GITEA_TOKEN_AUTHOR": "author-pass",
            "GITEA_TOKEN_REVIEWER": "reviewer-pass",
            "GITEA_TOKEN_RECONCILER": "reconciler-pass",
        }

    def _delete_calls(self):
        return [c for c in self.mock_api.call_args_list if c.args[0] == "DELETE"]

    def test_reconciler_resolver_allows_delete_branch(self):
        """#729: resolve(delete_branch) on the reconciler profile is allowed and
        classified as a reconciler task."""
        with patch.dict(os.environ, self._env("reconciler-profile"), clear=True):
            resolve = mcp_server.gitea_resolve_task_capability(
                task="delete_branch", remote="prgs")
        # Deterministic role-map outcomes (avoid runtime-staleness-dependent
        # allowed_in_current_session, which folds in reconnect state).
        self.assertEqual(resolve["required_role_kind"], "reconciler")
        self.assertEqual(
            resolve["required_operation_permission"], "gitea.branch.delete")
        self.assertTrue(resolve["active_profile_permission_allowed"])
        self.assertTrue(resolve["configured"])
        self.assertIn("reconciler-profile", resolve["matching_configured_profile"])
        self.assertEqual(resolve["active_role_kind"], "reconciler")

    def test_author_resolver_denies_delete_branch(self):
        """#729: an author session cannot resolve delete_branch — required role
        is reconciler and the author lacks the permission."""
        with patch.dict(os.environ, self._env("author-no-delete"), clear=True):
            resolve = mcp_server.gitea_resolve_task_capability(
                task="delete_branch", remote="prgs")
        self.assertEqual(resolve["required_role_kind"], "reconciler")
        self.assertFalse(resolve["allowed_in_current_session"])

    def test_reviewer_resolver_denial_blocks_raw_tool(self):
        with patch.dict(os.environ, self._env("reviewer-profile"), clear=True):
            resolve = mcp_server.gitea_resolve_task_capability(
                task="delete_branch", remote="prgs")
            self.assertFalse(resolve["allowed_in_current_session"])
            patch("mcp_server.get_profile", return_value={
                "profile_name": "prgs-reviewer",
                "role": "reviewer",
                "allowed_operations": ["gitea.read", "gitea.pr.review"],
                "forbidden_operations": ["gitea.branch.delete"],
            }).start()
            res = gitea_delete_branch(branch="feat/branch", remote="prgs")
        self.assertFalse(res["success"])
        self.assertEqual(
            res["permission_report"]["missing_permission"],
            resolve["required_operation_permission"],
        )
        self.assertFalse(self._delete_calls())


class TestDeleteBranchRoleMapParity(unittest.TestCase):
    """#729: both single-source-of-truth maps must classify delete_branch as
    reconciler and stay in agreement."""

    def test_task_capability_map_role_reconciler(self):
        self.assertEqual(
            task_capability_map.required_role("delete_branch"), "reconciler")
        self.assertEqual(
            task_capability_map.required_permission("delete_branch"),
            "gitea.branch.delete",
        )

    def test_router_required_role_reconciler(self):
        self.assertEqual(
            role_session_router.TASK_REQUIRED_ROLE["delete_branch"],
            "reconciler",
        )
        self.assertEqual(
            role_session_router.required_role_for_task("delete_branch"),
            "reconciler",
        )

    def test_router_set_membership_moved(self):
        self.assertIn("delete_branch", role_session_router.RECONCILER_TASKS)
        self.assertNotIn("delete_branch", role_session_router.AUTHOR_TASKS)

    def test_maps_agree(self):
        self.assertEqual(
            task_capability_map.required_role("delete_branch"),
            role_session_router.TASK_REQUIRED_ROLE["delete_branch"],
        )


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