"""Invariant tests pinning task_capability_map role assignments (#722/#723).

Added with the operator-authorized break-glass repair for incident #722:
commit 970e68b remapped ten reviewer tasks to ``role="merger"`` while every
configured merger profile forbids the review permissions, so no configured
profile could resolve any formal review task (``matching_configured_profile``
was empty repository-wide). These tests fail loudly if that class of
regression recurs:

- every role-exclusive formal-review task must be satisfiable by at least one
  canonical role profile (permission AND role together);
- the capability map must agree with ``role_session_router`` task sets;
- ``adopt_merger_pr_lease`` stays merger-only (the legitimate hunk of
  970e68b, preserved by the repair);
- merger profiles must not be able to resolve review_pr/approve_pr.
"""

import unittest

import gitea_config
from role_session_router import MERGER_TASKS, REVIEWER_TASKS
from task_capability_map import (
    ROLE_EXCLUSIVE_TASKS,
    TASK_CAPABILITY_MAP,
    required_permission,
    required_role,
)

# Canonical role-profile permission shape. Mirrors the configured
# author/reviewer/merger/reconciler profiles (profiles.json v2 role split):
# reviewers review/approve/request changes but never merge; mergers merge but
# never review/approve/request changes.
CANONICAL_ROLE_PROFILES = {
    "author": {
        "allowed": [
            "gitea.read",
            "gitea.branch.create",
            "gitea.branch.push",
            "gitea.repo.commit",
            "gitea.pr.create",
            "gitea.pr.comment",
            "gitea.issue.create",
            "gitea.issue.comment",
            "gitea.issue.close",
        ],
        "forbidden": [
            "gitea.pr.approve",
            "gitea.pr.request_changes",
            "gitea.pr.merge",
        ],
    },
    "reviewer": {
        "allowed": [
            "gitea.read",
            "gitea.pr.review",
            "gitea.pr.approve",
            "gitea.pr.request_changes",
            "gitea.pr.comment",
            "gitea.issue.comment",
        ],
        "forbidden": [
            "gitea.branch.create",
            "gitea.branch.push",
            "gitea.repo.commit",
            "gitea.pr.create",
            "gitea.pr.merge",
        ],
    },
    "merger": {
        "allowed": [
            "gitea.read",
            "gitea.pr.merge",
            "gitea.pr.comment",
            "gitea.issue.comment",
        ],
        "forbidden": [
            "gitea.branch.create",
            "gitea.branch.push",
            "gitea.repo.commit",
            "gitea.pr.create",
            "gitea.pr.approve",
            "gitea.pr.review",
            "gitea.pr.request_changes",
        ],
    },
    "reconciler": {
        "allowed": [
            "gitea.read",
            "gitea.pr.close",
            "gitea.pr.comment",
            "gitea.issue.comment",
            "gitea.branch.delete",
            "gitea.decision_lock.irrecoverable_recovery",
        ],
        "forbidden": [
            "gitea.pr.approve",
            "gitea.pr.merge",
            "gitea.pr.review",
            "gitea.pr.request_changes",
            "gitea.pr.create",
            "gitea.branch.create",
            "gitea.branch.push",
            "gitea.repo.commit",
        ],
    },
}

# Role-exclusive formal-review tasks (mirrors the resolver's role-exclusive
# handling for review work): permission alone is not enough — the profile's
# role kind must also match, so both dimensions are pinned here.
FORMAL_REVIEW_TASKS = (
    "review_pr",
    "approve_pr",
    "request_changes_pr",
    "blind_pr_queue_review",
    "pr_queue_cleanup",
    "pr-queue-cleanup",
)

# Complete resolver role-exclusive set on master when #723 was reconstructed.
# The shared constant must replace this exact inline authority without dropping
# later lease and PR-sync aliases added after the preserved source commits.
EXPECTED_ROLE_EXCLUSIVE_TASKS = frozenset(
    {
        "acquire_reviewer_pr_lease",
        "gitea_acquire_reviewer_pr_lease",
        "review_pr",
        "approve_pr",
        "request_changes_pr",
        "blind_pr_queue_review",
        "pr_queue_cleanup",
        "pr-queue-cleanup",
        "merge_pr",
        "acquire_merger_pr_lease",
        "gitea_acquire_merger_pr_lease",
        "adopt_merger_pr_lease",
        "gitea_adopt_merger_pr_lease",
        "release_merger_pr_lease",
        "gitea_release_merger_pr_lease",
        "create_branch",
        "push_branch",
        "create_pr",
        "commit_files",
        "gitea_commit_files",
        "address_pr_change_requests",
        "update_pr_branch_by_merge",
        "gitea_update_pr_branch_by_merge",
        "delete_branch",
        "cleanup_merged_pr_branch",
        "reconciliation_cleanup",
        "work_issue",
        "work-issue",
    }
)


def _profile_satisfies(role_name, task):
    """True when the canonical *role_name* profile can perform *task*."""
    profile = CANONICAL_ROLE_PROFILES[role_name]
    ok, _reason = gitea_config.check_operation(
        required_permission(task), profile["allowed"], profile["forbidden"]
    )
    return ok and role_name == required_role(task)


class TestFormalReviewProfileCoverage(unittest.TestCase):
    """#722: some configured profile must be able to formally review."""

    def test_every_formal_review_task_has_a_satisfying_role_profile(self):
        for task in FORMAL_REVIEW_TASKS:
            with self.subTest(task=task):
                satisfying = [
                    role
                    for role in CANONICAL_ROLE_PROFILES
                    if _profile_satisfies(role, task)
                ]
                self.assertTrue(
                    satisfying,
                    f"no canonical role profile satisfies both permission "
                    f"{required_permission(task)!r} and role "
                    f"{required_role(task)!r} for task {task!r} — formal "
                    f"review would be impossible for every configured "
                    f"profile (incident #722)",
                )

    def test_formal_review_tasks_are_reviewer_role(self):
        for task in FORMAL_REVIEW_TASKS:
            with self.subTest(task=task):
                self.assertEqual(required_role(task), "reviewer")


class TestMapRouterAgreement(unittest.TestCase):
    """#723 AC2: the map and the role session router must not drift."""

    def test_reviewer_tasks_map_to_reviewer_role(self):
        for task in sorted(REVIEWER_TASKS):
            with self.subTest(task=task):
                self.assertEqual(
                    required_role(task),
                    "reviewer",
                    f"router classifies {task!r} as a reviewer task but the "
                    f"capability map assigns role {required_role(task)!r}",
                )

    def test_merger_tasks_map_to_merger_role(self):
        for task in sorted(MERGER_TASKS):
            with self.subTest(task=task):
                self.assertEqual(
                    required_role(task),
                    "merger",
                    f"router classifies {task!r} as a merger task but the "
                    f"capability map assigns role {required_role(task)!r}",
                )


class TestMergerBoundary(unittest.TestCase):
    """Preserve the legitimate hunk of 970e68b and the merger fence."""

    def test_adopt_merger_pr_lease_requires_merger_role(self):
        self.assertEqual(required_role("adopt_merger_pr_lease"), "merger")
        self.assertEqual(
            required_permission("adopt_merger_pr_lease"), "gitea.pr.comment"
        )

    def test_merge_pr_requires_merger_role(self):
        self.assertEqual(required_role("merge_pr"), "merger")
        self.assertEqual(required_permission("merge_pr"), "gitea.pr.merge")

    def test_merger_profile_cannot_resolve_formal_review_tasks(self):
        merger = CANONICAL_ROLE_PROFILES["merger"]
        for task in ("review_pr", "approve_pr", "request_changes_pr"):
            with self.subTest(task=task):
                ok, reason = gitea_config.check_operation(
                    required_permission(task),
                    merger["allowed"],
                    merger["forbidden"],
                )
                self.assertFalse(
                    ok,
                    f"merger profile must not hold {task!r} permission "
                    f"(got reason {reason!r})",
                )


class TestRoleExclusiveSetIntegrity(unittest.TestCase):
    """#723: the shared set is complete, mapped, and role-satisfiable."""

    def test_complete_current_role_exclusive_set(self):
        self.assertEqual(ROLE_EXCLUSIVE_TASKS, EXPECTED_ROLE_EXCLUSIVE_TASKS)

    def test_every_role_exclusive_task_exists_in_capability_map(self):
        for task in sorted(ROLE_EXCLUSIVE_TASKS):
            with self.subTest(task=task):
                self.assertIn(task, TASK_CAPABILITY_MAP)

    def test_formal_review_tasks_are_role_exclusive(self):
        self.assertTrue(set(FORMAL_REVIEW_TASKS) <= ROLE_EXCLUSIVE_TASKS)

    def test_every_role_exclusive_task_has_a_satisfying_profile(self):
        for task in sorted(ROLE_EXCLUSIVE_TASKS):
            with self.subTest(task=task):
                role = required_role(task)
                self.assertIn(role, CANONICAL_ROLE_PROFILES)
                self.assertTrue(
                    _profile_satisfies(role, task),
                    f"canonical {role!r} profile cannot satisfy {task!r}",
                )


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