import datetime
import logging
from copy import deepcopy
from datetime import date
from unittest.mock import patch

import pytest
from bs4 import BeautifulSoup
from flask import url_for
from pytest import FixtureRequest

from app import CollectionStatusEnum, GrantStatusEnum, TasklistSectionStatusEnum
from app.access_grant_funding.forms import DeclineSignOffForm
from app.common.collections.types import IntegerAnswer, TextSingleLineAnswer
from app.common.data.interfaces.collections import update_collection
from app.common.data.types import (
    CollectionType,
    DataSourceType,
    ExpressionType,
    ManagedExpressionsEnum,
    QuestionDataType,
    RoleEnum,
    SubmissionEventType,
    SubmissionModeEnum,
    SubmissionStatusEnum,
)
from app.common.expressions import ExpressionReference
from app.common.expressions.references import InterpolationStatement
from app.common.forms import GenericSubmitForm
from app.common.helpers.collections import SubmissionHelper
from app.metrics import MetricEventName
from tests.models import FactoryAnswer
from tests.utils import AnyStringMatching, get_h1_text, page_has_button, page_has_error, page_has_link


class TestViewLockedReport:
    @pytest.mark.parametrize(
        "client_fixture, can_access",
        (
            ("authenticated_no_role_client", False),
            ("authenticated_grant_recipient_member_client", True),
            ("authenticated_grant_recipient_data_provider_client", True),
            ("authenticated_grant_recipient_certifier_client", True),
        ),
    )
    def test_get_view_locked_report_access(
        self,
        request: FixtureRequest,
        client_fixture: str,
        can_access: bool,
        factories,
        submission_awaiting_sign_off,
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()

        response = client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            )
        )

        if not can_access:
            assert response.status_code == 403
        else:
            assert response.status_code == 200
            soup = BeautifulSoup(response.data, "html.parser")
            assert get_h1_text(soup) == f"Review report: {submission_awaiting_sign_off.collection.name}"

    @pytest.mark.parametrize(
        "client_fixture, can_certify",
        (
            ("authenticated_grant_recipient_member_client", False),
            ("authenticated_grant_recipient_data_provider_client", False),
            ("authenticated_grant_recipient_certifier_client", True),
        ),
    )
    def test_get_view_locked_reports_certifier(
        self,
        request: FixtureRequest,
        client_fixture: str,
        can_certify: bool,
        factories,
        submission_awaiting_sign_off,
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()

        response = client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            )
        )

        soup = BeautifulSoup(response.data, "html.parser")

        assert [key.text for key in soup.find_all("dt", class_="app-metadata__key")] == [
            "Submission deadline:",
            "Submitted by:",
            "Date submitted for sign off:",
            "Status:",
        ]

        if not can_certify:
            assert page_has_button(soup, button_text="Continue to sign off and submit") is None
            assert page_has_link(soup, link_text="Decline sign off") is None
        else:
            assert page_has_button(soup, button_text="Continue to sign off and submit") is not None
            assert page_has_link(soup, link_text="Decline sign off") is not None

    def test_get_view_locked_report_submitted(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_submitted,
    ):
        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient
        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_submitted.collection.type,
                submission_id=submission_submitted.id,
            )
        )

        soup = BeautifulSoup(response.data, "html.parser")

        assert get_h1_text(soup) == submission_submitted.collection.name

        # now that its submitted we don't have certifier actions even though we have permissions
        # to do that
        assert page_has_button(soup, button_text="Sign off and submit report") is None
        assert page_has_link(soup, link_text="Decline sign off") is None

        assert [key.text for key in soup.find_all("dt", class_="app-metadata__key")] == [
            "Organisation:",
            "Submitted by:",
            "Certifier:",
            "Date submitted:",
        ]

    def test_get_view_locked_report_collection_closed(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_collection_closed,
    ):
        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient
        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_collection_closed.collection.type,
                submission_id=submission_collection_closed.id,
            )
        )

        soup = BeautifulSoup(response.data, "html.parser")

        assert get_h1_text(soup) == submission_collection_closed.collection.name

        assert page_has_button(soup, button_text="Sign off and submit report") is None
        assert page_has_link(soup, link_text="Decline sign off") is None

        assert [key.text for key in soup.find_all("dt", class_="app-metadata__key")] == [
            "Submission deadline:",
            "Status:",
        ]
        assert "You cannot edit or submit this report." in soup.text

    def test_view_locked_report_not_locked_redirects(
        self,
        authenticated_grant_recipient_member_client,
        factories,
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        submission = factories.submission.create(
            grant_recipient=grant_recipient, collection__grant=grant_recipient.grant, mode=SubmissionModeEnum.LIVE
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            )
        )

        assert response.status_code == 302
        assert response.location == (
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
            )
        )

    def test_post_view_locked_report_certify_redirect(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_awaiting_sign_off,
    ):
        organisation = authenticated_grant_recipient_certifier_client.organisation
        grant = authenticated_grant_recipient_certifier_client.grant

        assert submission_awaiting_sign_off.status == SubmissionStatusEnum.AWAITING_SIGN_OFF
        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        form = GenericSubmitForm()

        response = authenticated_grant_recipient_certifier_client.post(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=organisation.id,
                grant_id=grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=form.data,
            follow_redirects=False,
        )

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.confirm_submission_with_certify",
            organisation_id=organisation.id,
            grant_id=grant.id,
            collection_type=submission_awaiting_sign_off.collection.type,
            submission_id=submission_awaiting_sign_off.id,
        )
        assert submission_awaiting_sign_off.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

    def test_post_view_locked_report_403_with_incorrect_permissions(
        self, authenticated_grant_recipient_data_provider_client, submission_awaiting_sign_off
    ):
        organisation = authenticated_grant_recipient_data_provider_client.organisation
        grant = authenticated_grant_recipient_data_provider_client.grant

        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        form = GenericSubmitForm()

        response = authenticated_grant_recipient_data_provider_client.post(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=organisation.id,
                grant_id=grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=form.data,
            follow_redirects=True,
        )

        assert response.status_code == 403
        soup = BeautifulSoup(response.data, "html.parser")
        assert get_h1_text(soup) == "You do not have permission to access this page"

    @pytest.mark.parametrize(
        "allow_multiple_submissions, expected_back_link_route",
        [
            (False, "access_grant_funding.list_collections"),
            (True, "access_grant_funding.list_collection_submissions"),
        ],
    )
    def test_back_link_depends_on_allow_multiple_submissions(
        self,
        authenticated_grant_recipient_member_client,
        factories,
        db_session,
        allow_multiple_submissions,
        expected_back_link_route,
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=allow_multiple_submissions,
            form__collection__submission_period_end_date=date.today(),
        )
        submission = factories.submission.create(
            collection=question.form.collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            events=[],
            answers=[FactoryAnswer(question, TextSingleLineAnswer("Answer"))],
        )
        factories.submission_event.create(
            submission=submission,
            event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
            related_entity_id=question.form.id,
        )
        factories.submission_event.create(
            submission=submission, event_type=SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION
        )
        submission.status = SubmissionStatusEnum.AWAITING_SIGN_OFF

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        back_link = soup.select_one(".govuk-back-link")
        expected_url_kwargs = {
            "organisation_id": grant_recipient.organisation.id,
            "grant_id": grant_recipient.grant.id,
        }
        if allow_multiple_submissions:
            expected_url_kwargs["collection_id"] = question.form.collection.id
        assert back_link["href"] == url_for(expected_back_link_route, **expected_url_kwargs)

    @pytest.mark.parametrize(
        "client_fixture",
        [
            "authenticated_grant_recipient_data_provider_client",
            "authenticated_grant_recipient_certifier_client",
        ],
    )
    def test_shows_diff_for_awaiting_sign_off_with_changes(
        self,
        request: FixtureRequest,
        client_fixture: str,
        submission_awaiting_sign_off_with_changes_made,
    ):
        client = request.getfixturevalue(client_fixture)
        submission = submission_awaiting_sign_off_with_changes_made
        grant_recipient = client.grant_recipient

        assert submission.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        response = client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        tag_texts = {tag.text.strip() for tag in soup.select(".govuk-tag")}
        assert "Changed" in tag_texts


class TestCollectionUnavailable:
    def _make_collection_with_missing_referenced_data(
        self, factories, db_session, grant_recipient, **collection_kwargs
    ):
        collection = factories.collection.create(grant=grant_recipient.grant, **collection_kwargs)
        data_source = factories.data_source.create(
            grant=grant_recipient.grant,
            collection=collection,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[None],
        )
        factories.question.create(
            form__collection=collection,
            hint=InterpolationStatement(
                "You were allocated " + ExpressionReference.from_data_source_column(data_source, "c_allocation").wrapped
            ),
        )
        return collection

    @pytest.mark.parametrize(
        "client_fixture, can_access",
        (
            ("authenticated_no_role_client", False),
            ("authenticated_grant_recipient_member_client", True),
        ),
    )
    def test_get_collection_unavailable_access(
        self, request: FixtureRequest, client_fixture: str, can_access: bool, factories, db_session
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()
        collection = self._make_collection_with_missing_referenced_data(factories, db_session, grant_recipient)

        response = client.get(
            url_for(
                "access_grant_funding.collection_unavailable",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        if not can_access:
            assert response.status_code == 403
        else:
            assert response.status_code == 200
            soup = BeautifulSoup(response.data, "html.parser")
            assert get_h1_text(soup) == f"Sorry, the {collection.type.constants.singular} is unavailable"
            assert (
                f"You cannot access this {collection.type.constants.singular} right now. You will be "
                + "able to access it later."
                in soup.text
            )

    @patch("app.access_grant_funding.routes.collections.emit_metric_count")
    def test_records_metric_and_error_log_when_shown(
        self, mock_count, authenticated_grant_recipient_member_client, factories, db_session, caplog
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        collection = self._make_collection_with_missing_referenced_data(factories, db_session, grant_recipient)

        with caplog.at_level(logging.ERROR):
            response = authenticated_grant_recipient_member_client.get(
                url_for(
                    "access_grant_funding.collection_unavailable",
                    organisation_id=grant_recipient.organisation.id,
                    grant_id=grant_recipient.grant.id,
                    collection_id=collection.id,
                )
            )

        assert response.status_code == 200
        mock_count.assert_called_once_with(
            MetricEventName.COLLECTION_BLOCKED_BY_MISSING_DATA, grant_recipient=grant_recipient, collection=collection
        )
        assert any(
            r.getMessage()
            == AnyStringMatching(
                rf"^User [a-z0-9-]{{36}} from grant recipient {grant_recipient.id} was shown the "
                rf"collection-unavailable page for collection {collection.id} because a referenced grant recipient "
                r"data source is missing data$"
            )
            for r in caplog.records
            if r.levelno == logging.ERROR
        )

    def test_redirects_to_route_to_submission_when_data_no_longer_missing(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        collection = factories.collection.create(grant=grant_recipient.grant)

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.collection_unavailable",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 302
        assert response.location == AnyStringMatching(
            "^/access/organisation/[a-z0-9-]{36}/grants/[a-z0-9-]{36}/collection/[a-z0-9-]{36}$"
        )

    def test_redirects_to_route_to_submission_when_data_no_longer_missing_and_multiple_submissions(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        collection = factories.collection.create(grant=grant_recipient.grant, allow_multiple_submissions=True)

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.collection_unavailable",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 302
        assert response.location == AnyStringMatching(
            "^/access/organisation/[a-z0-9-]{36}/grants/[a-z0-9-]{36}/collection/[a-z0-9-]{36}$"
        )


class TextExportReportPDF:
    # the first method under test will spin up chromium which will always be marked as as a slow test
    @pytest.mark.fail_slow("1000ms", enabled=False)
    @pytest.mark.parametrize(
        "client_fixture, can_access",
        (
            ("authenticated_no_role_client", False),
            ("authenticated_grant_recipient_member_client", True),
            ("authenticated_grant_recipient_data_provider_client", True),
            ("authenticated_grant_recipient_certifier_client", True),
        ),
    )
    def test_get_export_report_pdf(
        self,
        request: FixtureRequest,
        client_fixture: str,
        can_access: bool,
        factories,
        submission_awaiting_sign_off,
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()

        response = client.get(
            url_for(
                "access_grant_funding.export_submission_pdf",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            )
        )

        if not can_access:
            assert response.status_code == 403
        else:
            assert response.status_code == 200
            assert response.headers["Content-Type"] == "application/pdf"

    def test_shows_changed_tag_and_original_response_for_resubmitted_answers(
        self, authenticated_grant_recipient_data_provider_client, factories, db_session
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        question = factories.question.create(form__collection__grant=grant_recipient.grant)
        submission = factories.submission.create(
            grant_recipient=grant_recipient,
            collection=question.form.collection,
            answers=[FactoryAnswer(question, TextSingleLineAnswer("original answer"))],
        )
        previous_data = deepcopy(submission.data_manager.data)

        factories.submission_event.create(
            submission=submission,
            event_type=SubmissionEventType.SUBMISSION_CHANGES_REQUESTED,
            data={
                "changes_requested_reason": "Please fix",
                "submission_data": previous_data,
                "section_ids": [],
            },
        )
        submission.data_manager.set(question, TextSingleLineAnswer("updated answer"))
        submission.status = SubmissionStatusEnum.SUBMITTED
        db_session.flush()

        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.view_locked_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=question.form.collection.type,
                submission_id=submission.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")

        tag_texts = {tag.text.strip() for tag in soup.select(".govuk-tag")}
        assert "Changed" in tag_texts

        assert "Original response" in soup.text
        assert "original answer" in soup.text
        assert "updated answer" in soup.text


class TestExportReportPDFLock:
    def test_lock_is_held_around_sync_playwright(
        self,
        authenticated_grant_recipient_member_client,
        submission_awaiting_sign_off,
        monkeypatch,
    ):
        from app.common.helpers import pdf as pdf_module

        observed_lock_states: list[bool] = []

        class _FakePage:
            def set_content(self, html_content, wait_until):
                pass

            def pdf(self, **kwargs):
                return b"%PDF-fake-content"

        class _FakeBrowser:
            def new_page(self, **kwargs):
                return _FakePage()

        class _FakeChromium:
            def launch(self):
                return _FakeBrowser()

        class _FakePlaywright:
            chromium = _FakeChromium()

        class _FakeSyncPlaywrightCM:
            def __enter__(self_inner):
                observed_lock_states.append(pdf_module._pdf_export_lock.locked())
                return _FakePlaywright()

            def __exit__(self_inner, *args):
                pass

        monkeypatch.setattr(pdf_module, "sync_playwright", lambda: _FakeSyncPlaywrightCM())

        client = authenticated_grant_recipient_member_client
        grant_recipient = client.grant_recipient

        response = client.get(
            url_for(
                "access_grant_funding.export_submission_pdf",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            )
        )

        assert response.status_code == 200
        assert observed_lock_states == [True]
        assert not pdf_module._pdf_export_lock.locked()


class TestListReports:
    def test_get_list_reports(self, authenticated_grant_recipient_member_client, factories):
        organisation = authenticated_grant_recipient_member_client.organisation or factories.organisation.create(
            can_manage_grants=False,
        )
        grant = authenticated_grant_recipient_member_client.grant
        grant.status = GrantStatusEnum.LIVE

        _ = factories.collection.create_batch(
            2,
            grant=grant,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )
        response = authenticated_grant_recipient_member_client.get(
            url_for("access_grant_funding.list_collections", organisation_id=organisation.id, grant_id=grant.id)
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert get_h1_text(soup) == "Reports"
        table_elem = soup.find("table", class_="govuk-table")
        assert table_elem is not None
        assert len(table_elem.find_all("tr")) == 3

    def test_get_list_forms_pre_award_enabled(self, authenticated_grant_recipient_member_client, factories):
        organisation = authenticated_grant_recipient_member_client.organisation or factories.organisation.create(
            can_manage_grants=False,
        )
        grant = authenticated_grant_recipient_member_client.grant
        grant.status = GrantStatusEnum.LIVE
        grant.allow_pre_award = True

        _ = factories.collection.create_batch(
            3,
            grant=grant,
            type=CollectionType.APPLICATION,
            status=CollectionStatusEnum.OPEN,
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )
        _ = factories.collection.create_batch(
            2,
            grant=grant,
            type=CollectionType.MONITORING_REPORT,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )
        response = authenticated_grant_recipient_member_client.get(
            url_for("access_grant_funding.list_collections", organisation_id=organisation.id, grant_id=grant.id)
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert get_h1_text(soup) == "Forms"
        [pre_award_table, monitoring_table] = soup.find_all("table", class_="govuk-table")
        assert pre_award_table is not None
        assert len(pre_award_table.find_all("tr")) == 4
        assert monitoring_table is not None
        assert len(monitoring_table.find_all("tr")) == 3

    def test_get_list_reports_not_grant_recipient(self, authenticated_grant_recipient_member_client, factories):
        organisation = authenticated_grant_recipient_member_client.organisation
        grant = factories.grant.create(organisation=organisation, status=GrantStatusEnum.LIVE)
        factories.grant_recipient.create(grant=grant, organisation=organisation)

        _ = factories.collection.create_batch(
            2,
            grant=grant,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )
        response = authenticated_grant_recipient_member_client.get(
            url_for("access_grant_funding.list_collections", organisation_id=organisation.id, grant_id=grant.id)
        )

        assert response.status_code == 403

    def test_get_list_collections_with_overdue_status(
        self, authenticated_grant_recipient_member_client, grant_recipient, factories
    ):
        collection = factories.collection.create(
            status=CollectionStatusEnum.OPEN,
            grant=authenticated_grant_recipient_member_client.grant,
            submission_period_end_date=date(2020, 1, 1),  # past date
            name="Test Report",
        )

        factories.submission.create(
            collection=collection,
            mode=SubmissionModeEnum.LIVE,
            grant_recipient=grant_recipient,
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=authenticated_grant_recipient_member_client.grant.id,
            )
        )

        assert response.status_code == 200

        soup = BeautifulSoup(response.data, "html.parser")
        tag_texts = {tag.text.strip() for tag in soup.select(".govuk-tag")}

        assert "Ready to submit (Overdue)" in tag_texts

    def test_assessed_submission_shows_submission_status_tag_not_approved_assessment_tag(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_with_allow_validation,
        grant_team_user,
        db_session,
    ):
        # Validate submission
        SubmissionHelper(submission_with_allow_validation).validate_submission(user=grant_team_user, is_approved=True)
        db_session.commit()

        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient

        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
            )
        )

        assert response.status_code == 200

        soup = BeautifulSoup(response.data, "html.parser")
        tag_texts = {tag.text.strip() for tag in soup.select(".govuk-tag")}
        # "Marked as approved" not on the page
        assert len(tag_texts) == 0
        # Make sure we select the span from the table cell
        span_texts = {span.text.strip() for span in soup.select("table.govuk-table>tbody>tr>td>span.govuk-body")}
        # Submittes shows as text on the page
        assert len(span_texts) == 1
        assert "Submitted" in span_texts

    def test_assessed_submission_shows_submission_status_tag_not_rejected_assessment_tag(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_with_allow_validation,
        grant_team_user,
        db_session,
    ):
        # Validate submission
        SubmissionHelper(submission_with_allow_validation).validate_submission(
            user=grant_team_user, is_approved=False, rejected_reason="The reason"
        )
        db_session.commit()

        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient
        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
            )
        )

        assert response.status_code == 200

        soup = BeautifulSoup(response.data, "html.parser")
        tag_texts = {tag.text.strip() for tag in soup.select(".govuk-tag")}
        # "Marked as rejected" not on the page
        assert len(tag_texts) == 0
        # Make sure we select the span from the table cell
        span_texts = {span.text.strip() for span in soup.select("table.govuk-table>tbody>tr>td>span.govuk-body")}
        # Submittes shows as text on the page
        assert len(span_texts) == 1
        assert "Submitted" in span_texts


class TestListCollectionSubmissions:
    def test_lists_submissions_for_collection(self, authenticated_grant_recipient_member_client, factories):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        question = factories.question.create(
            text="Project name",
            name="project name",
            data_type=QuestionDataType.TEXT_SINGLE_LINE,
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__status=CollectionStatusEnum.OPEN,
        )
        collection = question.form.collection
        collection.submission_name_question_id = question.id
        factories.submission.create(
            collection=collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            answers=[FactoryAnswer(question, TextSingleLineAnswer("Alpha"))],
        )
        factories.submission.create(
            collection=collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            answers=[FactoryAnswer(question, TextSingleLineAnswer("Beta"))],
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert get_h1_text(soup) == collection.name
        assert "Alpha" in soup.text
        assert "Beta" in soup.text

    def test_shows_empty_state_when_no_submissions(self, authenticated_grant_recipient_member_client, factories):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        collection = factories.collection.create(
            grant=grant_recipient.grant,
            allow_multiple_submissions=True,
            status=CollectionStatusEnum.OPEN,
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "You have not started any reports yet." in soup.text

    def test_shows_submission_name_from_submission_name_question(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__status=CollectionStatusEnum.OPEN,
            data_type=QuestionDataType.TEXT_SINGLE_LINE,
        )
        collection = question.form.collection
        collection.submission_name_question_id = question.id
        factories.submission.create(
            collection=collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            answers=[FactoryAnswer(question, TextSingleLineAnswer("My custom report name"))],
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "My custom report name" in soup.text

    def test_report_list_shows_go_to_reports_link_for_multi_submission_collection(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        grant = grant_recipient.grant
        grant.status = GrantStatusEnum.LIVE
        factories.collection.create(
            grant=grant,
            allow_multiple_submissions=True,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert page_has_link(soup, "Go to reports") is not None

    def test_report_list_shows_not_started_for_multi_submission_with_no_submissions(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        grant = grant_recipient.grant
        grant.status = GrantStatusEnum.LIVE
        factories.collection.create(
            grant=grant,
            allow_multiple_submissions=True,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        tag = soup.find("strong", class_="govuk-tag", string=lambda t: t and "Not started" in t)
        assert tag is not None

    def test_report_list_shows_in_progress_for_multi_submission_with_incomplete_submissions(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        grant = grant_recipient.grant
        grant.status = GrantStatusEnum.LIVE
        collection = factories.collection.create(
            grant=grant,
            allow_multiple_submissions=True,
            status=CollectionStatusEnum.OPEN,
            reporting_period_start_date=date(2025, 1, 1),
            reporting_period_end_date=date(2025, 3, 31),
            submission_period_start_date=date(2025, 11, 1),
            submission_period_end_date=date(2026, 2, 28),
        )
        factories.submission.create(
            collection=collection, grant_recipient=grant_recipient, mode=SubmissionModeEnum.LIVE
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        tag = soup.find("strong", class_="govuk-tag", string=lambda t: t and "In progress" in t)
        assert tag is not None

    def test_report_list_shows_submitted_count_when_all_submitted(
        self, authenticated_grant_recipient_member_client, factories
    ):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        grant = grant_recipient.grant
        grant.status = GrantStatusEnum.LIVE
        user = authenticated_grant_recipient_member_client.user
        question = factories.question.create(
            form__collection__grant=grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__status=CollectionStatusEnum.OPEN,
            form__collection__reporting_period_start_date=date(2025, 1, 1),
            form__collection__reporting_period_end_date=date(2025, 3, 31),
            form__collection__submission_period_start_date=date(2025, 11, 1),
            form__collection__submission_period_end_date=date(2026, 2, 28),
            form__collection__requires_certification=False,
        )
        collection = question.form.collection
        for i in range(2):
            submission = factories.submission.create(
                collection=collection,
                grant_recipient=grant_recipient,
                mode=SubmissionModeEnum.LIVE,
                answers=[FactoryAnswer(question, TextSingleLineAnswer(f"Answer {i}"))],
            )
            factories.submission_event.create(
                submission=submission,
                related_entity_id=question.form.id,
                event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
                created_by=user,
            )
            factories.submission_event.create(
                submission=submission,
                event_type=SubmissionEventType.SUBMISSION_SUBMITTED,
                created_by=user,
            )
            submission.status = SubmissionStatusEnum.SUBMITTED

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "2 submitted" in soup.text

    def test_submission_list_has_start_new_report_link(
        self, authenticated_grant_recipient_data_provider_client, factories, db_session
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__status=CollectionStatusEnum.OPEN,
            data_type=QuestionDataType.TEXT_SINGLE_LINE,
        )
        collection = question.form.collection
        collection.submission_name_question_id = question.id
        db_session.commit()

        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        start_link = page_has_link(soup, "Start a new report")
        assert start_link is not None
        assert start_link["href"] == url_for(
            "access_grant_funding.start_new_multiple_submission",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
            collection_id=collection.id,
        )

    @pytest.mark.parametrize("multiple_submissions_are_managed_by_service", [True, False])
    def test_submission_list_hides_start_new_report_when_managed_by_service(
        self,
        authenticated_grant_recipient_data_provider_client,
        factories,
        db_session,
        multiple_submissions_are_managed_by_service,
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__multiple_submissions_are_managed_by_service=multiple_submissions_are_managed_by_service,
            form__collection__status=CollectionStatusEnum.OPEN,
            data_type=QuestionDataType.TEXT_SINGLE_LINE,
        )
        collection = question.form.collection
        collection.submission_name_question_id = question.id
        db_session.commit()

        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert bool(page_has_link(soup, "Start a new report")) is not multiple_submissions_are_managed_by_service

    def test_submission_list_hides_start_new_report_when_collection_closed(
        self, authenticated_grant_recipient_data_provider_client, factories, db_session
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=True,
            form__collection__status=CollectionStatusEnum.CLOSED,
            data_type=QuestionDataType.TEXT_SINGLE_LINE,
        )
        collection = question.form.collection
        collection.submission_name_question_id = question.id
        db_session.commit()

        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert page_has_link(soup, "Start a new report") is None

    def test_submission_list_renders_guidance_as_markdown(self, authenticated_grant_recipient_member_client, factories):
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient
        collection = factories.collection.create(
            grant=grant_recipient.grant,
            allow_multiple_submissions=True,
            status=CollectionStatusEnum.OPEN,
            submission_guidance="## Getting started\n\nPlease complete a report for each project.",
        )

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.list_collection_submissions",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_id=collection.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        heading = soup.find("h2", string="Getting started")
        assert heading is not None
        assert "Please complete a report for each project." in soup.text


class TestDeclineSignOff:
    def test_decline_certification_post_success(
        self,
        authenticated_grant_recipient_certifier_client,
        factories,
        submission_awaiting_sign_off,
        grant_recipient,
        user,
        app,
        mock_notification_service_calls,
    ):
        organisation = authenticated_grant_recipient_certifier_client.organisation
        grant = authenticated_grant_recipient_certifier_client.grant

        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF
        form = submission_awaiting_sign_off.collection.forms[0]
        assert helper.get_status_for_form(form) == TasklistSectionStatusEnum.COMPLETED

        submitted_by_user = factories.user.create()
        # Give the user DATA_PROVIDER and CERTIFIER permissions to ensure they get both distinct emails sent as part
        # of this flow
        factories.user_role.create(
            user=submitted_by_user,
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.DATA_PROVIDER, RoleEnum.CERTIFIER],
        )
        certification_event = next(
            event
            for event in submission_awaiting_sign_off.events
            if event.event_type == SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION
        )
        certification_event.created_by = submitted_by_user

        decline_form = DeclineSignOffForm()
        decline_form.decline_reason.data = "Reason for declining"

        response = authenticated_grant_recipient_certifier_client.post(
            url_for(
                "access_grant_funding.decline_submission",
                organisation_id=grant_recipient.organisation_id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=decline_form.data,
            follow_redirects=False,
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.list_collections",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
        )

        assert helper.status == SubmissionStatusEnum.IN_PROGRESS
        assert helper.get_status_for_form(form) == TasklistSectionStatusEnum.IN_PROGRESS

        assert len(mock_notification_service_calls) == 3

    def test_decline_certification_post_form_validation_fails(
        self,
        authenticated_grant_recipient_certifier_client,
        factories,
        submission_awaiting_sign_off,
        grant_recipient,
        user,
        app,
        mock_notification_service_calls,
    ):
        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF
        form = submission_awaiting_sign_off.collection.forms[0]
        assert helper.get_status_for_form(form) == TasklistSectionStatusEnum.COMPLETED

        decline_form = DeclineSignOffForm()
        decline_form.decline_reason.data = ""

        response = authenticated_grant_recipient_certifier_client.post(
            url_for(
                "access_grant_funding.decline_submission",
                organisation_id=grant_recipient.organisation_id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=decline_form.data,
            follow_redirects=False,
        )
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert page_has_error(soup, "Enter a reason for declining sign off")

        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF
        assert helper.get_status_for_form(form) == TasklistSectionStatusEnum.COMPLETED

        assert len(mock_notification_service_calls) == 0

    def test_decline_certification_invalid_status(
        self,
        authenticated_grant_recipient_certifier_client,
        factories,
        grant_recipient,
        submission_in_progress,
        user,
    ):
        helper = SubmissionHelper(submission_in_progress)
        assert helper.status == SubmissionStatusEnum.IN_PROGRESS

        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.decline_submission",
                organisation_id=grant_recipient.organisation_id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_in_progress.collection.type,
                submission_id=submission_in_progress.id,
            ),
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.view_locked_submission",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
            collection_type=submission_in_progress.collection.type,
            submission_id=submission_in_progress.id,
        )

    def test_decline_certification_redirects_when_collection_closed(
        self,
        authenticated_grant_recipient_certifier_client,
        factories,
        grant_recipient,
        submission_awaiting_sign_off,
    ):
        update_collection(
            submission_awaiting_sign_off.collection,
            submission_period_start_date=datetime.date.today() - datetime.timedelta(days=1),
            submission_period_end_date=datetime.date.today(),
            status=CollectionStatusEnum.CLOSED,
        )
        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert not helper.is_awaiting_sign_off
        assert helper.in_immutable_state

        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.decline_submission",
                organisation_id=grant_recipient.organisation_id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.view_locked_submission",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
            collection_type=submission_awaiting_sign_off.collection.type,
            submission_id=submission_awaiting_sign_off.id,
        )

    def test_decline_sign_off_get(
        self, authenticated_grant_recipient_certifier_client, factories, submission_awaiting_sign_off, grant_recipient
    ):
        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.decline_submission",
                organisation_id=grant_recipient.organisation_id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
        )
        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert (
            get_h1_text(soup)
            == f"Why are you declining sign off for the {submission_awaiting_sign_off.collection.name} report?"
        )


class TestConfirmReportSubmission:
    @pytest.mark.parametrize(
        "client_fixture, can_access",
        (
            ("authenticated_no_role_client", False),
            ("authenticated_grant_recipient_member_client", False),
            ("authenticated_grant_recipient_data_provider_client", True),
            ("authenticated_grant_recipient_certifier_client", False),
        ),
    )
    def test_get_confirm_report_submission_access(
        self, submission_ready_to_submit, client_fixture, can_access, request, factories, db_session
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()
        submission_ready_to_submit.collection.requires_certification = False
        db_session.commit()

        response = client.get(
            url_for(
                "access_grant_funding.confirm_submission_direct_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_ready_to_submit.collection.type,
                submission_id=submission_ready_to_submit.id,
            ),
            follow_redirects=False,
        )

        if not can_access:
            assert response.status_code == 403
        else:
            assert response.status_code == 200
            soup = BeautifulSoup(response.data, "html.parser")
            assert get_h1_text(soup) == "Confirm and submit report"

    @pytest.mark.parametrize(
        "client_fixture, can_access",
        (
            ("authenticated_no_role_client", False),
            ("authenticated_grant_recipient_member_client", False),
            ("authenticated_grant_recipient_data_provider_client", False),
            ("authenticated_grant_recipient_certifier_client", True),
        ),
    )
    def test_get_confirm_report_submission_certify_access(
        self, submission_awaiting_sign_off, client_fixture, can_access, request, factories, db_session
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()
        submission_awaiting_sign_off.collection.requires_certification = True
        db_session.commit()

        response = client.get(
            url_for(
                "access_grant_funding.confirm_submission_with_certify",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            follow_redirects=False,
        )

        if not can_access:
            assert response.status_code == 403
        else:
            assert response.status_code == 200
            soup = BeautifulSoup(response.data, "html.parser")
            assert get_h1_text(soup) == "Confirm sign off and submit report"

    def test_get_certify_redirects_when_collection_closed(
        self, authenticated_grant_recipient_certifier_client, submission_awaiting_sign_off
    ):
        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient
        submission_awaiting_sign_off.collection.status = CollectionStatusEnum.CLOSED

        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.confirm_submission_with_certify",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            follow_redirects=False,
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.view_locked_submission",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
            collection_type=submission_awaiting_sign_off.collection.type,
            submission_id=submission_awaiting_sign_off.id,
        )

    def test_get_redirects_if_requires_certification_and_not_awaiting_sign_off(
        self, authenticated_grant_recipient_certifier_client, submission_ready_to_submit
    ):
        grant_recipient = authenticated_grant_recipient_certifier_client.grant_recipient

        response = authenticated_grant_recipient_certifier_client.get(
            url_for(
                "access_grant_funding.confirm_submission_with_certify",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_ready_to_submit.collection.type,
                submission_id=submission_ready_to_submit.id,
            ),
            follow_redirects=False,
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.view_locked_submission",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
            collection_type=submission_ready_to_submit.collection.type,
            submission_id=submission_ready_to_submit.id,
        )

    def test_get_redirects_if_not_requires_certification_and_not_ready_to_submit(
        self, authenticated_grant_recipient_data_provider_client, submission_in_progress
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        submission_in_progress.collection.requires_certification = False

        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.confirm_submission_direct_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_in_progress.collection.type,
                submission_id=submission_in_progress.id,
            ),
            follow_redirects=False,
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.list_collections",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
        )

    def test_post_confirm_report_submission_when_requires_certification(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_awaiting_sign_off,
        factories,
        mock_notification_service_calls,
    ):
        organisation = authenticated_grant_recipient_certifier_client.organisation
        grant = authenticated_grant_recipient_certifier_client.grant

        submitted_by_user = factories.user.create()
        # Give the user DATA_PROVIDER and CERTIFIER permissions to ensure they still only get one confirmation email
        factories.user_role.create(
            user=submitted_by_user,
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.DATA_PROVIDER, RoleEnum.CERTIFIER],
        )
        certification_event = next(
            event
            for event in submission_awaiting_sign_off.events
            if event.event_type == SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION
        )
        certification_event.created_by = submitted_by_user

        # Make a couple more grant recipient users to check they all receive the notification email
        additional_users = factories.user.create_batch(2)
        factories.user_role.create(
            user=additional_users[0],
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )
        factories.user_role.create(
            user=additional_users[1],
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.CERTIFIER],
        )

        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        form = GenericSubmitForm()

        response = authenticated_grant_recipient_certifier_client.post(
            url_for(
                "access_grant_funding.confirm_submission_with_certify",
                organisation_id=organisation.id,
                grant_id=grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=form.data,
            follow_redirects=False,
        )

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.submitted_confirmation",
            organisation_id=organisation.id,
            grant_id=grant.id,
            collection_type=submission_awaiting_sign_off.collection.type,
            submission_id=submission_awaiting_sign_off.id,
        )

        assert helper.status == SubmissionStatusEnum.SUBMITTED
        assert helper.events.submission_state.is_approved
        assert helper.events.submission_state.is_submitted
        assert len(mock_notification_service_calls) == 4

    def test_post_confirm_report_submission_when_no_certification(
        self,
        authenticated_grant_recipient_data_provider_client,
        submission_ready_to_submit,
        factories,
        mock_notification_service_calls,
    ):
        organisation = authenticated_grant_recipient_data_provider_client.organisation
        grant = authenticated_grant_recipient_data_provider_client.grant
        submission_ready_to_submit.collection.requires_certification = False

        # Make a couple more grant recipient users to check they all receive the notification email
        additional_users = factories.user.create_batch(2)
        factories.user_role.create(
            user=additional_users[0],
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.DATA_PROVIDER, RoleEnum.CERTIFIER],
        )
        factories.user_role.create(
            user=additional_users[1],
            organisation=organisation,
            grant=grant,
            permissions=[RoleEnum.CERTIFIER],
        )

        helper = SubmissionHelper(submission_ready_to_submit)
        assert helper.status == SubmissionStatusEnum.READY_TO_SUBMIT

        form = GenericSubmitForm()

        response = authenticated_grant_recipient_data_provider_client.post(
            url_for(
                "access_grant_funding.confirm_submission_direct_submission",
                organisation_id=organisation.id,
                grant_id=grant.id,
                collection_type=submission_ready_to_submit.collection.type,
                submission_id=submission_ready_to_submit.id,
            ),
            data=form.data,
            follow_redirects=False,
        )

        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.submitted_confirmation",
            organisation_id=organisation.id,
            grant_id=grant.id,
            collection_type=submission_ready_to_submit.collection.type,
            submission_id=submission_ready_to_submit.id,
        )

        assert helper.status == SubmissionStatusEnum.SUBMITTED
        assert helper.events.submission_state.is_submitted
        assert len(mock_notification_service_calls) == 3

    def test_post_confirm_report_submission_certify_failure_should_not_submit(
        self,
        authenticated_grant_recipient_certifier_client,
        submission_awaiting_sign_off,
        factories,
        mocker,
        app,
    ):
        organisation = authenticated_grant_recipient_certifier_client.organisation
        grant = authenticated_grant_recipient_certifier_client.grant

        submitted_by_user = factories.user.create()
        certification_event = next(
            event
            for event in submission_awaiting_sign_off.events
            if event.event_type == SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION
        )
        certification_event.created_by = submitted_by_user

        helper = SubmissionHelper(submission_awaiting_sign_off)
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF

        form = GenericSubmitForm()

        def side_effect(*args, **kwargs):
            raise Exception("Failed email")

        mocker.patch(
            "app.services.notify.NotificationService._send_email",
            side_effect=side_effect,
        )

        # for this test we don't want the test to raise the actual exception to end the test
        # as we want to assert on what the app did after the response
        mocker.patch.dict(app.config, {"TESTING": False})

        response = authenticated_grant_recipient_certifier_client.post(
            url_for(
                "access_grant_funding.confirm_submission_with_certify",
                organisation_id=organisation.id,
                grant_id=grant.id,
                collection_type=submission_awaiting_sign_off.collection.type,
                submission_id=submission_awaiting_sign_off.id,
            ),
            data=form.data,
            follow_redirects=False,
        )
        assert response.status_code == 500

        soup = BeautifulSoup(response.data, "html.parser")
        assert get_h1_text(soup) == "Sorry, there is a problem with the service"

        # even though we received a managed error response the submission should not have been
        # updated
        assert helper.status == SubmissionStatusEnum.AWAITING_SIGN_OFF
        assert helper.events.submission_state.is_approved is False
        assert helper.events.submission_state.is_submitted is False

    def test_post_confirm_report_submission_with_invalid_data_redirects_and_shows_error(
        self,
        authenticated_grant_recipient_data_provider_client,
        factories,
    ):
        client = authenticated_grant_recipient_data_provider_client
        grant_recipient = client.grant_recipient
        form = factories.form.create(title="Financial Report", collection__grant=grant_recipient.grant)
        q1 = factories.question.create(form=form, data_type=QuestionDataType.NUMBER, order=0, name="threshold")
        q2 = factories.question.create(form=form, data_type=QuestionDataType.NUMBER, order=1, name="amount")
        form.collection.requires_certification = False

        factories.expression.create(
            question=q2,
            created_by=client.user,
            type_=ExpressionType.VALIDATION,
            managed_name=ManagedExpressionsEnum.GREATER_THAN,
            statement=f"{q2.safe_qid} > {q1.safe_qid}",
            context={
                "subject_reference": ExpressionReference.from_question(q2),
                "minimum_value": None,
                "minimum_expression": ExpressionReference.from_question(q1),
            },
        )

        submission = factories.submission.create(
            collection=form.collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            answers=[
                FactoryAnswer(q1, IntegerAnswer(value=150)),
                FactoryAnswer(q2, IntegerAnswer(value=100)),
            ],
        )
        factories.submission_event.create(
            created_by=client.user,
            submission=submission,
            related_entity_id=form.id,
            event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
        )
        submission.status = SubmissionStatusEnum.READY_TO_SUBMIT

        certifier = factories.user.create()
        factories.user_role.create(
            user=certifier,
            organisation=grant_recipient.organisation,
            permissions=[RoleEnum.CERTIFIER],
        )
        response = authenticated_grant_recipient_data_provider_client.post(
            url_for(
                "access_grant_funding.confirm_submission_direct_submission",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            ),
            data={"submit": "y"},
            follow_redirects=True,
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        assert "You cannot submit because you need to review some answers" in soup.text
        assert "amount" in soup.text


class TestViewSubmittedConfirmation:
    @pytest.mark.parametrize(
        "client_fixture, requires_certification, can_access",
        (
            ("authenticated_no_role_client", False, False),
            ("authenticated_no_role_client", True, False),
            ("authenticated_grant_recipient_member_client", False, False),
            ("authenticated_grant_recipient_member_client", True, False),
            ("authenticated_grant_recipient_data_provider_client", False, True),
            ("authenticated_grant_recipient_data_provider_client", True, False),
            ("authenticated_grant_recipient_certifier_client", False, False),
            ("authenticated_grant_recipient_certifier_client", True, True),
        ),
    )
    def test_submitted_confirmation_access(
        self,
        request: FixtureRequest,
        client_fixture: str,
        requires_certification: bool,
        can_access: bool,
        factories,
        submission_submitted,
        db_session,
    ):
        client = request.getfixturevalue(client_fixture)
        grant_recipient = getattr(client, "grant_recipient", None) or factories.grant_recipient.create()
        submission_submitted.collection.requires_certification = requires_certification
        db_session.commit()

        response = client.get(
            url_for(
                "access_grant_funding.submitted_confirmation",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission_submitted.collection.type,
                submission_id=submission_submitted.id,
            )
        )

        if client_fixture == "authenticated_no_role_client":
            assert response.status_code == 403
        elif can_access:
            assert response.status_code == 200
            soup = BeautifulSoup(response.data, "html.parser")
            if requires_certification:
                assert get_h1_text(soup) == "Report signed off and submitted"
            else:
                assert get_h1_text(soup) == "Report submitted"
        else:
            assert response.status_code == 302
            assert response.location == url_for(
                "access_grant_funding.list_collections",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
            )

    @pytest.mark.parametrize(
        "submission_fixture",
        (
            ("submission_awaiting_sign_off"),
            ("submission_ready_to_submit"),
            ("submission_in_progress"),
        ),
    )
    def test_submitted_confirm_redirects_if_not_submitted(
        self, authenticated_grant_recipient_member_client, submission_fixture, request
    ):
        submission = request.getfixturevalue(submission_fixture)
        grant_recipient = authenticated_grant_recipient_member_client.grant_recipient

        response = authenticated_grant_recipient_member_client.get(
            url_for(
                "access_grant_funding.submitted_confirmation",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            )
        )
        assert response.status_code == 302
        assert response.location == url_for(
            "access_grant_funding.list_collections",
            organisation_id=grant_recipient.organisation.id,
            grant_id=grant_recipient.grant.id,
        )

    @pytest.mark.parametrize(
        "allow_multiple_submissions, expected_back_link_route",
        [
            (False, "access_grant_funding.list_collections"),
            (True, "access_grant_funding.list_collection_submissions"),
        ],
    )
    def test_return_to_reports_link_depends_on_allow_multiple_submissions(
        self,
        authenticated_grant_recipient_data_provider_client,
        factories,
        db_session,
        allow_multiple_submissions,
        expected_back_link_route,
    ):
        grant_recipient = authenticated_grant_recipient_data_provider_client.grant_recipient
        question = factories.question.create(
            form__collection__grant=grant_recipient.grant,
            form__collection__allow_multiple_submissions=allow_multiple_submissions,
            form__collection__requires_certification=False,
            form__collection__submission_period_start_date=date.today(),
            form__collection__submission_period_end_date=date.today(),
            form__collection__reporting_period_start_date=date.today(),
            form__collection__reporting_period_end_date=date.today(),
        )
        submission = factories.submission.create(
            collection=question.form.collection,
            grant_recipient=grant_recipient,
            mode=SubmissionModeEnum.LIVE,
            events=[],
            answers=[FactoryAnswer(question, TextSingleLineAnswer("Answer"))],
        )
        factories.submission_event.create(
            submission=submission,
            event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
            related_entity_id=question.form.id,
        )
        factories.submission_event.create(submission=submission, event_type=SubmissionEventType.SUBMISSION_SUBMITTED)
        submission.status = SubmissionStatusEnum.SUBMITTED
        response = authenticated_grant_recipient_data_provider_client.get(
            url_for(
                "access_grant_funding.submitted_confirmation",
                organisation_id=grant_recipient.organisation.id,
                grant_id=grant_recipient.grant.id,
                collection_type=submission.collection.type,
                submission_id=submission.id,
            )
        )

        assert response.status_code == 200
        soup = BeautifulSoup(response.data, "html.parser")
        back_link = page_has_link(soup, "Return to reports")
        expected_url_kwargs = {
            "organisation_id": grant_recipient.organisation.id,
            "grant_id": grant_recipient.grant.id,
        }
        if allow_multiple_submissions:
            expected_url_kwargs["collection_id"] = question.form.collection.id
        assert back_link["href"] == url_for(expected_back_link_route, **expected_url_kwargs)
