"""Workstream B: closed permission registry (design §3.2, §6)."""

import asyncio
from datetime import UTC, datetime
from types import SimpleNamespace
from uuid import uuid4

import pytest
from src.core.exceptions import ConflictError, ValidationError, to_http_exception
from src.domain.project_auth import permissions as perms
from src.domain.project_auth.models import AuthAssignmentTier, AuthRoleCreator
from src.domain.project_auth.schemas import CreateAuthRoleRequest, UpdateAuthRoleRequest
from src.domain.project_auth.service import ProjectAuthService


def _validate(values, *, protected=False, system_created=False):
    return perms.validate_role_permissions(
        values, protected=protected, system_created=system_created
    )


def _code(exc_info):
    return exc_info.value.public_error


ASSIGNABLE_ON_USER_ROLES = sorted(
    perms.VOCABULARY - perms.NEVER_ROLE_ASSIGNABLE - perms.PROTECTED_ONLY
)


@pytest.mark.parametrize("permission", ASSIGNABLE_ON_USER_ROLES)
def test_each_assignable_vocabulary_permission_is_accepted_on_user_roles(permission):
    assert _validate([permission]) == [permission]


@pytest.mark.parametrize("permission", sorted(perms.NEVER_ROLE_ASSIGNABLE))
def test_owner_grant_and_revoke_never_assignable(permission):
    with pytest.raises(ValidationError) as exc:
        _validate([permission], protected=True, system_created=True)
    assert _code(exc) == "permission_not_assignable"


def test_owner_transfer_only_on_protected_roles():
    with pytest.raises(ValidationError) as exc:
        _validate([perms.OWNER_TRANSFER])
    assert _code(exc) == "permission_protected_only"
    assert _validate([perms.OWNER_TRANSFER], protected=True) == [perms.OWNER_TRANSFER]


def test_legacy_super_admin_only_on_system_created_roles():
    with pytest.raises(ValidationError) as exc:
        _validate([perms.LEGACY_SUPER_ADMIN])
    assert _code(exc) == "permission_deprecated"
    assert _validate([perms.LEGACY_SUPER_ADMIN], system_created=True) == [perms.LEGACY_SUPER_ADMIN]


def test_unknown_permission_rejected_and_maps_to_422():
    with pytest.raises(ValidationError) as exc:
        _validate(["invoices.delete", "staff"])
    assert _code(exc) == "unknown_permission"
    assert "invoices.delete" in str(exc.value.message)
    assert to_http_exception(exc.value).status_code == 422


def test_duplicates_are_removed_in_order():
    assert _validate(["staff", "billing", "staff"]) == ["staff", "billing"]


def test_owner_preset_is_valid_and_excludes_owner_role_mutation():
    owner = _validate(perms.OWNER_PRESET, protected=True, system_created=True)
    assert set(owner) == set(perms.OWNER_PRESET)
    assert perms.ROLE_GRANT_OWNER not in owner
    assert perms.ROLE_REVOKE_OWNER not in owner
    assert perms.OWNER_TRANSFER in owner


def test_admin_preset_cannot_suspend_admins_or_touch_owner():
    admin = set(perms.ADMIN_PRESET)
    assert set(_validate(perms.ADMIN_PRESET)) == admin
    assert perms.MEMBER_SUSPEND_ADMIN not in admin
    assert perms.MEMBER_REINSTATE_ADMIN not in admin
    assert perms.OWNER_TRANSFER not in admin
    assert perms.ROLE_GRANT_OWNER not in admin
    assert {perms.ROLE_GRANT_ADMIN, perms.ROLE_REVOKE_ADMIN} <= admin


def test_presets_use_only_known_names():
    for preset in (perms.OWNER_PRESET, perms.ADMIN_PRESET, perms.STAFF_PRESET, perms.MEMBER_PRESET):
        assert set(preset) <= perms.ACCEPTED_NAMES


class _FakeConfigRepo:
    async def get_by_project(self, project_id):
        return None


class _FakeRoleRepo:
    def __init__(self, existing=None):
        self.existing = existing
        self.created = []
        self.updated = []
        self.deleted = []

    async def get_by_name(self, project_id, name):
        return self.existing

    async def create(self, **fields):
        self.created.append(fields)
        now = datetime.now(UTC)
        base = dict(
            id=uuid4(), description=None, is_default=False, protected=False,
            created_at=now, updated_at=now,
        )
        base.update(fields)
        return SimpleNamespace(**base)

    async def update(self, row, **fields):
        self.updated.append(fields)
        return row

    async def delete(self, row):
        self.deleted.append(row)

    async def clear_default_flag(self, project_id):
        return None


def _service(role_repo):
    return ProjectAuthService(
        config_repo=_FakeConfigRepo(), key_repo=None, role_repo=role_repo, cloud_client=None
    )


def _row(**overrides):
    base = dict(
        id=uuid4(), project_id=uuid4(), name="editor", description=None, is_default=False,
        permissions_json=[], created_by=AuthRoleCreator.user, protected=False,
        assignment_tier=AuthAssignmentTier.staff,
        created_at=datetime.now(UTC), updated_at=datetime.now(UTC),
    )
    base.update(overrides)
    return SimpleNamespace(**base)


def test_service_create_unknown_permission_returns_422_and_writes_nothing():
    repo = _FakeRoleRepo()
    body = CreateAuthRoleRequest(
        name="editor", permissions=["invoices.delete"], assignment_tier="staff"
    )
    with pytest.raises(ValidationError) as exc:
        asyncio.run(_service(repo).create_role(uuid4(), body))
    assert to_http_exception(exc.value).status_code == 422
    assert repo.created == []


def test_service_create_ignores_client_supplied_codegen_creator():
    repo = _FakeRoleRepo()
    body = CreateAuthRoleRequest(name="editor", permissions=["staff"], assignment_tier="staff")
    asyncio.run(_service(repo).create_role(uuid4(), body))
    assert repo.created[0]["created_by"] == AuthRoleCreator.user
    assert repo.created[0]["permissions_json"] == ["staff"]


def test_service_create_rejects_owner_grant_permission():
    repo = _FakeRoleRepo()
    body = CreateAuthRoleRequest(
        name="pseudo_owner", permissions=["member.role.grant_owner"], assignment_tier="admin"
    )
    with pytest.raises(ValidationError) as exc:
        asyncio.run(_service(repo).create_role(uuid4(), body))
    assert _code(exc) == "permission_not_assignable"
    assert repo.created == []


def test_service_update_validates_permissions():
    repo = _FakeRoleRepo(existing=_row())
    body = UpdateAuthRoleRequest(permissions=["nope"])
    with pytest.raises(ValidationError):
        asyncio.run(_service(repo).update_role(uuid4(), "editor", body))
    assert repo.updated == []


def test_service_protected_role_cannot_be_updated_or_deleted():
    owner = _row(name="owner", protected=True, created_by=AuthRoleCreator.codegen)
    repo = _FakeRoleRepo(existing=owner)
    with pytest.raises(ConflictError):
        asyncio.run(
            _service(repo).update_role(uuid4(), "owner", UpdateAuthRoleRequest(description="x"))
        )
    with pytest.raises(ConflictError):
        asyncio.run(_service(repo).delete_role(uuid4(), "owner"))
    assert repo.updated == [] and repo.deleted == []


def test_service_codegen_role_may_keep_legacy_permission_on_update():
    repo = _FakeRoleRepo(existing=_row(name="super_admin", created_by=AuthRoleCreator.codegen))
    body = UpdateAuthRoleRequest(permissions=["super_admin", "staff"])
    asyncio.run(_service(repo).update_role(uuid4(), "super_admin", body))
    assert repo.updated[0]["permissions_json"] == ["super_admin", "staff"]


def test_service_create_requires_and_stores_assignment_tier():
    repo = _FakeRoleRepo()
    body = CreateAuthRoleRequest(name="finance", permissions=["billing"], assignment_tier="staff")
    asyncio.run(_service(repo).create_role(uuid4(), body))
    assert repo.created[0]["assignment_tier"] == "staff"


def test_service_assignment_tier_fixed_once_set():
    repo = _FakeRoleRepo(existing=_row(assignment_tier=AuthAssignmentTier.staff))
    body = UpdateAuthRoleRequest(assignment_tier="admin")
    with pytest.raises(ConflictError):
        asyncio.run(_service(repo).update_role(uuid4(), "editor", body))
    assert repo.updated == []


def test_service_assignment_tier_can_be_classified_when_unset():
    repo = _FakeRoleRepo(existing=_row(assignment_tier=None))
    body = UpdateAuthRoleRequest(assignment_tier="member")
    asyncio.run(_service(repo).update_role(uuid4(), "editor", body))
    assert repo.updated[0]["assignment_tier"] == "member"


def test_create_request_without_tier_is_rejected():
    with pytest.raises(Exception):
        CreateAuthRoleRequest(name="editor", permissions=["staff"])
