import uuid
from uuid import UUID

import pytest
from flask import session, url_for
from flask_login import login_user
from sqlalchemy.exc import NoResultFound
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound

from app import GrantStatusEnum
from app.common.auth.decorators import (
    access_grant_funding_login_required,
    collection_is_editable,
    collection_is_open_for_sign_up,
    deliver_grant_funding_login_required,
    has_access_grant_recipient_role,
    has_access_grant_role,
    has_deliver_grant_role,
    has_feature_flag_enabled,
    is_access_org_member,
    is_deliver_grant_funding_user,
    is_deliver_org_admin,
    is_deliver_org_member,
    is_platform_admin,
    is_signing_up,
    redirect_if_authenticated,
)
from app.common.data import interfaces
from app.common.data.types import AuthMethodEnum, CollectionStatusEnum, CollectionType, RoleEnum
from app.common.helpers.feature_flags import StaticFeatureFlag
from tests.models import _get_grant_managing_organisation


class TestDeliverGrantFundingLoginRequired:
    def test_logged_in_user_gets_response(self, app, factories):
        @deliver_grant_funding_login_required
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk", azure_ad_subject_id=None)

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO
        response = test_deliver_grant_funding_login_required()
        assert response == "OK"

    def test_anonymous_user_gets_redirect(self, app):
        @deliver_grant_funding_login_required
        def test_deliver_grant_funding_login_required():
            return "OK"

        response = test_deliver_grant_funding_login_required()
        assert response.status_code == 302
        assert response.location == url_for("auth.sso_sign_in")

    def test_no_session_auth_variable(self, factories, app) -> None:
        @deliver_grant_funding_login_required
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@anything.com", azure_ad_subject_id=None)

        login_user(user)
        with pytest.raises(InternalServerError):
            test_deliver_grant_funding_login_required()


class TestAccessGrantFundingLoginRequired:
    def test_logged_in_user_gets_response(self, app, factories):
        @access_grant_funding_login_required
        def test_access_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@example.com", azure_ad_subject_id=None)

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        response = test_access_grant_funding_login_required()
        assert response == "OK"

    def test_anonymous_user_gets_redirect(self, app):
        @access_grant_funding_login_required
        def test_access_grant_funding_login_required():
            return "OK"

        response = test_access_grant_funding_login_required()
        assert response.status_code == 302
        assert response.location == url_for("auth.request_a_link_to_sign_in")

    def test_signing_up_for_collection_redirects_to_eligible_page(self, app, factories):
        @access_grant_funding_login_required
        def test_access_grant_funding_login_required():
            return "OK"

        grant = factories.grant.create(slug="grant-slug")
        collection = factories.collection.create(slug="collection-slug", grant=grant)
        user = factories.user.create(email="test@example.com", azure_ad_subject_id=None)

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        session["signing_up_for_collection_id"] = collection.id

        response = test_access_grant_funding_login_required()

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.eligible_to_apply", grant_slug=grant.slug, collection_slug=collection.slug
        )

    def test_no_session_auth_variable(self, factories, app) -> None:
        @access_grant_funding_login_required
        def test_access_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@anything.com", azure_ad_subject_id=None)

        login_user(user)
        with pytest.raises(InternalServerError):
            test_access_grant_funding_login_required()


class TestMHCLGLoginRequired:
    def test_logged_in_mhclg_user_gets_response(self, app, factories):
        @is_deliver_grant_funding_user
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO
        response = test_deliver_grant_funding_login_required()
        assert response == "OK"

    def test_non_mhclg_user_is_forbidden(self, app, factories):
        @is_deliver_grant_funding_user
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@anything.com")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.MAGIC_LINK
            test_deliver_grant_funding_login_required()

    def test_anonymous_user_gets_redirect(self, app):
        @is_deliver_grant_funding_user
        def test_deliver_grant_funding_login_required():
            return "OK"

        response = test_deliver_grant_funding_login_required()
        assert response.status_code == 302

    def test_deliver_grant_funding_user_auth_via_magic_link(self, app, factories) -> None:
        @is_deliver_grant_funding_user
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN])

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        response = test_deliver_grant_funding_login_required()
        current_user = interfaces.user.get_current_user()
        assert response.status_code == 302
        assert current_user.is_anonymous is True

    def test_authed_via_magic_link_not_sso(self, app, factories) -> None:
        @is_deliver_grant_funding_user
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.MAGIC_LINK
            test_deliver_grant_funding_login_required()


class TestPlatformAdminRoleRequired:
    def test_logged_in_platform_admin_gets_response(self, app, factories):
        @is_platform_admin
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN])

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_deliver_grant_funding_login_required()
        assert response == "OK"

    def test_non_platform_admin_is_forbidden(self, app, factories):
        @is_platform_admin
        def test_deliver_grant_funding_login_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_grant_funding_login_required()

    def test_anonymous_user_gets_redirect(self, app):
        @is_platform_admin
        def test_deliver_grant_funding_login_required():
            return "OK"

        response = test_deliver_grant_funding_login_required()
        assert response.status_code == 302


class TestDeliverOrgAdminRoleRequired:
    def test_logged_in_deliver_org_admin_gets_response(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(
            user_id=user.id, user=user, permissions=[RoleEnum.ADMIN], organisation=grant.organisation
        )

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_deliver_org_admin_required()
        assert response == "OK"

    def test_platform_admin_gets_response(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN])

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_deliver_org_admin_required()
        assert response == "OK"

    def test_non_deliver_org_admin_is_forbidden(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_admin_required()

    def test_org_admin_without_can_manage_grants_is_forbidden(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        organisation = factories.organisation.create(can_manage_grants=False)
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN], organisation=organisation)

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_admin_required()

    def test_grant_admin_is_forbidden(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_admin_required()

    def test_member_role_is_forbidden(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(
            user_id=user.id, user=user, permissions=[RoleEnum.MEMBER], organisation=grant.organisation
        )

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_admin_required()

    def test_anonymous_user_gets_redirect(self, app):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        response = test_deliver_org_admin_required()
        assert response.status_code == 302

    def test_deliver_org_admin_user_auth_via_magic_link(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(
            user_id=user.id, user=user, permissions=[RoleEnum.ADMIN], organisation=grant.organisation
        )

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        response = test_deliver_org_admin_required()
        current_user = interfaces.user.get_current_user()
        assert response.status_code == 302
        assert current_user.is_anonymous is True

    def test_authed_via_magic_link_not_sso(self, app, factories):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.MAGIC_LINK
            test_deliver_org_admin_required()


class TestDeliverOrgMemberRoleRequired:
    def test_logged_in_deliver_org_member_gets_response(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(
            user_id=user.id, user=user, permissions=[RoleEnum.MEMBER], organisation=grant.organisation
        )

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_deliver_org_member_required()
        assert response == "OK"

    def test_platform_admin_gets_response(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.ADMIN])

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_deliver_org_member_required()
        assert response == "OK"

    def test_non_deliver_org_member_is_forbidden(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_member_required()

    def test_org_member_without_can_manage_grants_is_forbidden(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        organisation = factories.organisation.create(can_manage_grants=False)
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.MEMBER], organisation=organisation)

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_member_required()

    def test_grant_member_is_forbidden(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(user_id=user.id, user=user, permissions=[RoleEnum.MEMBER], grant=grant)

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.SSO
            test_deliver_org_member_required()

    def test_anonymous_user_gets_redirect(self, app):
        @is_deliver_org_admin
        def test_deliver_org_admin_required():
            return "OK"

        response = test_deliver_org_admin_required()
        assert response.status_code == 302

    def test_deliver_org_member_user_auth_via_magic_link(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(
            user_id=user.id, user=user, permissions=[RoleEnum.MEMBER], organisation=grant.organisation
        )

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        response = test_deliver_org_member_required()
        current_user = interfaces.user.get_current_user()
        assert response.status_code == 302
        assert current_user.is_anonymous is True

    def test_authed_via_magic_link_not_sso(self, app, factories):
        @is_deliver_org_member
        def test_deliver_org_member_required():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        with pytest.raises(Forbidden):
            login_user(user)
            session["auth"] = AuthMethodEnum.MAGIC_LINK
            test_deliver_org_member_required()


class TestRedirectIfAuthenticated:
    def test_authenticated_user_gets_redirect(self, app, factories):
        @redirect_if_authenticated
        def test_authenticated_redirect():
            return "OK"

        user = factories.user.create(email="test@communities.gov.uk")

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = test_authenticated_redirect()
        assert response.status_code == 302
        assert response.location == url_for("deliver_grant_funding.list_grants")

    def test_external_authenticated_user_gets_redirected(self, app, factories):
        @redirect_if_authenticated
        def test_authenticated_redirect():
            return "OK"

        user = factories.user.create(email="test@anything.com")

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO
        response = test_authenticated_redirect()
        assert response.status_code == 302
        assert response.location == url_for("access_grant_funding.index")

    def test_anonymous_user_gets_response(self, app):
        @redirect_if_authenticated
        def test_authenticated_redirect():
            return "OK"

        response = test_authenticated_redirect()
        assert response == "OK"


class TestCollectionIsOpenForSignUp:
    def test_without_grant_slug(self, factories):
        collection = factories.collection.create(slug="collection-slug")

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(ValueError, match="Grant slug required"):
            view_func(grant_slug=None, collection_slug=collection.slug)

    def test_without_collection_slug(self, factories):
        grant = factories.grant.create(slug="grant-slug")

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(ValueError, match="Collection slug required"):
            view_func(grant_slug=grant.slug, collection_slug=None)

    def test_unknown_grant_slug(self, factories):
        collection = factories.collection.create(slug="collection-slug")

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NoResultFound):
            view_func(grant_slug="not-a-real-grant", collection_slug=collection.slug)

    def test_unknown_collection_slug(self, factories):
        grant = factories.grant.create(slug="grant-slug")

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NoResultFound):
            view_func(grant_slug=grant.slug, collection_slug="not-a-real-collection")

    def test_grant_not_live(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.DRAFT)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NotFound):
            view_func(grant_slug=grant.slug, collection_slug=collection.slug)

    def test_collection_not_open(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.CLOSED, allow_public_sign_up=True
        )

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NotFound):
            view_func(grant_slug=grant.slug, collection_slug=collection.slug)

    def test_collection_not_allowed_public_sign_up(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=False
        )

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NotFound):
            view_func(grant_slug=grant.slug, collection_slug=collection.slug)

    def test_open_for_public_sign_up(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )

        @collection_is_open_for_sign_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)
        assert response == "OK"


class TestIsSigningUp:
    def test_without_grant_slug(self, factories):
        collection = factories.collection.create(slug="collection-slug")

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(ValueError, match="Grant slug required"):
            view_func(grant_slug=None, collection_slug=collection.slug)

    def test_without_collection_slug(self, factories):
        grant = factories.grant.create(slug="grant-slug")

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(ValueError, match="Collection slug required"):
            view_func(grant_slug=grant.slug, collection_slug=None)

    def test_authenticated_and_signing_up_gets_response(self, app, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )
        user = factories.user.create(email="test@example.com", azure_ad_subject_id=None)

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        login_user(user)
        session["signing_up_for_collection_id"] = collection.id

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)
        assert response == "OK"

    def test_anonymous_user_redirects_to_start_page(self, app, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.public_sign_up_start_page", grant_slug=grant.slug, collection_slug=collection.slug
        )

    def test_anonymous_user_with_session_flag_redirects_to_start_page(self, app, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        session["signing_up_for_collection_id"] = collection.id

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.public_sign_up_start_page", grant_slug=grant.slug, collection_slug=collection.slug
        )

    def test_authenticated_without_session_flag_redirects_to_start_page(self, app, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )
        user = factories.user.create(email="test@example.com", azure_ad_subject_id=None)

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        login_user(user)

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.public_sign_up_start_page", grant_slug=grant.slug, collection_slug=collection.slug
        )

    def test_authenticated_with_mismatched_session_collection_redirects_to_start_page(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.LIVE)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )
        other_collection = factories.collection.create(
            slug="other-collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )
        user = factories.user.create(email="test@example.com", azure_ad_subject_id=None)

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        login_user(user)
        session["signing_up_for_collection_id"] = other_collection.id

        response = view_func(grant_slug=grant.slug, collection_slug=collection.slug)

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.public_sign_up_start_page", grant_slug=grant.slug, collection_slug=collection.slug
        )

    def test_still_guarded_by_collection_is_open_for_sign_up(self, factories):
        grant = factories.grant.create(slug="grant-slug", status=GrantStatusEnum.DRAFT)
        collection = factories.collection.create(
            slug="collection-slug", grant=grant, status=CollectionStatusEnum.OPEN, allow_public_sign_up=True
        )

        @is_signing_up
        def view_func(grant_slug: str, collection_slug: str):
            return "OK"

        with pytest.raises(NotFound):
            view_func(grant_slug=grant.slug, collection_slug=collection.slug)


class TestHasDeliverGrantRole:
    def test_user_no_roles(self, factories):
        user = factories.user.create(email="test.norole@communities.gov.uk")
        grant = factories.grant.create()

        @has_deliver_grant_role(role=RoleEnum.ADMIN)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO
        with pytest.raises(Forbidden) as exc_info:
            view_func(grant_id=grant.id)

        assert "Access denied" in str(exc_info.value)

    def test_admin_user_has_access(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN])

        @has_deliver_grant_role(role=RoleEnum.ADMIN)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(grant_id="abc")
        assert response == "OK"

    def test_no_result_on_non_existent_grant(self, factories):
        user = factories.user.create(email="test.member2@communities.gov.uk")

        @has_deliver_grant_role(role=RoleEnum.ADMIN)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        with pytest.raises(NoResultFound):
            view_func(grant_id=uuid.uuid4())

    def test_without_grant_id(self, factories):
        user = factories.user.create(email="test.member2@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)

        @has_deliver_grant_role(role=RoleEnum.ADMIN)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO
        with pytest.raises(ValueError, match="Grant ID required"):
            view_func(grant_id=None)

    def test_member_user_has_access(self, factories):
        user = factories.user.create(email="test.member@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)

        @has_deliver_grant_role(role=RoleEnum.MEMBER)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(grant_id=grant.id)
        assert response == "OK"

    def test_member_user_denied_for_admin_role(self, factories):
        user = factories.user.create(email="test.member2@communities.gov.uk")
        grant = factories.grant.create()
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)

        @has_deliver_grant_role(role=RoleEnum.ADMIN)
        def view_func(grant_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        with pytest.raises(Forbidden) as e:
            view_func(grant_id=grant.id)
        assert "Access denied" in str(e.value)


class TestCollectionIsEditable:
    def test_grant_admin_can_access_draft_collection(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response == "OK"

    @pytest.mark.parametrize(
        "collection_status",
        [status for status in CollectionStatusEnum if status != CollectionStatusEnum.DRAFT],
    )
    def test_grant_admin_redirected_for_non_draft_collection(self, factories, collection_status):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=collection_status)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302
        assert response.location == url_for("deliver_grant_funding.list_reports", grant_id=grant.id)

    def test_platform_admin_can_access_draft_collection(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=None)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response == "OK"

    @pytest.mark.parametrize(
        "collection_type, expect_redirect_contains",
        (
            (CollectionType.APPLICATION, "/pre-award"),
            (CollectionType.MONITORING_REPORT, "/reports"),
        ),
    )
    def test_platform_admin_redirected_for_non_draft_collection(
        self, factories, collection_type, expect_redirect_contains
    ):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.OPEN, type=collection_type)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=None)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302
        assert response.location.endswith(expect_redirect_contains)

    def test_member_forbidden(self, factories):
        user = factories.user.create(email="test.member@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302
        assert response.location == url_for("deliver_grant_funding.list_reports", grant_id=grant.id)

    def test_anonymous_user_gets_redirect(self, app, factories):
        collection = factories.collection.create(status=CollectionStatusEnum.DRAFT)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302

    def test_magic_link_auth_forbidden(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302
        assert response.location == url_for("auth.sso_sign_in")

    def test_redirects_to_correct_unauthorised_endpoint(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.OPEN)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(collection_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(collection_id=collection.id)
        assert response.status_code == 302
        assert response.location == url_for("deliver_grant_funding.list_reports", grant_id=grant.id)

    def test_with_form_id_parameter(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        form = factories.form.create(collection=collection)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(form_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(form_id=form.id)
        assert response == "OK"

    def test_with_component_id_parameter(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant, status=CollectionStatusEnum.DRAFT)
        form = factories.form.create(collection=collection)
        question = factories.question.create(form=form)
        factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN], grant=grant)

        @collection_is_editable()
        def view_func(component_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.SSO

        response = view_func(component_id=question.id)
        assert response == "OK"


class TestIsAccessOrgMember:
    def test_user_missing_params(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")

        @is_access_org_member
        def view_func():
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(ValueError, match="Organisation ID required."):
            view_func()

    def test_user_non_org_member_rejected(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        grant_recipient = factories.grant_recipient.create(grant=grant)

        @is_access_org_member
        def view_func(organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(Forbidden):
            view_func(organisation_id=grant_recipient.organisation.id)

    def test_user_non_live_grant_not_found(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create(status=GrantStatusEnum.DRAFT)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.MEMBER],
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
        )

        @is_access_org_member
        def view_func(organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(NotFound):
            view_func(organisation_id=grant_recipient.organisation.id, grant_id=grant.id)

    def test_user_org_member_responds(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        grant_recipient = factories.grant_recipient.create(grant=grant)
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.MEMBER],
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
        )

        @is_access_org_member
        def view_func(organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        assert view_func(organisation_id=grant_recipient.organisation.id) == "OK"


class TestHasAccessGrantRole:
    def test_user_no_roles(self, factories):
        user = factories.user.create(email="test.norole@council.gov.uk")
        grant = factories.grant.create()
        organisation = factories.organisation.create(can_manage_grants=False)

        @has_access_grant_role(role=RoleEnum.MEMBER)
        def view_func(grant_id: UUID, organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        with pytest.raises(Forbidden) as exc_info:
            view_func(grant_id=grant.id, organisation_id=organisation.id)

        # blocked by not having access to the organisation
        assert "Forbidden" in str(exc_info.value)

    def test_no_result_organisation_is_not_grant_recipient(self, factories):
        user = factories.user.create(email="test.member@council.gov.uk")
        organisation = factories.organisation.create(can_manage_grants=False)
        grant = factories.grant.create(status=GrantStatusEnum.LIVE)
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], organisation=organisation, grant=None)

        @has_access_grant_role(role=RoleEnum.MEMBER)
        def view_func(grant_id: UUID, organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(NoResultFound):
            view_func(grant_id=grant.id, organisation_id=organisation.id)

    def test_missing_params(self, factories):
        user = factories.user.create(email="test.member@council.gov.uk")
        organisation = factories.organisation.create(can_manage_grants=False)
        factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], organisation=organisation, grant=None)

        @has_access_grant_role(role=RoleEnum.MEMBER)
        def view_func(organisation_id: UUID | None = None):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(ValueError, match="Organisation ID required."):
            view_func()

        with pytest.raises(ValueError, match="Grant ID required."):
            view_func(organisation_id=organisation.id)

    @pytest.mark.parametrize("role", [RoleEnum.MEMBER, RoleEnum.DATA_PROVIDER, RoleEnum.CERTIFIER])
    def test_access_to_valid_grant_recipient(self, factories, role):
        user = factories.user.create(email="test.member@council.gov.uk")
        grant_recipient = factories.grant_recipient.create(grant__status=GrantStatusEnum.LIVE)
        factories.user_role.create(
            user=user, permissions=[role], organisation=grant_recipient.organisation, grant=grant_recipient.grant
        )

        @has_access_grant_role(role=role)
        def view_func(grant_id: UUID, organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK
        response = view_func(grant_id=grant_recipient.grant.id, organisation_id=grant_recipient.organisation.id)
        assert response == "OK"

    def test_valid_but_wrong_role(self, factories):
        user = factories.user.create(email="test.member@council.gov.uk")
        grant_recipient = factories.grant_recipient.create(grant__status=GrantStatusEnum.LIVE)
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.MEMBER],
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
        )

        @has_access_grant_role(role=RoleEnum.CERTIFIER)
        def view_func(grant_id: UUID, organisation_id: UUID):
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(Forbidden) as exc_info:
            view_func(grant_id=grant_recipient.grant.id, organisation_id=grant_recipient.organisation.id)

        assert "Access denied" in str(exc_info.value)

        factories.user_role.create(
            user=user, permissions=[RoleEnum.CERTIFIER], organisation=grant_recipient.organisation
        )

        response = view_func(grant_id=grant_recipient.grant.id, organisation_id=grant_recipient.organisation.id)
        assert response == "OK"


class TestHasGrantRecipientRole:
    def test_user_no_gr_roles_rejected(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")

        @has_access_grant_recipient_role
        def view_func():
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(Forbidden):
            view_func()

    def test_user_only_deliver_roles_rejected(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        mchlg = _get_grant_managing_organisation()
        grant = factories.grant.create()
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.MEMBER],
            organisation=mchlg,
            grant=grant,
        )

        @has_access_grant_recipient_role
        def view_func():
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(Forbidden):
            view_func()

    def test_no_org_roles_rejected(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.ADMIN],
            organisation=None,
            grant=None,
        )

        @has_access_grant_recipient_role
        def view_func():
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        with pytest.raises(Forbidden):
            view_func()

    def test_user_gr_org_member_responds(self, factories):
        user = factories.user.create(email="test.admin@communities.gov.uk")
        grant = factories.grant.create()
        grant_recipient = factories.grant_recipient.create(grant=grant)
        factories.user_role.create(
            user=user,
            permissions=[RoleEnum.MEMBER],
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
        )

        @has_access_grant_recipient_role
        def view_func():
            return "OK"

        login_user(user)
        session["auth"] = AuthMethodEnum.MAGIC_LINK

        assert view_func() == "OK"


class TestHasFeatureFlagEnabled:
    def test_has_feature_flag_enabled(self, app, factories):
        class EnabledFlag(StaticFeatureFlag):
            description = "Test feature flag"
            resolver_description = "Always enabled"
            uses_request_context = False

            @classmethod
            def resolve(cls) -> bool:
                return True

        class DisabledFlag(StaticFeatureFlag):
            description = "Test feature flag"
            resolver_description = "Always disabled"
            uses_request_context = False

            @classmethod
            def resolve(cls) -> bool:
                return False

        @has_feature_flag_enabled(EnabledFlag())
        def test_view_enabled():
            return "OK"

        @has_feature_flag_enabled(DisabledFlag())
        def test_view_disabled():
            return "OK"

        response = test_view_enabled()
        assert response == "OK"

        with pytest.raises(NotFound):
            test_view_disabled()
