﻿"""Mandatory tests 4-7: permission enforcement, per-user overrides, Deny precedence, delegation limits."""
import datetime

from django.utils import timezone

from apps.access import services as access
from apps.access.models import MembershipRole, Role, UserPermissionOverride
from tests.base import PASSWORD, CoreTestCase


class AccessHelpers:
    def _pid(self, code):
        from apps.access.models import Permission

        return Permission.objects.get(code=code).pk

    def _supervisor(self, delegate):
        """A custom role holding member/role management but (optionally) no delegation rights."""
        root_ctx = self.ctx(self.root, self.org_a)
        role = access.create_role(actor=root_ctx, code="supervisor", name="Supervisor")
        access.set_role_permissions(
            actor=root_ctx, role=role,
            grants=[
                {"code": code, "scope": "organization", "can_delegate": delegate}
                for code in ("members.view", "members.update", "roles.view", "roles.manage", "access.manage", "attendance.view")
            ],
        )
        user = self.make_user("supervisor")
        membership = self.join(user, self.org_a)
        MembershipRole.objects.create(membership=membership, role=role)
        return user


class PermissionEnforcementTests(CoreTestCase):
    def test_4_user_without_permission_cannot_act(self):
        employee = self.api(self.employee_a, self.org_a)
        # deny by default: the employee template holds nothing beyond attendance basics
        for method, path, body in (
            ("get", "/api/v1/members/", None),
            ("get", "/api/v1/roles/", None),
            ("get", "/api/v1/audit-logs/", None),
            ("post", "/api/v1/members/", {"username": "new-person", "password": PASSWORD}),
            ("post", "/api/v1/roles/", {"code": "sneaky", "name": "x"}),
        ):
            response = getattr(employee, method)(path, body, format="json") if body else getattr(employee, method)(path)
            self.assertEqual(response.status_code, 403, path)
            self.assertEqual(response.data["code"], "permission_denied", path)
        self.assertEqual(employee.get("/probe/").status_code, 200)  # what the role does grant works

    def test_view_without_a_permission_mapping_is_denied(self):
        response = self.api(self.root, self.org_a).get("/unmapped/")
        self.assertEqual(response.status_code, 403)
        self.assertEqual(response.data["code"], "permission_denied")
        self.assertNotIn("leaked", response.data)

    def test_even_a_super_admin_cannot_put_platform_only_permissions_into_a_customer_org(self):
        from domain.exceptions import PermissionDelegationError

        root_ctx = self.ctx(self.root, self.org_a)
        self.assertTrue(root_ctx.effective.can_delegate("modules.manage", "organization"))  # root may delegate it...
        role = access.create_role(actor=root_ctx, code="sneaky", name="Sneaky")
        with self.assertRaises(PermissionDelegationError):  # ...but not inside a customer organization
            access.set_role_permissions(
                actor=root_ctx, role=role, grants=[{"code": "modules.manage", "scope": "organization"}]
            )
        with self.assertRaises(PermissionDelegationError):
            access.set_override(
                actor=root_ctx, target=self.membership(self.employee_a, self.org_a),
                permission_code="organizations.create", effect="allow", scope="organization",
            )
        self.assertFalse(role.grants.exists())

    def test_platform_only_permissions_never_work_inside_a_customer_organization(self):
        from apps.access.models import Permission, RolePermission

        role = self.role(self.org_a, "org_manager")
        RolePermission.objects.create(
            role=role, permission=Permission.objects.get(code="modules.manage"), scope="organization"
        )
        ctx = self.ctx(self.manager_a, self.org_a)
        self.assertFalse(ctx.has("modules.manage"))


class OverrideTests(AccessHelpers, CoreTestCase):
    def test_5_per_user_override_grants_and_revokes(self):
        member = self.membership(self.employee_a, self.org_a)
        client = self.api(self.manager_a, self.org_a)

        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/audit-logs/").status_code, 403)
        response = client.put(
            f"/api/v1/members/{member.pk}/overrides/",
            {"permission": "audit.view", "effect": "allow", "scope": "organization", "reason": "supervisor duty"},
            format="json",
        )
        self.assertEqual(response.status_code, 200, response.data)
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/audit-logs/").status_code, 200)
        # ...and only that: nothing else changed
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/members/").status_code, 403)

        listed = client.get(f"/api/v1/members/{member.pk}/overrides/").data
        self.assertEqual([o["permission"] for o in listed], ["audit.view"])

        self.assertEqual(client.delete(f"/api/v1/members/{member.pk}/overrides/audit.view/").status_code, 204)
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/audit-logs/").status_code, 403)

    def test_override_applies_only_inside_its_own_organization(self):
        member = self.membership(self.multi, self.org_a)  # manager in A
        UserPermissionOverride.objects.create(
            membership=self.membership(self.multi, self.org_b),
            permission_id=self._pid("audit.view"), effect="allow", scope="organization",
        )
        self.assertEqual(self.api(self.multi, self.org_b).get("/api/v1/audit-logs/").status_code, 200)
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/audit-logs/").status_code, 403)
        self.assertEqual(member.organization, self.org_a)

    def test_6_explicit_deny_beats_a_role_grant(self):
        target = self.membership(self.manager_a2, self.org_a)
        client = self.api(self.manager_a, self.org_a)
        self.assertEqual(self.api(self.manager_a2, self.org_a).get("/api/v1/members/").status_code, 200)

        response = client.put(
            f"/api/v1/members/{target.pk}/overrides/", {"permission": "members.create", "effect": "deny"}, format="json"
        )
        self.assertEqual(response.status_code, 200)

        denied = self.api(self.manager_a2, self.org_a).post(
            "/api/v1/members/", {"username": "late-hire", "password": PASSWORD}, format="json"
        )
        self.assertEqual(denied.status_code, 403)
        self.assertEqual(self.api(self.manager_a2, self.org_a).get("/api/v1/members/").status_code, 200)

    def test_deny_override_does_not_need_delegation_rights(self):
        supervisor = self._supervisor(delegate=False)
        target = self.membership(self.employee_a, self.org_a)
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "attendance.create", "effect": "deny"}, format="json",
        )
        self.assertEqual(response.status_code, 200, response.data)
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/probe/").status_code, 200)
        self.assertFalse(self.ctx(self.employee_a, self.org_a).has("attendance.create"))

    def test_expired_override_is_ignored(self):
        UserPermissionOverride.objects.create(
            membership=self.membership(self.employee_a, self.org_a), permission_id=self._pid("audit.view"),
            effect="allow", scope="organization", expires_at=timezone.now() - datetime.timedelta(hours=1),
        )
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/api/v1/audit-logs/").status_code, 403)

class DelegationTests(AccessHelpers, CoreTestCase):
    """Mandatory test 7: an organization manager cannot hand out access beyond what they may delegate."""

    def test_7_supervisor_cannot_grant_what_they_do_not_hold(self):
        supervisor = self._supervisor(delegate=True)
        target = self.membership(self.employee_a, self.org_a)
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "audit.view", "effect": "allow", "scope": "organization"}, format="json",
        )
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["code"], "permission_delegation")
        self.assertEqual(response.data["permissions"], ["audit.view"])
        self.assertFalse(self.ctx(self.employee_a, self.org_a).has("audit.view"))

    def test_7_holding_a_permission_without_delegation_rights_is_not_enough(self):
        supervisor = self._supervisor(delegate=False)
        target = self.membership(self.employee_a, self.org_a)
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "attendance.view", "effect": "allow", "scope": "organization"}, format="json",
        )
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["code"], "permission_delegation")

    def test_7_cannot_hand_out_a_wider_scope_than_held(self):
        supervisor = self._supervisor(delegate=True)  # holds attendance.view at organization scope
        target = self.membership(self.employee_a, self.org_a)
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "attendance.view", "effect": "allow", "scope": "all"}, format="json",
        )
        self.assertEqual(response.status_code, 400)  # 'all' is not even valid for a customer organization

    def test_7_assigning_a_role_is_limited_by_the_permissions_inside_it(self):
        supervisor = self._supervisor(delegate=True)
        target = self.membership(self.employee_a, self.org_a)
        manager_role = self.role(self.org_a, "org_manager")
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/members/{target.pk}/roles/",
            {"role_ids": [manager_role.pk, self.role(self.org_a, "employee").pk]}, format="json",
        )
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["code"], "permission_delegation")
        self.assertFalse(MembershipRole.objects.filter(membership=target, role=manager_role).exists())

    def test_7_nobody_can_change_their_own_access(self):
        own = self.membership(self.manager_a, self.org_a)
        client = self.api(self.manager_a, self.org_a)
        roles = client.put(f"/api/v1/members/{own.pk}/roles/", {"role_ids": []}, format="json")
        override = client.put(
            f"/api/v1/members/{own.pk}/overrides/",
            {"permission": "attendance.view", "effect": "allow", "scope": "organization"}, format="json",
        )
        for response in (roles, override):
            self.assertEqual(response.status_code, 400)
            self.assertEqual(response.data["code"], "permission_delegation")
        self.assertTrue(MembershipRole.objects.filter(membership=own).exists())

    def test_7_cannot_edit_a_role_that_is_assigned_to_you(self):
        supervisor = self._supervisor(delegate=True)
        own_role = Role.objects.get(organization=self.org_a, code="supervisor")
        response = self.api(supervisor, self.org_a).put(
            f"/api/v1/roles/{own_role.pk}/permissions/",
            {"grants": [{"code": "attendance.view", "scope": "organization", "can_delegate": True},
                        {"code": "audit.view", "scope": "organization", "can_delegate": True}]},
            format="json",
        )
        self.assertEqual(response.status_code, 400)
        self.assertFalse(own_role.grants.filter(permission__code="audit.view").exists())

    def test_7_roles_cannot_contain_platform_only_permissions_or_platform_scopes(self):
        client = self.api(self.manager_a, self.org_a)
        created = client.post("/api/v1/roles/", {"code": "ops", "name": "Ops"}, format="json")
        self.assertEqual(created.status_code, 201)
        url = f"/api/v1/roles/{created.data['id']}/permissions/"

        platform_only = client.put(url, {"grants": [{"code": "modules.manage", "scope": "organization"}]}, format="json")
        self.assertEqual(platform_only.status_code, 400)
        self.assertEqual(platform_only.data["code"], "permission_delegation")

        wide_scope = client.put(url, {"grants": [{"code": "attendance.view", "scope": "all"}]}, format="json")
        self.assertEqual(wide_scope.status_code, 400)

    def test_manager_with_delegation_rights_can_grant_what_they_hold(self):
        target = self.membership(self.employee_a, self.org_a)
        client = self.api(self.manager_a, self.org_a)
        response = client.put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "attendance.report", "effect": "allow", "scope": "organization", "can_delegate": False},
            format="json",
        )
        self.assertEqual(response.status_code, 200, response.data)
        self.assertTrue(self.ctx(self.employee_a, self.org_a).has("attendance.report", "organization"))

    def test_custom_role_lifecycle_through_the_api(self):
        client = self.api(self.manager_a, self.org_a)
        role = client.post("/api/v1/roles/", {"code": "accountant", "name": "حسابدار"}, format="json").data
        updated = client.put(
            f"/api/v1/roles/{role['id']}/permissions/",
            {"grants": [{"code": "attendance.view", "scope": "organization"}, {"code": "attendance.report", "scope": "organization"}]},
            format="json",
        )
        self.assertEqual(updated.status_code, 200, updated.data)
        self.assertEqual(len(updated.data["grants"]), 2)

        member = self.membership(self.employee_a, self.org_a)
        assigned = client.put(
            f"/api/v1/members/{member.pk}/roles/",
            {"role_ids": [self.role(self.org_a, "employee").pk, role["id"]]}, format="json",
        )
        self.assertEqual(assigned.status_code, 200, assigned.data)
        self.assertTrue(self.ctx(self.employee_a, self.org_a).has("attendance.report", "organization"))

        self.assertEqual(client.delete(f"/api/v1/roles/{role['id']}/").status_code, 400)  # still assigned
        client.put(f"/api/v1/members/{member.pk}/roles/", {"role_ids": [self.role(self.org_a, "employee").pk]}, format="json")
        self.assertEqual(client.delete(f"/api/v1/roles/{role['id']}/").status_code, 204)
        system_role = self.role(self.org_a, "employee")
        self.assertEqual(client.delete(f"/api/v1/roles/{system_role.pk}/").status_code, 400)

    def test_every_access_change_is_audited(self):
        from apps.audit.models import AuditLog

        target = self.membership(self.employee_a, self.org_a)
        self.api(self.manager_a, self.org_a).put(
            f"/api/v1/members/{target.pk}/overrides/",
            {"permission": "attendance.report", "effect": "allow", "scope": "organization"}, format="json",
        )
        entry = AuditLog.objects.filter(organization=self.org_a, action="member.override_set").latest("id")
        self.assertEqual(entry.actor, self.manager_a)
        self.assertEqual(entry.entity_id, str(target.pk))
        self.assertEqual(entry.after["permission"], "attendance.report")

