"""Regression coverage for issue #723 role and capability invariants."""

from __future__ import annotations

import unittest
from unittest.mock import patch

import gitea_mcp_server as mcp_server
import task_capability_map


REVIEWER_PROFILE = {
    "profile_name": "prgs-reviewer",
    "role": "reviewer",
    "allowed_operations": [
        "gitea.read",
        "gitea.pr.review",
        "gitea.pr.approve",
        "gitea.pr.request_changes",
        "gitea.pr.comment",
        "gitea.issue.comment",
    ],
    "forbidden_operations": [
        "gitea.branch.create",
        "gitea.branch.push",
        "gitea.repo.commit",
        "gitea.pr.create",
        "gitea.pr.merge",
    ],
}

CONFIG = {
    "profiles": {
        "prgs-reviewer": {
            "role": "reviewer",
            "allowed_operations": REVIEWER_PROFILE["allowed_operations"],
            "forbidden_operations": REVIEWER_PROFILE["forbidden_operations"],
        },
        "prgs-merger": {
            "role": "merger",
            "allowed_operations": [
                "gitea.read",
                "gitea.pr.merge",
                "gitea.pr.comment",
                "gitea.issue.comment",
            ],
            "forbidden_operations": [
                "gitea.pr.approve",
                "gitea.pr.review",
                "gitea.pr.request_changes",
            ],
        },
    }
}


def _reset_preflight() -> None:
    mcp_server._clear_preflight_capability_state()
    mcp_server._preflight_whoami_called = False
    mcp_server._preflight_whoami_violation = False
    mcp_server.capability_stop_terminal.clear()
    mcp_server.role_session_router.clear_route_state()


class _ResolveHarness(unittest.TestCase):
    def setUp(self):
        _reset_preflight()

    def tearDown(self):
        _reset_preflight()

    def _resolve(
        self,
        task,
        profile=REVIEWER_PROFILE,
        required_role=None,
        init_side_effect=None,
    ):
        patches = [
            patch.object(mcp_server, "get_profile", return_value=profile),
            patch.object(
                mcp_server.gitea_config, "load_config", return_value=CONFIG
            ),
            patch.object(
                mcp_server, "_authenticated_username", return_value="tester"
            ),
            patch.object(
                mcp_server,
                "init_review_decision_lock",
                return_value=None,
                side_effect=init_side_effect,
            ),
            patch.object(
                mcp_server, "record_mutation_authority", return_value=None
            ),
            patch.object(
                mcp_server, "_check_mcp_runtimes_diagnostics", return_value=[]
            ),
        ]
        if required_role is not None:
            patches.append(
                patch.object(
                    mcp_server.task_capability_map,
                    "required_role",
                    side_effect=lambda candidate: (
                        required_role
                        if candidate == task
                        else task_capability_map.TASK_CAPABILITY_MAP[candidate][
                            "role"
                        ]
                    ),
                )
            )
        for context in patches:
            context.__enter__()
        try:
            return mcp_server.gitea_resolve_task_capability(
                task=task, remote="prgs"
            )
        finally:
            for context in reversed(patches):
                context.__exit__(None, None, None)


class TestCapabilityRoleStampSafety(_ResolveHarness):
    def test_allowed_resolution_records_the_correct_stamp(self):
        result = self._resolve("review_pr")
        self.assertTrue(result["allowed_in_current_session"], result)
        self.assertEqual(mcp_server._preflight_resolved_role, "reviewer")
        self.assertEqual(mcp_server._preflight_resolved_task, "review_pr")

    def test_denied_resolution_records_no_stamp(self):
        with patch.object(
            mcp_server,
            "record_preflight_check",
            wraps=mcp_server.record_preflight_check,
        ) as record:
            result = self._resolve("review_pr", required_role="merger")

        self.assertFalse(result["allowed_in_current_session"], result)
        stamped_calls = [
            call
            for call in record.call_args_list
            if len(call.args) > 1 and call.args[1] is not None
        ]
        self.assertEqual(
            stamped_calls,
            [],
            "a denied resolution must never transiently record a role stamp",
        )
        self.assertIsNone(mcp_server._preflight_resolved_role)
        self.assertIsNone(mcp_server._preflight_resolved_task)

    def test_denied_resolution_clears_an_existing_stamp(self):
        allowed = self._resolve("review_pr")
        self.assertTrue(allowed["allowed_in_current_session"], allowed)
        self.assertEqual(mcp_server._preflight_resolved_role, "reviewer")

        denied = self._resolve("merge_pr")
        self.assertFalse(denied["allowed_in_current_session"], denied)
        self.assertIsNone(mcp_server._preflight_resolved_role)
        self.assertIsNone(mcp_server._preflight_resolved_task)

    def test_denial_cannot_poison_a_later_allowed_task(self):
        denied = self._resolve("merge_pr")
        self.assertFalse(denied["allowed_in_current_session"], denied)

        allowed = self._resolve("review_pr")
        self.assertTrue(allowed["allowed_in_current_session"], allowed)
        self.assertEqual(mcp_server._preflight_resolved_role, "reviewer")
        self.assertEqual(mcp_server._preflight_resolved_task, "review_pr")

    def test_unexpected_resolver_failure_leaves_no_stamp(self):
        with self.assertRaisesRegex(RuntimeError, "malformed decision state"):
            self._resolve(
                "review_pr",
                init_side_effect=RuntimeError("malformed decision state"),
            )
        self.assertIsNone(mcp_server._preflight_resolved_role)
        self.assertIsNone(mcp_server._preflight_resolved_task)


class TestStructuredWorkspaceRoleFailures(unittest.TestCase):
    def test_review_submission_returns_workspace_role_binding_failure(self):
        error = RuntimeError(
            "namespace workspace binding blocked: merger role in reviewer workspace"
        )
        with patch.object(
            mcp_server, "_verify_role_mutation_workspace", side_effect=error
        ):
            result = mcp_server._evaluate_pr_review_submission(
                pr_number=721,
                action="approve",
                expected_head_sha="8" * 40,
                remote="prgs",
                live=True,
                final_review_decision_ready=True,
            )

        self.assertFalse(result["performed"])
        self.assertEqual(result["blocker_kind"], "workspace_role_binding")
        self.assertTrue(
            any("workspace/role binding failed" in reason for reason in result["reasons"]),
            result,
        )
        self.assertTrue(any("merger role" in reason for reason in result["reasons"]))

    def test_adopt_merger_lease_returns_workspace_role_binding_failure(self):
        error = RuntimeError("merger workspace binding rejected")
        with patch.object(
            mcp_server, "_profile_operation_gate", return_value=[]
        ), patch.object(
            mcp_server, "_verify_role_mutation_workspace", side_effect=error
        ), patch.object(mcp_server, "_resolve") as resolve:
            result = mcp_server.gitea_adopt_merger_pr_lease(
                pr_number=718,
                worktree="branches/merge-pr-718",
                expected_head_sha="7" * 40,
                remote="prgs",
            )

        self.assertFalse(result["success"])
        self.assertFalse(result["adopted"])
        self.assertEqual(result["blocker_kind"], "workspace_role_binding")
        self.assertEqual(result["pr_number"], 718)
        self.assertEqual(result["expected_head_sha"], "7" * 40)
        self.assertIsNone(result["live_head_sha"])
        self.assertTrue(any("binding rejected" in reason for reason in result["reasons"]))
        resolve.assert_not_called()

    def test_unexpected_verifier_failure_remains_fail_closed(self):
        with patch.object(
            mcp_server,
            "_verify_role_mutation_workspace",
            side_effect=ValueError("unexpected verifier state"),
        ), patch.object(mcp_server, "_resolve") as resolve:
            with self.assertRaisesRegex(ValueError, "unexpected verifier state"):
                mcp_server._evaluate_pr_review_submission(
                    pr_number=721,
                    action="approve",
                    remote="prgs",
                    live=True,
                )
        resolve.assert_not_called()


class TestRuntimeCapabilityRoleFiltering(unittest.TestCase):
    def test_runtime_role_filter_denies_permission_bearing_wrong_role(self):
        allowed = REVIEWER_PROFILE["allowed_operations"] + ["gitea.pr.merge"]
        capabilities = mcp_server._build_runtime_task_capabilities(
            allowed,
            [],
            CONFIG,
            remote="prgs",
            active_role_kind="reviewer",
        )
        merge_entry = next(
            item
            for item in capabilities["task_capabilities"]
            if item["task"] == "merge_pr"
        )
        self.assertTrue(merge_entry["role_exclusive"])
        self.assertEqual(merge_entry["capability_view"], "role_filtered")
        self.assertFalse(merge_entry["allowed_in_current_session"])
        self.assertFalse(capabilities["can_merge_prs"])

    def test_permission_only_view_is_explicit(self):
        capabilities = mcp_server._build_runtime_task_capabilities(
            ["gitea.read", "gitea.pr.merge"],
            [],
            CONFIG,
            active_role_kind=None,
        )
        merge_entry = next(
            item
            for item in capabilities["task_capabilities"]
            if item["task"] == "merge_pr"
        )
        self.assertEqual(merge_entry["capability_view"], "permission_only")
        self.assertTrue(merge_entry["allowed_in_current_session"])

    def test_matching_profiles_honor_declared_roles(self):
        capabilities = mcp_server._build_runtime_task_capabilities(
            ["gitea.read"],
            [],
            CONFIG,
            active_role_kind="author",
        )
        review_entry = next(
            item
            for item in capabilities["task_capabilities"]
            if item["task"] == "review_pr"
        )
        merge_entry = next(
            item
            for item in capabilities["task_capabilities"]
            if item["task"] == "merge_pr"
        )
        self.assertEqual(
            review_entry["matching_configured_profiles"], ["prgs-reviewer"]
        )
        self.assertEqual(
            merge_entry["matching_configured_profiles"], ["prgs-merger"]
        )


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