"""Workstream C: policy matrix for project authorization (design §3.3–3.6).

Expected outcomes are transcribed from the design, not computed from the engine.
"""

from uuid import uuid4

import pytest
from src.domain.project_auth import permissions as perms
from src.domain.project_auth.authorization import (
    Action,
    Actor,
    AssignmentTier,
    MembershipState,
    RoleDescriptor,
    Target,
    authorize_role_change,
    authorize_suspension,
    can,
    check_owner_invariant,
    permissions_from_roles,
)

PROJECT = uuid4()
OTHER_PROJECT = uuid4()

ACTIVE = MembershipState.active
SUSPENDED = MembershipState.suspended

OK = "ok"
PD = "permission_denied"
RP = "role_protected"
BM = "boundary_mismatch"
MI = "membership_inactive"
SE = "self_suspend_denied"


def _actor_spec(project, state, perm_set):
    return Actor(project, uuid4(), state, frozenset(perm_set))


ACTORS = {
    "owner": _actor_spec(PROJECT, ACTIVE, perms.OWNER_PRESET),
    "admin": _actor_spec(PROJECT, ACTIVE, perms.ADMIN_PRESET),
    "staff": _actor_spec(PROJECT, ACTIVE, perms.STAFF_PRESET),
    "member": _actor_spec(PROJECT, ACTIVE, perms.MEMBER_PRESET),
    "suspended_admin": _actor_spec(PROJECT, SUSPENDED, perms.ADMIN_PRESET),
    "admin_other_project": _actor_spec(OTHER_PROJECT, ACTIVE, perms.ADMIN_PRESET),
}


def _target(project=PROJECT, state=ACTIVE, is_owner=False, holds_admin=False):
    return Target(project, uuid4(), state, is_owner, holds_admin)


TARGETS = {
    "owner": _target(is_owner=True),
    "admin": _target(holds_admin=True),
    "staff": _target(),
    "member": _target(),
    "suspended_admin": _target(state=SUSPENDED, holds_admin=True),
    "other_project_member": _target(project=OTHER_PROJECT),
}

# Role-change operations: (action, assignment tier of the role being changed)
ROLE_OPS = {
    "grant_admin": (Action.grant, AssignmentTier.admin),
    "revoke_admin": (Action.revoke, AssignmentTier.admin),
    "grant_staff": (Action.grant, AssignmentTier.staff),
    "revoke_staff": (Action.revoke, AssignmentTier.staff),
    "grant_member": (Action.grant, AssignmentTier.member),
    "revoke_member": (Action.revoke, AssignmentTier.member),
}

# Expected outcomes per actor -> per target -> six role operations (design §3.3-3.4).
ROLE_EXPECTED = {
    "owner": {
        "owner": [RP] * 6,
        "admin": [OK] * 6,
        "staff": [OK] * 6,
        "member": [OK] * 6,
        "suspended_admin": [OK] * 6,
        "other_project_member": [BM] * 6,
    },
    "admin": {
        "owner": [RP] * 6,
        "admin": [OK] * 6,
        "staff": [OK] * 6,
        "member": [OK] * 6,
        "suspended_admin": [OK] * 6,
        "other_project_member": [BM] * 6,
    },
    "staff": {
        "owner": [RP] * 6,
        "admin": [PD] * 6,
        "staff": [PD] * 6,
        "member": [PD] * 6,
        "suspended_admin": [PD] * 6,
        "other_project_member": [BM] * 6,
    },
    "member": {
        "owner": [RP] * 6,
        "admin": [PD] * 6,
        "staff": [PD] * 6,
        "member": [PD] * 6,
        "suspended_admin": [PD] * 6,
        "other_project_member": [BM] * 6,
    },
    "suspended_admin": {
        "owner": [MI] * 6,
        "admin": [MI] * 6,
        "staff": [MI] * 6,
        "member": [MI] * 6,
        "suspended_admin": [MI] * 6,
        "other_project_member": [BM] * 6,
    },
    "admin_other_project": {
        "owner": [BM] * 6,
        "admin": [BM] * 6,
        "staff": [BM] * 6,
        "member": [BM] * 6,
        "suspended_admin": [BM] * 6,
        "other_project_member": [OK] * 6,
    },
}

# Suspension operations: (suspend, reinstate)
SUSPEND_EXPECTED = {
    "owner": {
        "owner": [RP, RP],
        "admin": [OK, OK],
        "staff": [OK, OK],
        "member": [OK, OK],
        "suspended_admin": [OK, OK],
        "other_project_member": [BM, BM],
    },
    "admin": {
        "owner": [RP, RP],
        "admin": [PD, PD],
        "staff": [OK, OK],
        "member": [OK, OK],
        "suspended_admin": [PD, PD],
        "other_project_member": [BM, BM],
    },
    "staff": {
        "owner": [RP, RP],
        "admin": [PD, PD],
        "staff": [PD, PD],
        "member": [PD, PD],
        "suspended_admin": [PD, PD],
        "other_project_member": [BM, BM],
    },
    "member": {
        "owner": [RP, RP],
        "admin": [PD, PD],
        "staff": [PD, PD],
        "member": [PD, PD],
        "suspended_admin": [PD, PD],
        "other_project_member": [BM, BM],
    },
    "suspended_admin": {
        "owner": [MI, MI],
        "admin": [MI, MI],
        "staff": [MI, MI],
        "member": [MI, MI],
        "suspended_admin": [MI, MI],
        "other_project_member": [BM, BM],
    },
    "admin_other_project": {
        "owner": [BM, BM],
        "admin": [BM, BM],
        "staff": [BM, BM],
        "member": [BM, BM],
        "suspended_admin": [BM, BM],
        "other_project_member": [OK, OK],
    },
}


def _code(decision):
    return OK if decision.allowed else decision.code


def _role_descriptor(tier, *, protected=False):
    return RoleDescriptor(uuid4(), tier, protected, frozenset())


def _role(actor, target, action, role, project=PROJECT):
    return _code(authorize_role_change(actor, target, role, project_id=project, action=action))


def _susp(actor, target, action, project=PROJECT):
    return _code(authorize_suspension(actor, target, project_id=project, action=action))


@pytest.mark.parametrize("actor_key", sorted(ROLE_EXPECTED))
@pytest.mark.parametrize("target_key", sorted(TARGETS))
def test_role_change_matrix(actor_key, target_key):
    actor = ACTORS[actor_key]
    target = TARGETS[target_key]
    expected = ROLE_EXPECTED[actor_key][target_key]
    for (op_name, (action, tier)), want in zip(ROLE_OPS.items(), expected):
        got = _role(actor, target, action, _role_descriptor(tier), project=target.project_id)
        assert got == want, f"{actor_key} -> {target_key} {op_name}: got {got}, want {want}"


@pytest.mark.parametrize("actor_key", sorted(SUSPEND_EXPECTED))
@pytest.mark.parametrize("target_key", sorted(TARGETS))
def test_suspension_matrix(actor_key, target_key):
    actor = ACTORS[actor_key]
    target = TARGETS[target_key]
    expected = SUSPEND_EXPECTED[actor_key][target_key]
    ops = (("suspend", Action.suspend), ("reinstate", Action.reinstate))
    for (op_name, action), want in zip(ops, expected):
        got = _susp(actor, target, action, project=target.project_id)
        assert got == want, f"{actor_key} -> {target_key} {op_name}: got {got}, want {want}"


def test_matrices_cover_every_actor_and_target():
    assert set(ROLE_EXPECTED) == set(ACTORS)
    assert set(SUSPEND_EXPECTED) == set(ACTORS)
    for row in list(ROLE_EXPECTED.values()) + list(SUSPEND_EXPECTED.values()):
        assert set(row) == set(TARGETS)


@pytest.mark.parametrize("op_name", sorted(ROLE_OPS))
def test_protected_role_is_never_changed_by_an_active_in_project_actor(op_name):
    action, tier = ROLE_OPS[op_name]
    protected = _role_descriptor(tier, protected=True)
    for key in ("owner", "admin", "staff", "member"):
        assert _role(ACTORS[key], TARGETS["member"], action, protected) == RP, key


@pytest.mark.parametrize("op_name", sorted(ROLE_OPS))
def test_protected_role_is_denied_to_every_other_actor(op_name):
    action, tier = ROLE_OPS[op_name]
    protected = _role_descriptor(tier, protected=True)
    for key in ("suspended_admin", "admin_other_project"):
        assert _role(ACTORS[key], TARGETS["member"], action, protected) != OK, key


@pytest.mark.parametrize("actor_key", ["owner", "admin", "staff", "member"])
def test_unclassified_role_is_not_assignable_even_with_permission(actor_key):
    unclassified = _role_descriptor(None)
    assert _role(ACTORS[actor_key], TARGETS["member"], Action.grant, unclassified) == (
        "role_tier_unclassified"
    )


def test_admin_cannot_suspend_admin_but_owner_can():
    admin_target = TARGETS["admin"]
    assert _susp(ACTORS["admin"], admin_target, Action.suspend) == PD
    assert _susp(ACTORS["owner"], admin_target, Action.suspend) == OK


def test_self_suspension_denied_for_every_actor_with_a_membership():
    for key in ("owner", "admin", "staff", "member"):
        actor = ACTORS[key]
        self_target = Target(
            actor.project_id, actor.membership_id, ACTIVE, key == "owner", key == "admin"
        )
        assert _susp(actor, self_target, Action.suspend) == SE


def test_admin_self_demotion_allowed_owner_self_demotion_denied():
    admin = ACTORS["admin"]
    self_admin = Target(admin.project_id, admin.membership_id, ACTIVE, False, True)
    assert _role(admin, self_admin, Action.revoke, _role_descriptor(AssignmentTier.admin)) == OK

    owner = ACTORS["owner"]
    self_owner = Target(owner.project_id, owner.membership_id, ACTIVE, True, False)
    assert _role(owner, self_owner, Action.revoke, _role_descriptor(AssignmentTier.admin)) == RP


def test_owner_target_is_never_changed_by_any_actor():
    owner_target = TARGETS["owner"]
    for key, actor in ACTORS.items():
        for action, tier in ROLE_OPS.values():
            assert _role(actor, owner_target, action, _role_descriptor(tier)) != OK, key
        for action in (Action.suspend, Action.reinstate):
            assert _susp(actor, owner_target, action) != OK, key


def test_can_is_default_deny_and_project_bound():
    owner = ACTORS["owner"]
    assert _code(can(owner, perms.OWNER_TRANSFER, PROJECT)) == OK
    assert _code(can(owner, perms.OWNER_TRANSFER, OTHER_PROJECT)) == BM
    assert _code(can(ACTORS["suspended_admin"], perms.MEMBER_READ, PROJECT)) == MI
    assert _code(can(ACTORS["staff"], perms.MEMBER_READ, PROJECT)) == PD
    assert _code(can(ACTORS["owner"], "invoices.delete", PROJECT)) == PD


def test_permissions_from_roles_is_a_union():
    got = permissions_from_roles([["staff"], ["billing", "staff"], []])
    assert got == frozenset({"staff", "billing"})


def test_owner_invariant_requires_exactly_one_active_owner():
    assert _code(check_owner_invariant(1)) == OK
    assert _code(check_owner_invariant(0)) == "owner_invariant"
    assert _code(check_owner_invariant(2)) == "owner_invariant"
