import datetime
from unittest.mock import patch
from uuid import uuid4

import pytest
from bs4 import BeautifulSoup
from flask import url_for
from sqlalchemy import func, select

from app.common.auth.authorisation_helper import AuthorisationHelper
from app.common.data import interfaces
from app.common.data.models_user import Invitation, MagicLink, User, UserRole
from app.common.data.types import CollectionStatusEnum, GrantStatusEnum, RoleEnum
from tests.models import _get_grant_managing_organisation
from tests.utils import AnyStringMatching, get_h1_text, page_has_error, page_has_h2


class TestMagicLinkSignInView:
    def test_get(self, anonymous_client):
        response = anonymous_client.get(url_for("auth.request_a_link_to_sign_in"))
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "Access grant funding" in get_h1_text(soup)
        assert "A service for grant recipients of central government funding" in soup.text

    def test_post_invalid_email(self, anonymous_client):
        response = anonymous_client.post(
            url_for("auth.request_a_link_to_sign_in"), data={"email_address": "invalid-email"}
        )
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert page_has_error(soup, "Enter an email address in the correct format")

    def test_post_mhclg_email_redirects_to_sso(self, app, anonymous_client):
        with patch("app.common.auth.build_auth_code_flow") as mock_build_auth_code_flow:
            mock_build_auth_code_flow.return_value = {"auth_uri": "http://auth.example.com/auth-uri"}
            response = anonymous_client.post(
                url_for("auth.request_a_link_to_sign_in"),
                data={"email_address": "test@communities.gov.uk"},
                follow_redirects=False,
            )
            assert response.status_code == 302
            assert response.location == "http://auth.example.com/auth-uri"

    def test_post_invalid_non_mhclg_email(self, anonymous_client, factories, mock_notification_service_calls):
        response = anonymous_client.post(
            url_for("auth.request_a_link_to_sign_in"), data={"email_address": "test@localgov.gov.uk"}
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert page_has_error(
            soup,
            (
                "The email address you entered does not have access to this service. "
                "Check the email address is correct or request access."
            ),
        )

    def test_post_valid_non_mhclg_email(self, anonymous_client, factories, mock_notification_service_calls):
        recipient_org = factories.organisation.create(can_manage_grants=False)
        mhclg = _get_grant_managing_organisation()
        grant = factories.grant.create(organisation=mhclg)
        factories.grant_recipient.create(grant=grant, organisation=recipient_org)
        user = factories.user.create(email="test@localgov.gov.uk")
        factories.user_role.create(
            user=user, organisation=recipient_org, grant=grant, permissions=[RoleEnum.DATA_PROVIDER]
        )

        response = anonymous_client.post(
            url_for("auth.request_a_link_to_sign_in"),
            data={"email_address": user.email},
            follow_redirects=True,
        )
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")

        assert "Check your email" in get_h1_text(soup)
        assert "test@localgov.gov.uk" in soup.text

        assert len(mock_notification_service_calls) == 1
        assert mock_notification_service_calls[0].kwargs["personalisation"]["magic_link"] == AnyStringMatching(
            r"http://funding.communities.gov.localhost:8080/sign-in/.*"
        )
        assert (
            mock_notification_service_calls[0].kwargs["personalisation"]["request_new_magic_link"]
            == "http://funding.communities.gov.localhost:8080/request-a-link-to-sign-in"
        )

    @pytest.mark.parametrize(
        "next_, safe_next",
        (
            ("/blah/blah", "/blah/blah"),
            ("https://bad.place/blah", "/"),  # Single test case; see TestSanitiseRedirectURL for more exhaustion
        ),
    )
    def test_post_valid_email_with_redirect(
        self, anonymous_client, mock_notification_service_calls, factories, db_session, next_, safe_next
    ):
        recipient_org = factories.organisation.create(can_manage_grants=False)
        mhclg = _get_grant_managing_organisation()
        grant = factories.grant.create(organisation=mhclg)
        factories.grant_recipient.create(grant=grant, organisation=recipient_org)
        user = factories.user.create(email="test@localgov.gov.uk")
        factories.user_role.create(
            user=user, organisation=recipient_org, grant=grant, permissions=[RoleEnum.DATA_PROVIDER]
        )

        with anonymous_client.session_transaction() as session:
            session["next"] = next_

        response = anonymous_client.post(
            url_for("auth.request_a_link_to_sign_in"),
            data={"email_address": user.email},
            follow_redirects=True,
        )
        assert response.status_code == 200
        assert (
            db_session.scalar(select(MagicLink).order_by(MagicLink.created_at_utc.desc())).redirect_to_path == safe_next
        )

        with anonymous_client.session_transaction() as session:
            assert "next" not in session

    def test_post_valid_email_with_no_next_redirects_to_route(
        self, anonymous_client, mock_notification_service_calls, factories, db_session
    ):
        recipient_org = factories.organisation.create(can_manage_grants=False)
        mhclg = _get_grant_managing_organisation()
        grant = factories.grant.create(organisation=mhclg)
        factories.grant_recipient.create(grant=grant, organisation=recipient_org)
        user = factories.user.create(email="test@localgov.gov.uk")
        factories.user_role.create(
            user=user, organisation=recipient_org, grant=grant, permissions=[RoleEnum.DATA_PROVIDER]
        )

        response = anonymous_client.post(
            url_for("auth.request_a_link_to_sign_in"),
            data={"email_address": user.email},
            follow_redirects=True,
        )
        assert response.status_code == 200
        assert (
            db_session.scalar(select(MagicLink).order_by(MagicLink.created_at_utc.desc())).redirect_to_path
            == "/access/"
        )

        with anonymous_client.session_transaction() as session:
            assert "next" not in session


class TestCollectionRequestALinkToPublicSignUpView:
    def test_get_404s_for_unknown_grant(self, anonymous_client, factories):
        collection = factories.collection.create(slug="collection-slug")

        response = anonymous_client.get(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug="not-a-real-grant",
                collection_slug=collection.slug,
            )
        )

        assert response.status_code == 404

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

        response = anonymous_client.get(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant.slug,
                collection_slug="not-a-real-collection",
            )
        )
        assert response.status_code == 404

    @pytest.mark.parametrize(
        "grant_status, collection_status, allow_public_sign_up, expected_status",
        (
            (GrantStatusEnum.LIVE, CollectionStatusEnum.OPEN, True, 200),
            (GrantStatusEnum.LIVE, CollectionStatusEnum.OPEN, False, 404),
            (GrantStatusEnum.DRAFT, CollectionStatusEnum.OPEN, True, 404),
            (GrantStatusEnum.ONBOARDING, CollectionStatusEnum.OPEN, True, 404),
            (GrantStatusEnum.LIVE, CollectionStatusEnum.DRAFT, True, 404),
            (GrantStatusEnum.LIVE, CollectionStatusEnum.CLOSED, True, 404),
        ),
    )
    def test_get_depends_on_status_and_allow_public_sign_up(
        self, anonymous_client, factories, grant_status, collection_status, allow_public_sign_up, expected_status
    ):
        grant = factories.grant.create(slug="grant-slug", status=grant_status)
        collection = factories.collection.create(
            slug="collection-slug",
            grant=grant,
            status=collection_status,
            allow_public_sign_up=allow_public_sign_up,
        )

        response = anonymous_client.get(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant.slug,
                collection_slug=collection.slug,
            )
        )

        assert response.status_code == expected_status

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

        response = anonymous_client.get(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant.slug,
                collection_slug=collection.slug,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "Enter your work email address" in get_h1_text(soup)
        assert "Test grant name" in soup.text

    def test_post_allows_unknown_email(self, anonymous_client, factories, mock_notification_service_calls, db_session):
        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,
        )

        response = anonymous_client.post(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant.slug,
                collection_slug=collection.slug,
            ),
            data={"email_address": "new-applicant@example.com"},
            follow_redirects=True,
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "Check your email" in get_h1_text(soup)
        assert "new-applicant@example.com" in soup.text

        assert len(mock_notification_service_calls) == 1
        assert mock_notification_service_calls[0].kwargs["personalisation"]["magic_link"] == AnyStringMatching(
            r"http://funding\.communities\.gov\.localhost:8080/sign-in/.*"
        )

        magic_link = db_session.scalar(select(MagicLink).where(MagicLink.email == "new-applicant@example.com"))
        assert magic_link.user is None
        assert magic_link.collection_id == collection.id
        assert magic_link.redirect_to_path == url_for(
            "access_grant_funding.eligible_to_apply", grant_slug=grant.slug, collection_slug=collection.slug
        )

        assert mock_notification_service_calls[0].kwargs["personalisation"]["request_new_magic_link"] == url_for(
            "auth.collection_request_a_link_to_public_sign_up",
            grant_slug=grant.slug,
            collection_slug=collection.slug,
            _external=True,
        )

        request_new_link = soup.find("a", string="request a new link")
        assert request_new_link["href"] == url_for(
            "auth.collection_request_a_link_to_public_sign_up", grant_slug=grant.slug, collection_slug=collection.slug
        )


class TestCheckEmailPage:
    def test_get(self, anonymous_client, factories):
        magic_link = factories.magic_link.create(email="test@communities.gov.uk")
        response = anonymous_client.get(url_for("auth.check_email", magic_link_id=magic_link.id))
        assert response.status_code == 200
        assert b"Check your email" in response.data
        assert b"test@communities.gov.uk" in response.data

    def test_get_magic_link_without_collection_link_redirect_to_usual_flow(self, anonymous_client, factories):
        magic_link = factories.magic_link.create(email="test@communities.gov.uk")

        response = anonymous_client.get(url_for("auth.check_email", magic_link_id=magic_link.id))
        assert response.status_code == 200

        soup = BeautifulSoup(response.data, "html.parser")
        request_new_link = soup.find("a", string="request a new link")
        assert request_new_link["href"] == url_for("auth.request_a_link_to_sign_in")

    def test_get_public_sign_off_magic_link_request_new_link_goes_to_public_flow(self, anonymous_client, factories):
        grant = factories.grant.create(slug="grant-slug")
        collection = factories.collection.create(slug="collection-slug", grant=grant)
        magic_link = factories.magic_link.create(email="new-applicant@example.com", collection=collection)

        response = anonymous_client.get(url_for("auth.check_email", magic_link_id=magic_link.id))
        assert response.status_code == 200

        soup = BeautifulSoup(response.data, "html.parser")
        request_new_link = soup.find("a", string="request a new link")
        assert request_new_link["href"] == url_for(
            "auth.collection_request_a_link_to_public_sign_up", grant_slug=grant.slug, collection_slug=collection.slug
        )


class TestClaimMagicLinkView:
    def test_get(self, anonymous_client, factories):
        magic_link = factories.magic_link.create()

        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code=magic_link.code))
        assert response.status_code == 200
        assert b"Sign in" in response.data

    def test_redirect_on_unknown_magic_link(self, anonymous_client):
        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code="unknown-code"))
        assert response.status_code == 302
        assert response.location == url_for("auth.request_a_link_to_sign_in", link_expired=True)

    def test_redirect_on_used_magic_link(self, anonymous_client, factories):
        # FIXME: Check that the session["next"] is the original redirect_to_path value
        magic_link = factories.magic_link.create(
            user__email="test@communities.gov.uk",
            redirect_to_path="/my-redirect",
            claimed_at_utc=datetime.datetime.now() - datetime.timedelta(hours=1),
        )
        response = anonymous_client.get(
            url_for("auth.claim_magic_link", magic_link_code=magic_link.code), follow_redirects=True
        )
        soup = BeautifulSoup(response.data, "html.parser")
        assert response.status_code == 200
        assert page_has_h2(soup, "Link expired")

    def test_redirect_on_expired_magic_link(self, anonymous_client, factories):
        magic_link = factories.magic_link.create(
            user__email="test@communities.gov.uk",
            redirect_to_path="/my-redirect",
            expires_at_utc=datetime.datetime.now() - datetime.timedelta(hours=1),
        )

        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code=magic_link.code))
        assert response.status_code == 302
        assert response.location == url_for("auth.request_a_link_to_sign_in", link_expired=True)

    def test_redirect_on_expired_public_sign_off_magic_link_preserves_grant_and_collection(
        self, anonymous_client, factories
    ):
        grant = factories.grant.create(slug="grant-slug")
        collection = factories.collection.create(slug="collection-slug", grant=grant)
        magic_link = factories.magic_link.create(
            user__email="test@example.com",
            redirect_to_path="/my-redirect",
            collection=collection,
            expires_at_utc=datetime.datetime.now() - datetime.timedelta(hours=1),
        )

        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code=magic_link.code))
        assert response.status_code == 302
        assert response.location == url_for(
            "auth.collection_request_a_link_to_public_sign_up",
            link_expired=True,
            grant_slug="grant-slug",
            collection_slug="collection-slug",
        )

    def test_get_without_session_flag_does_not_auto_submit(self, anonymous_client, factories):
        magic_link = factories.magic_link.create()

        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code=magic_link.code))

        assert response.status_code == 200
        assert b"Sign in" in response.data
        assert b'document.getElementById("submit").click()' not in response.data

    def test_get_with_session_flag_enables_auto_submit(self, anonymous_client, factories):
        magic_link = factories.magic_link.create()

        with anonymous_client.session_transaction() as session:
            session["magic_link_requested"] = True

        response = anonymous_client.get(url_for("auth.claim_magic_link", magic_link_code=magic_link.code))

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        form = soup.find("form", {"method": "post"})
        assert "app-js-hidden" in form.get("class")
        assert b'document.getElementById("submit").click()' in response.data

    @pytest.mark.parametrize(
        "redirect_to, safe_redirect_to",
        (
            ("/blah/blah", "/blah/blah"),
            ("https://bad.place/blah", "/"),  # Single test case; see TestSanitiseRedirectURL for more exhaustion
        ),
    )
    def test_post_claims_link_and_creates_user_and_redirects(
        self, anonymous_client, factories, db_session, redirect_to, safe_redirect_to, caplog
    ):
        user_email = "new_user@email.com"

        magic_link = interfaces.magic_link.create_magic_link(email=user_email, user=None, redirect_to_path=redirect_to)

        user_from_db = db_session.scalar(select(User).where(User.email == user_email))
        assert user_from_db is None

        user = interfaces.user.get_current_user()
        assert user.is_authenticated is False

        response = anonymous_client.post(
            url_for("auth.claim_magic_link", magic_link_code=magic_link.code),
            json={"submit": "yes"},
            follow_redirects=False,
        )

        user_from_db = db_session.scalar(select(User).where(User.email == user_email))

        assert response.status_code == 302
        assert response.location == safe_redirect_to
        assert magic_link.claimed_at_utc is not None
        assert magic_link.is_usable is False
        assert user.is_authenticated is True
        assert magic_link.user.id == user.id
        assert user_from_db is not None

        assert "Magic link claim page submitted: auto_submit=False" in caplog.messages

    def test_post_with_session_flag_logs_auto_submit_true(self, anonymous_client, factories, caplog):
        magic_link = factories.magic_link.create()

        with anonymous_client.session_transaction() as session:
            session["magic_link_requested"] = True

        response = anonymous_client.post(
            url_for("auth.claim_magic_link", magic_link_code=magic_link.code),
            json={"submit": "yes"},
            follow_redirects=False,
        )

        assert response.status_code == 302
        assert "Magic link claim page submitted: auto_submit=True" in caplog.messages

    def test_post_with_collection_sets_signing_up_session_flag(self, anonymous_client, factories):
        grant = factories.grant.create(slug="grant-slug")
        collection = factories.collection.create(slug="collection-slug", grant=grant)
        magic_link = factories.magic_link.create(email="new-applicant@example.com", collection=collection)

        response = anonymous_client.post(
            url_for("auth.claim_magic_link", magic_link_code=magic_link.code),
            json={"submit": "yes"},
            follow_redirects=False,
        )

        assert response.status_code == 302
        with anonymous_client.session_transaction() as session:
            assert session["signing_up_for_collection_id"] == collection.id

    def test_post_without_collection_clears_signing_up_session_flag(self, anonymous_client, factories):
        magic_link = factories.magic_link.create(email="test@communities.gov.uk")

        with anonymous_client.session_transaction() as session:
            session["signing_up_for_collection_id"] = uuid4()

        response = anonymous_client.post(
            url_for("auth.claim_magic_link", magic_link_code=magic_link.code),
            json={"submit": "yes"},
            follow_redirects=False,
        )

        assert response.status_code == 302
        with anonymous_client.session_transaction() as session:
            assert "signing_up_for_collection_id" not in session


class TestSignOutView:
    @pytest.mark.parametrize(
        "client_fixture, sign_out_redirect",
        [
            ("authenticated_grant_member_client", "auth.sso_sign_in"),
            ("authenticated_grant_recipient_member_client", "auth.request_a_link_to_sign_in"),
        ],
    )
    def test_get(self, anonymous_client, client_fixture, sign_out_redirect, request):
        client = request.getfixturevalue(client_fixture)

        response = client.get(url_for("auth.sign_out"), follow_redirects=True)
        assert response.status_code == 200
        assert response.request.path == url_for(sign_out_redirect)

        with client.session_transaction() as session:
            assert "_user_id" not in session
            assert "auth" not in session

    def test_get_with_signing_up_for_collection_redirects_to_start_page_and_clears_flag(
        self, authenticated_grant_recipient_member_client, factories
    ):
        client = authenticated_grant_recipient_member_client
        grant = factories.grant.create(slug="grant-slug")
        collection = factories.collection.create(slug="collection-slug", grant=grant)

        with client.session_transaction() as session:
            session["signing_up_for_collection_id"] = collection.id

        response = client.get(url_for("auth.sign_out"), follow_redirects=False)

        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
        )

        with client.session_transaction() as session:
            assert "_user_id" not in session
            assert "auth" not in session
            assert "signing_up_for_collection_id" not in session


class TestSSOSignInView:
    def test_get(self, anonymous_client):
        response = anonymous_client.get(url_for("auth.sso_sign_in"))
        assert response.status_code == 200
        assert b"A connected and consistent digital service" in response.data


class TestSSOGetTokenView:
    def test_get_without_fs_platform_admin_role_and_with_no_assigned_roles(self, app, anonymous_client):
        with patch("app.common.auth.build_msal_app") as mock_build_msap_app:
            # Partially mock the expected return value; just enough for the test.
            mock_build_msap_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@test.communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": "someStringValue",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"))

        assert response.status_code == 302
        assert response.location == url_for("auth.signed_in_but_no_permissions", invite_expired=False)

    def test_login_with_grant_member_role(self, anonymous_client, factories):
        with patch("app.common.auth.build_msal_app") as mock_build_msap_app:
            user = factories.user.create(email="test.member@communities.gov.uk")
            grant = factories.grant.create()
            factories.user_role.create(user=user, grant=grant, permissions=[RoleEnum.MEMBER])
            # Partially mock the expected return value; just enough for the test.
            mock_build_msap_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "Test.Member@communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": user.azure_ad_subject_id,
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)
            current_user = interfaces.user.get_current_user()
            assert not AuthorisationHelper.is_platform_admin(current_user)
            assert current_user.name == "SSO User"
            assert current_user.email == "Test.Member@communities.gov.uk"
            assert response.status_code == 200

    def test_get_without_any_roles_should_redirect(self, app, anonymous_client):
        with patch("app.common.auth.build_msal_app") as mock_build_msap_app:
            # Partially mock the expected return value; just enough for the test.
            mock_build_msap_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@test.communities.gov.uk",
                    "name": "SSO User",
                    "sub": "someStringValue",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"))

        assert response.status_code == 302
        assert response.location == url_for("auth.signed_in_but_no_permissions", invite_expired=False)

    def test_get_valid_token_with_redirect(self, anonymous_client, factories, db_session):
        dummy_grant = factories.grant.create()
        factories.user.create(email="test@test.communities.gov.uk", azure_ad_subject_id="subject_id")
        with anonymous_client.session_transaction() as session:
            session["next"] = url_for("deliver_grant_funding.grant_homepage", grant_id=dummy_grant.id)

        with patch("app.common.auth.build_msal_app") as mock_build_msap_app:
            # Partially mock the expected return value; just enough for the test.
            mock_build_msap_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@test.communities.gov.uk",
                    "name": "SSO User",
                    "roles": ["FS_PLATFORM_ADMIN"],
                    "sub": "subject_id",
                }
            }
            response = anonymous_client.get(
                url_for("auth.sso_get_token"),
                follow_redirects=True,
            )
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert dummy_grant.name in get_h1_text(soup)

        with anonymous_client.session_transaction() as session:
            assert "next" not in session

        new_user = db_session.scalar(select(User).where(User.email == "test@test.communities.gov.uk"))
        assert new_user.name == "SSO User"

    def test_platform_admin_first_login(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test.member@communities.gov.uk",
                    "name": "SSO User",
                    "roles": ["FS_PLATFORM_ADMIN"],
                    "sub": "abc123",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)
        user = interfaces.user.get_current_user()
        assert response.status_code == 200
        assert AuthorisationHelper.is_platform_admin(user)

    def test_platform_admin_with_fs_platform_admin_role_removed(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            user = factories.user.create(email="test.member@communities.gov.uk", azure_ad_subject_id="abc123")
            factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN])

            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test.member@communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": "abc123",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=False)
            updated_user = db_session.scalar(select(User).where(User.azure_ad_subject_id == "abc123"))

            assert AuthorisationHelper.is_platform_admin(updated_user) is False

        assert response.status_code == 302
        assert response.location == url_for("auth.signed_in_but_no_permissions", invite_expired=False)

    def test_platform_admin_with_grant_member_role_fs_platform_admin_role_removed(
        self, anonymous_client, factories, db_session
    ):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            user = factories.user.create(email="test.member@communities.gov.uk", azure_ad_subject_id="wer234")
            grant = factories.grant.create()
            factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN])
            factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)
            assert db_session.scalar(select(func.count()).select_from(UserRole)) == 2

            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test.member@communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": "wer234",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)
            updated_user = db_session.scalar(select(User).where(User.azure_ad_subject_id == "wer234"))

            assert db_session.scalar(select(func.count()).select_from(UserRole)) == 1
            assert AuthorisationHelper.is_deliver_grant_member(grant_id=grant.id, user=updated_user) is True
            assert AuthorisationHelper.is_platform_admin(updated_user) is False

        assert response.status_code == 200

    def test_platform_admin_does_not_remove_all_other_roles(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            user = factories.user.create(email="test.member@communities.gov.uk", azure_ad_subject_id="wer234")
            factories.user_role.create(user=user, permissions=[RoleEnum.ADMIN])
            grants = factories.grant.create_batch(2)
            for grant in grants:
                factories.user_role.create(user=user, permissions=[RoleEnum.MEMBER], grant=grant)
            assert db_session.scalar(select(func.count()).select_from(UserRole)) == 3

            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test.member@communities.gov.uk",
                    "name": "SSO User",
                    "roles": ["FS_PLATFORM_ADMIN"],
                    "sub": "wer234",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)
            updated_user = db_session.scalar(select(User).where(User.azure_ad_subject_id == "wer234"))
            assert AuthorisationHelper.is_platform_admin(updated_user) is True
            assert db_session.scalar(select(func.count()).select_from(UserRole)) == 3

        assert response.status_code == 200

    def test_platform_admin_signin_claims_pending_invitations(self, anonymous_client, factories, db_session):
        grants = factories.grant.create_batch(3)
        for grant in grants:
            factories.invitation.create(
                email="test@communities.gov.uk",
                organisation=grant.organisation,
                grant=grant,
                permissions=[RoleEnum.MEMBER],
            )
        assert db_session.scalar(select(func.count()).select_from(Invitation)) == 3

        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@communities.gov.uk",
                    "name": "SSO User",
                    "roles": ["FS_PLATFORM_ADMIN"],
                    "sub": "wer234",
                }
            }

            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)

        assert response.status_code == 200
        user = db_session.scalar(select(User).where(User.azure_ad_subject_id == "wer234"))
        assert AuthorisationHelper.is_platform_admin(user) is True
        assert db_session.scalar(select(func.count()).select_from(UserRole)) == 1
        usable_invites_from_db = db_session.scalars(select(Invitation).where(Invitation.is_usable.is_(True))).all()
        assert not usable_invites_from_db

    def test_grant_member_with_valid_invites_first_login(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            user = interfaces.user.get_current_user()
            assert user.is_anonymous

            grants = factories.grant.create_batch(3)
            invitations = []

            for grant in grants:
                invitation = factories.invitation.create(
                    email="test@communities.gov.uk",
                    organisation=grant.organisation,
                    grant=grant,
                    permissions=[RoleEnum.MEMBER],
                )
                invitations.append(invitation)

            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": "abc123",
                }
            }
            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=True)
        assert response.status_code == 200

        assert len(user.roles) == 3

        usable_invites_from_db = db_session.scalars(select(Invitation).where(Invitation.is_usable.is_(True))).all()
        assert not usable_invites_from_db

    @pytest.mark.freeze_time("2025-10-01 12:00:00")
    def test_invalid_grant_team_member_invitations_redirects_to_permissions_error(
        self, anonymous_client, factories, db_session
    ):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            grants = factories.grant.create_batch(2)
            # Create an expired invitation
            factories.invitation.create(
                email="test@communities.gov.uk",
                organisation=grants[-1].organisation,
                grant=grants[-1],
                permissions=[RoleEnum.MEMBER],
                expires_at_utc=datetime.datetime(2025, 9, 1, 12, 0, 0),
            )

            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "id_token_claims": {
                    "preferred_username": "test@communities.gov.uk",
                    "name": "SSO User",
                    "roles": [],
                    "sub": "abc123",
                }
            }
            usable_invites_from_db = db_session.scalars(select(Invitation).where(Invitation.is_usable.is_(True))).all()
            assert not usable_invites_from_db
            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=False)

        assert response.status_code == 302
        assert response.location == url_for("auth.signed_in_but_no_permissions", invite_expired=True)

    def test_response_when_build_msal_app_returns_token_used_error(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "error": "invalid_grant",
                "error_codes": [54005],
            }
            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=False)
            assert response.status_code == 302
            assert response.location == url_for("auth.sign_out")

    def test_response_when_build_msal_app_returns_other_error(self, anonymous_client, factories, db_session):
        with patch("app.common.auth.build_msal_app") as mock_build_msal_app:
            mock_build_msal_app.return_value.acquire_token_by_auth_code_flow.return_value = {
                "error": "bad_error",
                "error_codes": [12345],
            }
            response = anonymous_client.get(url_for("auth.sso_get_token"), follow_redirects=False)
            assert response.status_code == 500
            assert "Sorry, there is a problem with the service - MHCLG Funding Service" in response.data.decode()


class TestAuthenticatedUserRedirect:
    def test_magic_link_get(self, authenticated_no_role_client):
        response = authenticated_no_role_client.get(url_for("auth.request_a_link_to_sign_in"))
        assert response.status_code == 302

    def test_public_magic_link_get(self, authenticated_no_role_client, 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
        )

        response = authenticated_no_role_client.get(
            url_for(
                "auth.collection_request_a_link_to_public_sign_up",
                grant_slug=grant.slug,
                collection_slug=collection.slug,
            )
        )
        assert response.status_code == 302

    def test_sso_get(self, authenticated_no_role_client):
        response = authenticated_no_role_client.get(url_for("auth.sso_sign_in"))
        assert response.status_code == 302
