import uuid
from datetime import datetime
from decimal import Decimal

import pytest
from werkzeug.datastructures import MultiDict

from app.common.collections.forms import build_question_form
from app.common.collections.types import (
    DecimalAnswer,
    FileUploadAnswer,
    IntegerAnswer,
    TextSingleLineAnswer,
    YesNoAnswer,
)
from app.common.data.models import ComponentReference
from app.common.data.submission_data_manager import _deserialise_question_type
from app.common.data.types import (
    CollectionStatusEnum,
    ComponentVisibilityState,
    ConditionsOperator,
    ExpressionType,
    GrantStatusEnum,
    NumberTypeEnum,
    QuestionDataOptions,
    QuestionDataType,
    QuestionPresentationOptions,
    SubmissionEventType,
    SubmissionModeEnum,
    TasklistSectionStatusEnum,
)
from app.common.expressions import ExpressionContext
from app.common.expressions.references import EvaluationStatement
from app.common.helpers.collections import SubmissionHelper, _form_data_to_question_type
from tests.models import FactoryAnswer
from tests.utils import AnyStringMatching


class TestSubmissionHelper:
    class TestGetOrderedVisibleForms:
        def test_ordering(self, db_session, factories):
            submission = factories.submission.build()
            _form_0 = factories.form.build(order=0, collection=submission.collection)
            _form_2 = factories.form.build(order=2, collection=submission.collection)
            _form_3 = factories.form.build(order=3, collection=submission.collection)
            _form_1 = factories.form.build(order=1, collection=submission.collection)

            helper = SubmissionHelper(submission)
            helper_forms = helper.get_ordered_visible_forms()
            assert len(helper_forms) == 4
            assert [s.order for s in helper_forms] == [0, 1, 2, 3]

    class TestGetOrderedVisibleQuestions:
        def test_ordering(self, db_session, factories):
            submission = factories.submission.build()
            form = factories.form.build(order=0, collection=submission.collection)
            _question_2 = factories.question.build(order=2, form=form)
            _question_0 = factories.question.build(order=0, form=form)
            _question_1 = factories.question.build(order=1, form=form)

            helper = SubmissionHelper(submission)
            helper_questions = helper.cached_get_ordered_visible_questions(form)
            assert len(helper_questions) == 3
            assert [s.order for s in helper_questions] == [0, 1, 2]

        def test_visible_questions_filtered(self, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)
            visible_question = factories.question.build(form=form)

            invisible_question = factories.question.build(form=form)
            factories.expression.build(question=invisible_question, type_=ExpressionType.CONDITION, statement="False")

            helper = SubmissionHelper(submission)
            helper_questions = helper.cached_get_ordered_visible_questions(form)
            assert len(helper_questions) == 1
            assert helper_questions[0].id == visible_question.id

        def test_visible_questions_filtered_in_group(self, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)
            visible_question = factories.question.build(form=form)

            invisible_group = factories.group.build(form=form)
            factories.question.build(form_id=form.id, parent=invisible_group)
            factories.question.build(form_id=form.id, parent=invisible_group)
            factories.question.build(form_id=form.id, parent=invisible_group)
            factories.expression.build(question=invisible_group, type_=ExpressionType.CONDITION, statement="False")

            helper = SubmissionHelper(submission)
            helper_questions = helper.cached_get_ordered_visible_questions(form)
            assert len(helper_questions) == 1
            assert helper_questions[0].id == visible_question.id

        def test_visible_questions_filtered_for_group_parent(self, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)
            q0 = factories.question.build(form=form)
            group = factories.group.build(form=form)
            q1 = factories.question.build(form_id=form.id, parent=group)
            q2 = factories.question.build(form_id=form.id, parent=group)
            q3 = factories.question.build(form_id=form.id, parent=group)

            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement="False")

            helper = SubmissionHelper(submission)
            helper_questions = helper.cached_get_ordered_visible_questions(form)
            assert helper_questions == [q0, q1, q3]

            group_questions = helper.cached_get_ordered_visible_questions(group)
            assert group_questions == [q1, q3]

        def test_filters_questions_with_unanswered_references(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0)
            q2 = factories.question.build(form=form, order=1)
            q3 = factories.question.build(form=form, order=2)

            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            q3.owned_component_references = [ComponentReference(component=q3, depends_on_component=q2)]

            submission = factories.submission.build(collection=form.collection)
            helper = SubmissionHelper(submission)

            assert helper.cached_get_ordered_visible_questions(form) == [q1]

            submission.data_manager.set(q1, TextSingleLineAnswer("answer1"))
            helper = SubmissionHelper(submission)
            assert helper.cached_get_ordered_visible_questions(form) == [q1, q2]

            submission.data_manager.set(q2, TextSingleLineAnswer("answer2"))
            helper = SubmissionHelper(submission)
            assert helper.cached_get_ordered_visible_questions(form) == [q1, q2, q3]

    class TestGetForm:
        def test_exists(self, db_session, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_form(form.id) == form

        def test_does_not_exist(self, db_session, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            with pytest.raises(ValueError) as e:
                assert helper.get_form(uuid.uuid4())

            assert str(e.value) == AnyStringMatching(
                r"Could not find a form with id=[a-z0-9-]+ in collection=[a-z0-9-]+"
            )

    class TestGetQuestion:
        def test_exists(self, db_session, factories):
            question = factories.question.build()
            submission = factories.submission.build(collection=question.form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_question(question.id) == question

        def test_does_not_exist(self, db_session, factories):
            question = factories.question.build()
            submission = factories.submission.build(collection=question.form.collection)

            helper = SubmissionHelper(submission)
            with pytest.raises(ValueError) as e:
                assert helper.get_question(uuid.uuid4())

            assert str(e.value) == AnyStringMatching(
                r"Could not find a question with id=[a-z0-9-]+ in collection=[a-z0-9-]+"
            )

    class TestGetFirstQuestionForForm:
        def test_at_least_one_question_in_form(self, db_session, factories):
            form = factories.form.build()

            for x in reversed(range(5)):
                factories.question.build(form=form, id=uuid.UUID(int=x), order=x)

            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            question = helper.get_first_question_for_form(form)
            assert question.id == uuid.UUID("00000000-0000-0000-0000-000000000000")

        def test_no_visible_questions_in_form(self, db_session, factories):
            form = factories.form.build()

            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_first_question_for_form(form) is None

    class TestGetLastQuestionForForm:
        def test_at_least_one_question_in_form(self, db_session, factories):
            form = factories.form.build()

            for x in reversed(range(5)):
                factories.question.build(form=form, id=uuid.UUID(int=x), order=x)

            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            question = helper.get_last_question_for_form(form)
            assert question.id == uuid.UUID("00000000-0000-0000-0000-000000000004")

        def test_no_visible_questions_in_form(self, db_session, factories):
            form = factories.form.build()

            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_last_question_for_form(form) is None

    class TestGetFormForQuestion:
        def test_question_exists_in_collection_forms(self, db_session, factories):
            collection = factories.collection.build()
            form = factories.form.build(collection=collection)

            for x in reversed(range(5)):
                factories.question.build(form=form, id=uuid.UUID(int=x), order=x)

            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)

            for question in form.cached_questions:
                assert helper.get_form_for_question(question.id) == form

        def test_question_does_not_exist_in_collection_forms(self, db_session, factories):
            form = factories.form.build()
            factories.question.build(form=form, id=uuid.UUID(int=0), order=0)
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            with pytest.raises(ValueError) as e:
                helper.get_form_for_question(uuid.UUID(int=1))

            assert str(e.value) == AnyStringMatching(
                r"Could not find form for question_id=00000000-0000-0000-0000-000000000001 "
                r"in collection=[a-z0-9-]+"
            )

    class TestGetNextQuestion:
        def test_current_question_exists_and_is_not_last_question(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            assert helper.get_next_question(uuid.UUID(int=0)).id == uuid.UUID(int=1)
            assert helper.get_next_question(uuid.UUID(int=1)).id == uuid.UUID(int=2)
            assert helper.get_next_question(uuid.UUID(int=2)).id == uuid.UUID(int=3)
            assert helper.get_next_question(uuid.UUID(int=3)).id == uuid.UUID(int=4)

        def test_current_question_exists_but_is_last_question(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            assert helper.get_next_question(uuid.UUID(int=4)) is None

        def test_current_question_does_not_exist(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            with pytest.raises(ValueError) as e:
                helper.get_next_question(uuid.UUID(int=9))

            assert str(e.value) == AnyStringMatching(
                r"Could not find form for question_id=00000000-0000-0000-0000-000000000009 "
                r"in collection=[a-z0-9-]+"
            )

        def test_next_question_ignores_not_visible_questions(self, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)
            question_one = factories.question.build(form=form, id=uuid.UUID(int=0), order=0)
            question_two = factories.question.build(form=form, id=uuid.UUID(int=1), order=1)
            question_three = factories.question.build(form=form, id=uuid.UUID(int=2), order=2)

            factories.expression.build(question=question_two, type_=ExpressionType.CONDITION, statement="False")

            helper = SubmissionHelper(submission)

            question = helper.get_next_question(question_one.id)
            assert question
            assert question.id == question_three.id

        def test_next_question_with_add_another_index(self, factories):
            form = factories.form.build()
            q0 = factories.question.build(form=form, order=0)
            group = factories.group.build(form=form, add_another=True, order=1)
            q1 = factories.question.build(parent=group, data_type=QuestionDataType.NUMBER, order=0)
            q2 = factories.question.build(parent=group, order=1)
            q3 = factories.question.build(parent=group, order=2)
            q4 = factories.question.build(form=form, order=2)

            submission = factories.submission.build(
                collection=form.collection,
                answers=[
                    FactoryAnswer(q1, IntegerAnswer(value=55), add_another_index=0),
                    FactoryAnswer(q1, IntegerAnswer(value=20), add_another_index=1),
                ],
            )
            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50")

            helper = SubmissionHelper(submission)

            assert helper.get_next_question(q0.id).id == q1.id
            assert helper.get_next_question(q3.id).id == q4.id

            # without any information to go with, the default behaviour is for the condition to fail closed
            # which will mean the question is skipped
            assert helper.get_next_question(q1.id).id == q3.id

            # when the context is provided, the question is shown or hidden appropriately
            assert helper.get_next_question(q1.id, add_another_index=0).id == q2.id
            assert helper.get_next_question(q1.id, add_another_index=1).id == q3.id

    class TestGetPreviousQuestion:
        def test_current_question_exists_and_is_not_first_question(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            assert helper.get_previous_question(uuid.UUID(int=1)).id == uuid.UUID(int=0)
            assert helper.get_previous_question(uuid.UUID(int=2)).id == uuid.UUID(int=1)
            assert helper.get_previous_question(uuid.UUID(int=3)).id == uuid.UUID(int=2)
            assert helper.get_previous_question(uuid.UUID(int=4)).id == uuid.UUID(int=3)

        def test_current_question_exists_but_is_first_question(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            assert helper.get_previous_question(uuid.UUID(int=0)) is None

        def test_current_question_does_not_exist(self, db_session, factories):
            form = factories.form.build()
            for x in range(5):
                factories.question.build(form=form, id=uuid.UUID(int=x))
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)

            with pytest.raises(ValueError) as e:
                helper.get_previous_question(uuid.UUID(int=9))

            assert str(e.value) == AnyStringMatching(
                r"Could not find form for question_id=00000000-0000-0000-0000-000000000009 "
                r"in collection=[a-z0-9-]+"
            )

        def test_previous_question_ignores_not_visible_questions(self, factories):
            form = factories.form.build()
            submission = factories.submission.build(collection=form.collection)
            question_one = factories.question.build(form=form, id=uuid.UUID(int=0), order=0)
            question_two = factories.question.build(form=form, id=uuid.UUID(int=1), order=1)
            question_three = factories.question.build(form=form, id=uuid.UUID(int=2), order=2)

            factories.expression.build(question=question_two, type_=ExpressionType.CONDITION, statement="False")

            helper = SubmissionHelper(submission)

            question = helper.get_previous_question(question_three.id)
            assert question
            assert question.id == question_one.id

        def test_previous_question_with_add_another_index(self, factories):
            form = factories.form.build()
            q0 = factories.question.build(form=form, order=0)
            group = factories.group.build(form=form, add_another=True, order=1)
            q1 = factories.question.build(parent=group, data_type=QuestionDataType.NUMBER, order=0)
            q2 = factories.question.build(parent=group, order=1)
            q3 = factories.question.build(parent=group, order=2)
            q4 = factories.question.build(form=form, order=2)

            submission = factories.submission.build(
                collection=form.collection,
                answers=[
                    FactoryAnswer(q1, IntegerAnswer(value=55), add_another_index=0),
                    FactoryAnswer(q1, IntegerAnswer(value=20), add_another_index=1),
                ],
            )
            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50")

            helper = SubmissionHelper(submission)

            assert helper.get_previous_question(q1.id).id == q0.id
            assert helper.get_previous_question(q4.id).id == q3.id

            assert helper.get_previous_question(q3.id).id == q1.id

            assert helper.get_previous_question(q3.id, add_another_index=0).id == q2.id
            assert helper.get_previous_question(q3.id, add_another_index=1).id == q1.id

    class TestGetAllQuestionsAreAnsweredForForm:
        def test_all_questions_answered(self, factories):
            q1 = factories.question.build()
            q2 = factories.question.build(form=q1.form)
            submission = factories.submission.build(collection=q1.form.collection)
            submission.data_manager.set(q1, TextSingleLineAnswer("answer 1"))
            helper = SubmissionHelper(submission)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(q1.form).all_answered
            assert all_answered is False

            helper.clear_caches()

            submission.data_manager.set(q2, TextSingleLineAnswer("answer 2"))

            all_answered = helper.cached_get_all_questions_are_answered_for_form(q1.form).all_answered
            assert all_answered is True

        # this may sit more nicely as an integration test as its checking expression evaluation
        # and other moving parts
        def test_all_questions_answered_with_conditions(self, factories):
            q1 = factories.question.build()
            q2 = factories.question.build(form=q1.form)
            submission = factories.submission.build(collection=q1.form.collection)
            submission.data_manager.set(q1, TextSingleLineAnswer("answer 1"))
            helper = SubmissionHelper(submission)

            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement="False")

            all_answered = helper.cached_get_all_questions_are_answered_for_form(q1.form).all_answered
            assert all_answered is True

        def test_all_questions_answered_with_add_another_empty(self, factories):
            group = factories.group.build(add_another=True)
            _q1 = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(collection=group.form.collection)
            helper = SubmissionHelper(submission)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(group.form).all_answered

            # answers are currently required, no entries means the add another is not answered
            assert all_answered is False

        def test_all_questions_answered_with_add_another_empty_individual_conditions(self, factories):
            q1 = factories.question.build(data_type=QuestionDataType.NUMBER)
            group = factories.group.build(form=q1.form, add_another=True)
            q2 = factories.question.build(form=q1.form, parent=group)
            submission = factories.submission.build(collection=group.form.collection)

            # this scenario should rarely happen but if each of the questions in the add another
            # have independent failing conditions on questions outside the questions will never get asked
            # and shouldn't block - the preferred way to do this would be to specify the conditions
            # for the group showing on the group itself
            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50")
            submission.data_manager.set(q1, IntegerAnswer(value=30))

            assert (
                SubmissionHelper(submission).cached_get_all_questions_are_answered_for_form(group.form).all_answered
                is True
            )

            submission.data_manager.set(q1, IntegerAnswer(value=100))
            assert (
                SubmissionHelper(submission).cached_get_all_questions_are_answered_for_form(group.form).all_answered
                is False
            )

        def test_all_questions_answered_with_add_another(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(collection=group.form.collection)
            submission.data_manager.set(q1, TextSingleLineAnswer("answer 1"), add_another_index=0)
            submission.data_manager.set(q1, TextSingleLineAnswer("answer 2"), add_another_index=1)
            submission.data_manager.set(q2, TextSingleLineAnswer("answer 2"), add_another_index=1)
            helper = SubmissionHelper(submission)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(group.form).all_answered
            assert all_answered is False

            helper.clear_caches()

            submission.data_manager.set(q2, TextSingleLineAnswer("answer 1"), add_another_index=0)
            submission.data_manager.set(q2, TextSingleLineAnswer("answer 2"), add_another_index=1)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(group.form).all_answered
            assert all_answered is True

        # this may sit more nicely as an integration test as its checking expression evaluation
        # and other moving parts
        def test_all_questions_answered_with_add_another_conditions(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group, data_type=QuestionDataType.NUMBER)
            q2 = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, IntegerAnswer(value=20), add_another_index=0),
                    FactoryAnswer(q1, IntegerAnswer(value=55), add_another_index=1),
                    FactoryAnswer(q2, TextSingleLineAnswer("answer 2"), add_another_index=1),
                    FactoryAnswer(q1, IntegerAnswer(value=60), add_another_index=2),
                ],
            )
            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50")
            helper = SubmissionHelper(submission)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(group.form).all_answered
            assert all_answered is False

            helper.clear_caches()

            submission.data_manager.set(q2, TextSingleLineAnswer("answer 3"), add_another_index=2)

            all_answered = helper.cached_get_all_questions_are_answered_for_form(group.form).all_answered
            assert all_answered is True

    class TestStatuses:
        def test_all_needed_forms_are_completed(self, db_session, factories):
            form_one = factories.form.build()
            form_two = factories.form.build(collection=form_one.collection)
            question_one = factories.question.build(form=form_one)
            question_two = factories.question.build(form=form_two)

            submission = factories.submission.build(collection=form_one.collection)
            helper = SubmissionHelper(submission)

            # empty collections are not completed
            assert helper.all_needed_forms_are_completed is False

            submission.data_manager.set(question_one, TextSingleLineAnswer("User submitted data"))
            submission.events = [
                factories.submission_event.build(
                    submission=submission,
                    related_entity_id=form_one.id,
                    event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
                )
            ]
            helper.clear_caches()

            # one complete form and one incomplete is still not completed
            assert helper.all_needed_forms_are_completed is False

            submission.data_manager.set(question_two, TextSingleLineAnswer("User submitted data"))
            helper.clear_caches()

            # all questions complete but a form not marked as completed is still not completed
            assert helper.all_needed_forms_are_completed is False

            submission.events.append(
                factories.submission_event.build(
                    submission=submission,
                    related_entity_id=form_two.id,
                    event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
                )
            )
            helper.clear_caches()

            # all questions answered and all marked as complete is complete
            assert helper.all_needed_forms_are_completed is True

    class TestVisibleQuestion:
        def test_is_question_always_visible_with_no_conditions(self, factories):
            question = factories.question.build()
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True

        def test_is_component_visible_not_visible_with_failing_condition(self, factories):
            question = factories.question.build()
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

        def test_is_component_visible_visible_with_passing_condition(self, factories):
            question = factories.question.build()
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True

        def test_is_component_visible_not_visible_with_nested_conditions(self, factories):
            group = factories.group.build()
            sub_group = factories.group.build(parent=group)
            question = factories.question.build(form=group.form)
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            expression = factories.expression.build(question=group, type_=ExpressionType.CONDITION, statement="False")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True
            assert helper.is_component_visible(group, helper.cached_evaluation_context) is False

            # when nested sub-components inherit the property of their parents
            question.parent = group
            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

            # when further nested this still applies
            question.parent = sub_group
            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

            # if the parents condition changes this is reflected
            expression.statement = EvaluationStatement("True")
            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True

        def test_is_component_visible_visible_with_add_another_expression_index(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("True"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("False"), add_another_index=1),
                ],
            )
            helper = SubmissionHelper(submission)

            factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'True'"
            )

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True

            # the expression defaults to false if there is an condition on the same add another container and
            # no index is provided
            assert helper.is_component_visible(q2, helper.cached_evaluation_context) is False

            # the expression is evaluated appropriately when an index is provided
            assert helper.is_component_visible(q2, helper.cached_evaluation_context, add_another_index=0) is True
            assert helper.is_component_visible(q2, helper.cached_evaluation_context, add_another_index=1) is False

        def test_is_component_visible_with_any_operator_one_true_condition(self, factories):
            question = factories.question.build(conditions_operator=ConditionsOperator.ANY)
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True

        def test_is_component_visible_with_any_operator_all_false_conditions(self, factories):
            question = factories.question.build(conditions_operator=ConditionsOperator.ANY)
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

        def test_is_component_visible_with_all_operator_requires_all_true(self, factories):
            question = factories.question.build(conditions_operator=ConditionsOperator.ALL)
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")

            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

        def test_is_component_visible_with_nested_groups_different_operators(self, factories):
            group = factories.group.build(conditions_operator=ConditionsOperator.ANY)
            question = factories.question.build(
                form=group.form, parent=group, conditions_operator=ConditionsOperator.ALL
            )
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            # Group has ANY operator with one True and one False condition
            factories.expression.build(question=group, type_=ExpressionType.CONDITION, statement="True")
            factories.expression.build(question=group, type_=ExpressionType.CONDITION, statement="False")

            # Question has ALL operator with all True conditions
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")

            assert helper.is_component_visible(group, helper.cached_evaluation_context) is True
            assert helper.is_component_visible(question, helper.cached_evaluation_context) is True

        def test_is_component_visible_child_hidden_when_parent_hidden_regardless_of_operator(self, factories):
            group = factories.group.build(conditions_operator=ConditionsOperator.ALL)
            question = factories.question.build(
                form=group.form, parent=group, conditions_operator=ConditionsOperator.ANY
            )
            helper = SubmissionHelper(factories.submission.build(collection=question.form.collection))

            # Group has ALL operator with one False condition (so hidden)
            factories.expression.build(question=group, type_=ExpressionType.CONDITION, statement="False")

            # Question has ANY operator with one True condition
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")

            assert helper.is_component_visible(group, helper.cached_evaluation_context) is False
            assert helper.is_component_visible(question, helper.cached_evaluation_context) is False

        def test_is_component_not_visible_when_referenced_question_unanswered(self, factories):
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True
            assert helper.is_component_visible(q2, helper.cached_evaluation_context) is False

        def test_is_component_visible_when_referenced_question_answered(self, factories):
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(
                collection=q1.form.collection,
                answers=[FactoryAnswer(q1, TextSingleLineAnswer("answer"))],
            )
            helper = SubmissionHelper(submission)

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True
            assert helper.is_component_visible(q2, helper.cached_evaluation_context) is True

        def test_is_component_visible_ignores_references_to_groups(self, factories):
            group = factories.group.build()
            q1 = factories.question.build(form=group.form)
            q1.owned_component_references = [ComponentReference(component=q1, depends_on_component=group)]
            submission = factories.submission.build(collection=group.form.collection)
            helper = SubmissionHelper(submission)

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True

        def test_is_component_visible_ignores_self_references(self, factories):
            q1 = factories.question.build()
            q1.owned_component_references = [ComponentReference(component=q1, depends_on_component=q1)]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True

        def test_is_component_visible_ignores_data_source_column_references(self, factories):
            q1 = factories.question.build()
            q1.owned_component_references = [
                ComponentReference(component=q1, depends_on_data_source_id=uuid.uuid4(), depends_on_column_name="c_x")
            ]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert helper.is_component_visible(q1, helper.cached_evaluation_context) is True

        def test_any_operator_with_hidden_referenced_question(self, factories):
            """Scenario: Q1 yes/no, Q2 yes/no (show if Q1=yes), Q3 text (show if Q1=no OR Q2=yes).

            When Q1="no": Q2 is hidden (its condition Q1=="yes" fails). Q3 should be VISIBLE
            because ANY only needs one condition — Q1=="no" is True.
            When Q1="yes", Q2 unanswered: Q3 should be UNDETERMINED (Q1=="no" is False, Q2=="yes"
            can't be evaluated yet).
            When Q1="yes", Q2="yes": Q3 should be VISIBLE (Q2=="yes" passes).
            When Q1="yes", Q2="no": Q3 should be HIDDEN (both conditions fail).
            """
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            q2_expr1 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'yes'"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, depends_on_component=q1, expression=q2_expr1)
            ]

            q3 = factories.question.build(form=q1.form, order=2, conditions_operator=ConditionsOperator.ANY)
            q3_expr1 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'no'"
            )
            q3_expr2 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} == 'yes'"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, depends_on_component=q1, expression=q3_expr1),
                ComponentReference(component=q3, depends_on_component=q2, expression=q3_expr2),
            ]

            # Case 1: Q1="no" → Q2 hidden, Q3 visible (Q1=="no" is True)
            submission = factories.submission.build(
                collection=q1.form.collection, answers=[FactoryAnswer(q1, TextSingleLineAnswer("no"))]
            )
            helper = SubmissionHelper(submission)
            assert helper.is_component_visible(q2, helper.cached_evaluation_context) is False
            assert helper.is_component_visible(q3, helper.cached_evaluation_context) is True

            # Case 2: Q1="yes", Q2 not answered → Q3 undetermined
            submission = factories.submission.build(
                collection=q1.form.collection, answers=[FactoryAnswer(q1, TextSingleLineAnswer("yes"))]
            )
            helper = SubmissionHelper(submission)
            assert helper.is_component_visible(q2, helper.cached_evaluation_context) is True
            visibility = helper.get_component_visibility_state(q3, helper.cached_evaluation_context)
            assert visibility == ComponentVisibilityState.UNDETERMINED

            # Case 3: Q1="yes", Q2="yes" → Q3 visible
            submission = factories.submission.build(
                collection=q1.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("yes")),
                    FactoryAnswer(q2, TextSingleLineAnswer("yes")),
                ],
            )
            helper = SubmissionHelper(submission)
            assert helper.is_component_visible(q3, helper.cached_evaluation_context) is True

            # Case 4: Q1="yes", Q2="no" → Q3 hidden
            submission = factories.submission.build(
                collection=q1.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("yes")),
                    FactoryAnswer(q2, TextSingleLineAnswer("no")),
                ],
            )
            helper = SubmissionHelper(submission)
            assert helper.is_component_visible(q3, helper.cached_evaluation_context) is False

    class TestGetCountForAddAnother:
        def test_empty(self, db_session, factories):
            group = factories.group.build()
            submission = factories.submission.build(collection=group.form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_count_for_add_another(group) == 0

        def test_with_answers(self, db_session, factories):
            group = factories.group.build(add_another=True)
            question = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(question, TextSingleLineAnswer("answer 0"), add_another_index=0),
                    FactoryAnswer(question, TextSingleLineAnswer("answer 1"), add_another_index=1),
                    FactoryAnswer(question, TextSingleLineAnswer("answer 2"), add_another_index=2),
                ],
            )

            helper = SubmissionHelper(submission)
            assert helper.get_count_for_add_another(group) == 3

    class TestGetAnswerSummaryForAddAnother:
        def test_valid_summary_line_no_option_gets_all(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(parent=group)
            q2 = factories.question.build(parent=group)
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("line 0 answer 0"), add_another_index=0),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 0 answer 1"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 1 answer 0"), add_another_index=1),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 1 answer 1"), add_another_index=1),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 2 answer 0"), add_another_index=2),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 2 answer 1"), add_another_index=2),
                ],
            )
            helper = SubmissionHelper(submission)
            assert (
                helper.get_answer_summary_for_add_another(group, add_another_index=0).summary
                == "line 0 answer 0, line 0 answer 1"
            )

        def test_valid_summary_line_no_valid_options_gets_all(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(parent=group)
            q2 = factories.question.build(parent=group)

            group.question_presentation_options = QuestionPresentationOptions(
                add_another_summary_line_question_ids=[uuid.uuid4(), uuid.uuid4()]
            )
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("line 0 answer 0"), add_another_index=0),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 0 answer 1"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 1 answer 0"), add_another_index=1),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 1 answer 1"), add_another_index=1),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 2 answer 0"), add_another_index=2),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 2 answer 1"), add_another_index=2),
                ],
            )
            helper = SubmissionHelper(submission)
            assert (
                helper.get_answer_summary_for_add_another(group, add_another_index=0).summary
                == "line 0 answer 0, line 0 answer 1"
            )

        def test_valid_summary_line_valid_options_subselect_questions(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(parent=group)
            q2 = factories.question.build(parent=group)

            group.presentation_options = QuestionPresentationOptions(add_another_summary_line_question_ids=[q1.id])
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("line 0 answer 0"), add_another_index=0),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 0 answer 1"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 1 answer 0"), add_another_index=1),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 1 answer 1"), add_another_index=1),
                    FactoryAnswer(q1, TextSingleLineAnswer("line 2 answer 0"), add_another_index=2),
                    FactoryAnswer(q2, TextSingleLineAnswer("line 2 answer 1"), add_another_index=2),
                ],
            )
            helper = SubmissionHelper(submission)
            assert helper.get_answer_summary_for_add_another(group, add_another_index=0).summary == "line 0 answer 0"

        def test_empty_for_no_answers(self, factories):
            group = factories.group.build(add_another=True)
            factories.question.build(parent=group)
            submission = factories.submission.build(collection=group.form.collection)
            helper = SubmissionHelper(submission)

            assert helper.get_answer_summary_for_add_another(group, add_another_index=0).summary == ""

        def test_valid_summary_line_for_single_add_another_question(self, factories):
            q1 = factories.question.build(add_another=True)
            submission = factories.submission.build(
                collection=q1.form.collection,
                answers=[FactoryAnswer(q1, TextSingleLineAnswer("line 0 answer 0"), add_another_index=0)],
            )
            helper = SubmissionHelper(submission)
            assert helper.get_answer_summary_for_add_another(q1, add_another_index=0).summary == "line 0 answer 0"

        def test_raises_for_non_add_another(self, factories):
            q1 = factories.question.build(add_another=False)
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            with pytest.raises(ValueError) as e:
                helper.get_answer_summary_for_add_another(q1, add_another_index=0)
            assert str(e.value) == "answer summaries can only be generated for components in an add another container"

        def test_get_all_answered(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("True"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("False"), add_another_index=1),
                    FactoryAnswer(q1, TextSingleLineAnswer("True"), add_another_index=2),
                    FactoryAnswer(q2, TextSingleLineAnswer("True"), add_another_index=2),
                ],
            )
            helper = SubmissionHelper(submission)

            factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'True'"
            )

            assert helper.get_answer_summary_for_add_another(q1, add_another_index=0).is_answered is False
            assert helper.get_answer_summary_for_add_another(q1, add_another_index=1).is_answered is True
            assert helper.get_answer_summary_for_add_another(q1, add_another_index=1).is_answered is True

    class TestSentForCertificationBy:
        def test_property_gets_submitted_by_user(self, factories):
            user = factories.user.build()
            submission = factories.submission.build(mode=SubmissionModeEnum.LIVE)
            factories.submission_event.build(
                submission=submission, event_type=SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION, created_by=user
            )
            helper = SubmissionHelper(submission)

            assert helper.sent_for_certification_by == user

        def test_property_returns_none_when_no_submission_for_certification(self, factories):
            submission = factories.submission.build(mode=SubmissionModeEnum.LIVE)
            helper = SubmissionHelper(submission)

            assert helper.sent_for_certification_by is None

        def test_property_with_multiple_submission_events(self, factories):
            user = factories.user.build()
            user2 = factories.user.build()
            submission = factories.submission.build(mode=SubmissionModeEnum.LIVE)
            factories.submission_event.build(
                submission=submission,
                event_type=SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION,
                created_by=user2,
                created_at_utc=datetime(2020, 1, 1, 13, 30, 0),
            )
            factories.submission_event.build(
                submission=submission,
                event_type=SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION,
                created_by=user,
                created_at_utc=datetime(2025, 12, 1, 13, 30, 0),
            )
            factories.submission_event.build(
                submission=submission,
                event_type=SubmissionEventType.SUBMISSION_SENT_FOR_CERTIFICATION,
                created_by=user2,
                created_at_utc=datetime(2022, 6, 1, 13, 30, 0),
            )
            helper = SubmissionHelper(submission)

            assert helper.sent_for_certification_by == user

    class TestCanStartForm:
        def test_can_start_when_no_references(self, factories):
            form = factories.form.build()
            factories.question.build(form=form)
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.can_start_form(form) is True

        def test_can_start_when_reference_is_within_same_form(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form)
            q2 = factories.question.build(form=form)
            q2.owned_component_references = [
                ComponentReference(component=q2, depends_on_component=q1),
            ]
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.can_start_form(form) is True

        def test_cannot_start_when_cross_form_question_reference_unanswered(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=q_a),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.can_start_form(form_b) is False

        def test_can_start_when_cross_form_question_reference_answered(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=q_a),
            ]
            submission = factories.submission.build(
                collection=collection,
                answers=[FactoryAnswer(q_a, TextSingleLineAnswer("answered"))],
            )

            helper = SubmissionHelper(submission)
            assert helper.can_start_form(form_b) is True

        def test_ignores_cross_form_references_to_groups(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            group_a = factories.group.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=group_a),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.can_start_form(form_b) is True

    class TestGetReferencedFormsWithUnansweredReferences:
        def test_returns_empty_when_no_references(self, factories):
            form = factories.form.build()
            factories.question.build(form=form)
            submission = factories.submission.build(collection=form.collection)

            helper = SubmissionHelper(submission)
            assert helper.get_referenced_forms_with_unanswered_references(form) == []

        def test_returns_forms_with_unanswered_cross_form_references(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=q_a),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            result = helper.get_referenced_forms_with_unanswered_references(form_b)
            assert result == [form_a]

        def test_ignores_data_source_column_references(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_data_source_id=uuid.uuid4(), depends_on_column_name="c_x"),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_referenced_forms_with_unanswered_references(form_b) == []

        def test_returns_sorted_by_form_order(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            form_c = factories.form.build(collection=collection, order=2)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_c = factories.question.build(form=form_c)
            q_c.owned_component_references = [
                ComponentReference(component=q_c, depends_on_component=q_b),
                ComponentReference(component=q_c, depends_on_component=q_a),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            result = helper.get_referenced_forms_with_unanswered_references(form_c)
            assert result == [form_a, form_b]

        def test_deduplicates_forms(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a1 = factories.question.build(form=form_a, order=0)
            q_a2 = factories.question.build(form=form_a, order=1)
            q_b1 = factories.question.build(form=form_b, order=0)
            q_b2 = factories.question.build(form=form_b, order=1)
            q_b1.owned_component_references = [
                ComponentReference(component=q_b1, depends_on_component=q_a1),
            ]
            q_b2.owned_component_references = [
                ComponentReference(component=q_b2, depends_on_component=q_a2),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            result = helper.get_referenced_forms_with_unanswered_references(form_b)
            assert result == [form_a]

        def test_does_not_return_form_when_referenced_question_is_definitively_hidden(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            cond_q_b = factories.expression.build(
                question=q_b, type_=ExpressionType.CONDITION, statement=f"{q_a.safe_qid} > 100"
            )
            q_b.owned_component_references = [
                ComponentReference(component=q_b, expression=cond_q_b, depends_on_component=q_a),
            ]
            factories.expression.build(question=q_a, type_=ExpressionType.CONDITION, statement="False")
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_referenced_forms_with_unanswered_references(form_b) == []

        def test_returns_form_when_referenced_question_visibility_is_undetermined(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_prereq = factories.question.build(form=form_a, order=0)
            q_a = factories.question.build(form=form_a, order=1)
            q_b = factories.question.build(form=form_b)
            cond_q_b = factories.expression.build(
                question=q_b, type_=ExpressionType.CONDITION, statement=f"{q_a.safe_qid} > 100"
            )
            q_b.owned_component_references = [
                ComponentReference(component=q_b, expression=cond_q_b, depends_on_component=q_a),
            ]
            cond_q_a = factories.expression.build(
                question=q_a, type_=ExpressionType.CONDITION, statement=f"{q_prereq.safe_qid} > 50"
            )
            q_a.owned_component_references = [
                ComponentReference(component=q_a, expression=cond_q_a, depends_on_component=q_prereq),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_referenced_forms_with_unanswered_references(form_b) == [form_a]

    class TestGetTasklistStatusForForm:
        def test_returns_cannot_start_yet_when_cross_form_reference_unanswered(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=q_a),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_tasklist_status_for_form(form_b) == TasklistSectionStatusEnum.CANNOT_START_YET

        def test_returns_not_started_when_references_satisfied(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            q_b.owned_component_references = [
                ComponentReference(component=q_b, depends_on_component=q_a),
            ]
            submission = factories.submission.build(
                collection=collection,
                answers=[FactoryAnswer(q_a, TextSingleLineAnswer("answered"))],
            )

            helper = SubmissionHelper(submission)
            assert helper.get_tasklist_status_for_form(form_b) == TasklistSectionStatusEnum.NOT_STARTED

        def test_returns_not_needed_when_cross_form_ref_to_definitively_hidden_question(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_a = factories.question.build(form=form_a)
            q_b = factories.question.build(form=form_b)
            cond_q_b = factories.expression.build(
                question=q_b, type_=ExpressionType.CONDITION, statement=f"{q_a.safe_qid} > 100"
            )
            q_b.owned_component_references = [
                ComponentReference(component=q_b, expression=cond_q_b, depends_on_component=q_a),
            ]
            factories.expression.build(question=q_a, type_=ExpressionType.CONDITION, statement="False")
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_tasklist_status_for_form(form_b) == TasklistSectionStatusEnum.NOT_NEEDED

        def test_returns_cannot_start_when_cross_form_ref_visibility_undetermined(self, factories):
            collection = factories.collection.build()
            form_a = factories.form.build(collection=collection, order=0)
            form_b = factories.form.build(collection=collection, order=1)
            q_prereq = factories.question.build(form=form_a, order=0)
            q_a = factories.question.build(form=form_a, order=1)
            q_b = factories.question.build(form=form_b)
            cond_q_b = factories.expression.build(
                question=q_b, type_=ExpressionType.CONDITION, statement=f"{q_a.safe_qid} > 100"
            )
            q_b.owned_component_references = [
                ComponentReference(component=q_b, expression=cond_q_b, depends_on_component=q_a),
            ]
            cond_q_a = factories.expression.build(
                question=q_a, type_=ExpressionType.CONDITION, statement=f"{q_prereq.safe_qid} > 50"
            )
            q_a.owned_component_references = [
                ComponentReference(component=q_a, expression=cond_q_a, depends_on_component=q_prereq),
            ]
            submission = factories.submission.build(collection=collection)

            helper = SubmissionHelper(submission)
            assert helper.get_tasklist_status_for_form(form_b) == TasklistSectionStatusEnum.CANNOT_START_YET

    class TestGetComponentVisibilityState:
        def test_returns_visible_when_no_conditions(self, factories):
            question = factories.question.build()
            submission = factories.submission.build(collection=question.form.collection)
            helper = SubmissionHelper(submission)
            assert (
                helper.get_component_visibility_state(question, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )

        def test_returns_visible_when_conditions_pass(self, factories):
            question = factories.question.build()
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="True")
            submission = factories.submission.build(collection=question.form.collection)
            helper = SubmissionHelper(submission)
            assert (
                helper.get_component_visibility_state(question, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )

        def test_returns_hidden_when_conditions_fail(self, factories):
            question = factories.question.build()
            factories.expression.build(question=question, type_=ExpressionType.CONDITION, statement="False")
            submission = factories.submission.build(collection=question.form.collection)
            helper = SubmissionHelper(submission)
            assert (
                helper.get_component_visibility_state(question, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_returns_undetermined_when_referenced_question_visible_but_unanswered(self, factories):
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            condition = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=condition, depends_on_component=q1)
            ]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_returns_hidden_when_referenced_question_is_hidden(self, factories):
            q1 = factories.question.build(order=0)
            factories.expression.build(question=q1, type_=ExpressionType.CONDITION, statement="False")
            q2 = factories.question.build(form=q1.form, order=1)
            condition = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} > 50"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=condition, depends_on_component=q1)
            ]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_chained_visibility_with_early_failure(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0, data_type=QuestionDataType.NUMBER)
            q2 = factories.question.build(form=form, order=1, data_type=QuestionDataType.NUMBER)
            q3 = factories.question.build(form=form, order=2, data_type=QuestionDataType.NUMBER)
            q4 = factories.question.build(form=form, order=3, data_type=QuestionDataType.NUMBER)

            cond_q2 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} >= 10"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=cond_q2, depends_on_component=q1)
            ]

            cond_q3 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} >= 20"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=cond_q3, depends_on_component=q2)
            ]

            cond_q4 = factories.expression.build(
                question=q4, type_=ExpressionType.CONDITION, statement=f"{q3.safe_qid} > 30"
            )
            q4.owned_component_references = [
                ComponentReference(component=q4, expression=cond_q4, depends_on_component=q3)
            ]

            submission = factories.submission.build(
                collection=form.collection,
                answers=[FactoryAnswer(q1, IntegerAnswer(value=6))],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )
            assert (
                helper.get_component_visibility_state(q4, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_chained_visibility_with_partial_data(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0, data_type=QuestionDataType.NUMBER)
            q2 = factories.question.build(form=form, order=1, data_type=QuestionDataType.NUMBER)
            q3 = factories.question.build(form=form, order=2, data_type=QuestionDataType.NUMBER)
            q4 = factories.question.build(form=form, order=3, data_type=QuestionDataType.NUMBER)

            cond_q2 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} >= 10"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=cond_q2, depends_on_component=q1)
            ]

            cond_q3 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} >= 20"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=cond_q3, depends_on_component=q2)
            ]

            cond_q4 = factories.expression.build(
                question=q4, type_=ExpressionType.CONDITION, statement=f"{q3.safe_qid} > 30"
            )
            q4.owned_component_references = [
                ComponentReference(component=q4, expression=cond_q4, depends_on_component=q3)
            ]

            submission = factories.submission.build(
                collection=form.collection,
                answers=[FactoryAnswer(q1, IntegerAnswer(value=11))],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )
            assert (
                helper.get_component_visibility_state(q4, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_returns_undetermined_when_text_interpolation_ref_unanswered(self, factories):
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_returns_hidden_when_text_interpolation_ref_is_hidden(self, factories):
            q1 = factories.question.build(order=0)
            factories.expression.build(question=q1, type_=ExpressionType.CONDITION, statement="False")
            q2 = factories.question.build(form=q1.form, order=1)
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_returns_visible_when_text_interpolation_ref_answered(self, factories):
            q1 = factories.question.build(order=0)
            q2 = factories.question.build(form=q1.form, order=1)
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(
                collection=q1.form.collection,
                answers=[FactoryAnswer(q1, TextSingleLineAnswer("answer"))],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )

        def test_handles_circular_references_without_infinite_loop(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0)
            q2 = factories.question.build(form=form, order=1)
            q1.owned_component_references = [ComponentReference(component=q1, depends_on_component=q2)]
            q2.owned_component_references = [ComponentReference(component=q2, depends_on_component=q1)]
            submission = factories.submission.build(collection=form.collection)
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q1, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_ignores_self_references(self, factories):
            q1 = factories.question.build()
            q1.owned_component_references = [ComponentReference(component=q1, depends_on_component=q1)]
            submission = factories.submission.build(collection=q1.form.collection)
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q1, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )

        def test_add_another_undetermined_when_referenced_question_unanswered_at_index(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            q3 = factories.question.build(form=group.form, parent=group)
            condition = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} == 'yes'"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=condition, depends_on_component=q2)
            ]
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("Test answer"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("Test answer"), add_another_index=1),
                ],
            )

            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context, add_another_index=0)
                == ComponentVisibilityState.UNDETERMINED
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context, add_another_index=1)
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_add_another_visible_when_referenced_question_answered_and_condition_passes(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            condition = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'yes'"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=condition, depends_on_component=q1)
            ]
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("yes"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("no"), add_another_index=1),
                ],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context, add_another_index=0)
                == ComponentVisibilityState.VISIBLE
            )
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context, add_another_index=1)
                == ComponentVisibilityState.HIDDEN
            )

        def test_add_another_hidden_when_referenced_question_is_hidden(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            q3 = factories.question.build(form=group.form, parent=group)
            factories.expression.build(question=q2, type_=ExpressionType.CONDITION, statement="False")
            condition = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} == 'yes'"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=condition, depends_on_component=q2)
            ]
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[FactoryAnswer(q1, TextSingleLineAnswer("Test answer"), add_another_index=0)],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context, add_another_index=0)
                == ComponentVisibilityState.HIDDEN
            )

        def test_add_another_different_visibility_per_index(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q_trigger = factories.question.build(form=group.form, parent=group)
            q_conditional = factories.question.build(form=group.form, parent=group)
            condition = factories.expression.build(
                question=q_conditional,
                type_=ExpressionType.CONDITION,
                statement=f"{q_trigger.safe_qid} == 'show'",
            )
            q_conditional.owned_component_references = [
                ComponentReference(component=q_conditional, expression=condition, depends_on_component=q_trigger)
            ]
            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q_trigger, TextSingleLineAnswer("show"), add_another_index=0),
                    FactoryAnswer(q_trigger, TextSingleLineAnswer("hide"), add_another_index=1),
                    FactoryAnswer(q1, TextSingleLineAnswer("anything"), add_another_index=2),
                ],
            )

            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(
                    q_conditional, helper.cached_evaluation_context, add_another_index=0
                )
                == ComponentVisibilityState.VISIBLE
            )
            assert (
                helper.get_component_visibility_state(
                    q_conditional, helper.cached_evaluation_context, add_another_index=1
                )
                == ComponentVisibilityState.HIDDEN
            )
            assert (
                helper.get_component_visibility_state(
                    q_conditional, helper.cached_evaluation_context, add_another_index=2
                )
                == ComponentVisibilityState.UNDETERMINED
            )

        def test_returns_hidden_when_any_reference_is_hidden_among_multiple(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0, data_type=QuestionDataType.YES_NO)
            q2 = factories.question.build(form=form, order=1)
            q3 = factories.question.build(form=form, order=2)
            q4 = factories.question.build(form=form, order=2)

            cond_q2 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} is True"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=cond_q2, depends_on_component=q1)
            ]

            factories.expression.build(question=q3, type_=ExpressionType.CONDITION, statement="False")

            q4.owned_component_references = [
                ComponentReference(component=q4, depends_on_component=q2),  # undetermined
                ComponentReference(component=q4, depends_on_component=q3),  # hidden
            ]
            submission = factories.submission.build(
                collection=form.collection,
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q1, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.UNDETERMINED
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )
            assert (
                helper.get_component_visibility_state(q4, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_add_another_chained_undetermined_visibility(self, factories):
            group = factories.group.build(add_another=True)
            q1 = factories.question.build(form=group.form, parent=group)
            q2 = factories.question.build(form=group.form, parent=group)
            q3 = factories.question.build(form=group.form, parent=group)

            cond_q2 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} == 'yes'"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=cond_q2, depends_on_component=q1)
            ]

            cond_q3 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} == 'go'"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=cond_q3, depends_on_component=q2)
            ]

            submission = factories.submission.build(
                collection=group.form.collection,
                answers=[
                    FactoryAnswer(q1, TextSingleLineAnswer("yes"), add_another_index=0),
                    FactoryAnswer(q1, TextSingleLineAnswer("no"), add_another_index=1),
                ],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context, add_another_index=0)
                == ComponentVisibilityState.UNDETERMINED
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context, add_another_index=1)
                == ComponentVisibilityState.HIDDEN
            )

        def test_referencing_hidden_question_in_condition_returns_hidden_even_with_existing_answers(self, factories):
            form = factories.form.build()
            q1 = factories.question.build(form=form, order=0, data_type=QuestionDataType.YES_NO)
            q2 = factories.question.build(form=form, order=1, data_type=QuestionDataType.YES_NO)
            cond_q2 = factories.expression.build(
                question=q2, type_=ExpressionType.CONDITION, statement=f"{q1.safe_qid} is True"
            )
            q2.owned_component_references = [
                ComponentReference(component=q2, expression=cond_q2, depends_on_component=q1)
            ]
            q3 = factories.question.build(form=form, order=2)
            cond_q3 = factories.expression.build(
                question=q3, type_=ExpressionType.CONDITION, statement=f"{q2.safe_qid} is True"
            )
            q3.owned_component_references = [
                ComponentReference(component=q3, expression=cond_q3, depends_on_component=q2)
            ]

            submission = factories.submission.build(
                collection=form.collection,
                answers=[
                    FactoryAnswer(q1, YesNoAnswer(False)),
                    FactoryAnswer(q2, YesNoAnswer(True)),
                    FactoryAnswer(q3, TextSingleLineAnswer("yes")),
                ],
            )
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(q1, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )
            assert (
                helper.get_component_visibility_state(q2, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )
            assert (
                helper.get_component_visibility_state(q3, helper.cached_evaluation_context)
                == ComponentVisibilityState.HIDDEN
            )

        def test_get_group_visibility_if_has_validation_referencing_child_question(self, factories):
            group = factories.group.build(
                presentation_options=QuestionPresentationOptions(show_questions_on_the_same_page=True)
            )
            question = factories.question.build(form=group.form, parent=group, data_type=QuestionDataType.NUMBER)

            validation = factories.expression.build(
                question=group, type_=ExpressionType.VALIDATION, statement=f"{question.safe_qid} < 1000"
            )
            group.owned_component_references = [
                ComponentReference(component=group, expression=validation, depends_on_component=question)
            ]

            submission = factories.submission.build(collection=group.form.collection)
            helper = SubmissionHelper(submission)

            assert (
                helper.get_component_visibility_state(group, helper.cached_evaluation_context)
                == ComponentVisibilityState.VISIBLE
            )

    class TestCanBeReopenedByUser:
        @pytest.mark.parametrize(
            "collection_status,grant_status,is_submitted,expected_result",
            [
                (CollectionStatusEnum.OPEN, GrantStatusEnum.LIVE, True, True),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.DRAFT, True, True),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.LIVE, False, False),
                (CollectionStatusEnum.CLOSED, GrantStatusEnum.LIVE, False, False),
                (CollectionStatusEnum.CLOSED, GrantStatusEnum.LIVE, True, False),
            ],
        )
        def test_when_in_test_mode(
            self, factories, collection_status, grant_status, is_submitted, expected_result, mocker
        ):
            submission = factories.submission.build(
                mode=SubmissionModeEnum.TEST,
                collection__status=collection_status,
                collection__grant__status=grant_status,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=is_submitted,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_reopened_by_user(user) == expected_result

        @pytest.mark.parametrize(
            "collection_status,grant_status,is_submitted, user_can_reopen,expected_result",
            [
                (CollectionStatusEnum.OPEN, GrantStatusEnum.LIVE, True, True, True),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.DRAFT, True, True, False),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.ONBOARDING, True, True, False),
                (CollectionStatusEnum.DRAFT, GrantStatusEnum.LIVE, True, True, False),
                (CollectionStatusEnum.SCHEDULED, GrantStatusEnum.LIVE, True, True, False),
                (CollectionStatusEnum.CLOSED, GrantStatusEnum.LIVE, True, True, False),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.LIVE, False, True, False),
                (CollectionStatusEnum.OPEN, GrantStatusEnum.LIVE, True, False, False),
            ],
        )
        def test_when_in_live_mode(
            self, factories, collection_status, grant_status, is_submitted, user_can_reopen, expected_result, mocker
        ):

            submission = factories.submission.build(
                mode=SubmissionModeEnum.LIVE,
                collection__status=collection_status,
                collection__grant__status=grant_status,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=is_submitted,
            )
            mocker.patch(
                "app.common.helpers.collections.AuthorisationHelper.has_deliver_grant_role",
                return_value=user_can_reopen,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_reopened_by_user(user) == expected_result

        @pytest.mark.parametrize("mode", [SubmissionModeEnum.TEST, SubmissionModeEnum.LIVE])
        def test_when_collection_does_not_allow_reopening(self, factories, mode, mocker):
            submission = factories.submission.build(
                mode=mode,
                collection__status=CollectionStatusEnum.OPEN,
                collection__grant__status=GrantStatusEnum.LIVE,
                collection__allow_submission_reopening=False,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=True,
            )
            mocker.patch(
                "app.common.helpers.collections.AuthorisationHelper.has_deliver_grant_role",
                return_value=True,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_reopened_by_user(user) is False

    class TestCanBeAssessedByUser:
        @pytest.mark.parametrize(
            "grant_status, is_submitted, expected_result",
            [
                (GrantStatusEnum.LIVE, True, True),
                (GrantStatusEnum.DRAFT, True, True),
                (GrantStatusEnum.LIVE, False, False),
            ],
        )
        def test_when_in_test_mode(self, factories, grant_status, is_submitted, expected_result, mocker):
            submission = factories.submission.build(
                mode=SubmissionModeEnum.TEST,
                collection__grant__status=grant_status,
                collection__allow_validation=True,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=is_submitted,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_assessed_by_user(user) == expected_result

        def test_when_in_test_mode_and_validation_not_allowed(self, factories, mocker):
            submission = factories.submission.build(
                mode=SubmissionModeEnum.TEST,
                collection__allow_validation=False,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=True,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_assessed_by_user(user) is False

        @pytest.mark.parametrize(
            "grant_status, is_submitted, user_can_assess, expected_result",
            [
                (GrantStatusEnum.LIVE, True, True, True),
                (GrantStatusEnum.DRAFT, True, True, False),
                (GrantStatusEnum.ONBOARDING, True, True, False),
                (GrantStatusEnum.LIVE, False, True, False),
                (GrantStatusEnum.LIVE, True, False, False),
            ],
        )
        def test_when_in_live_mode(
            self, factories, grant_status, is_submitted, user_can_assess, expected_result, mocker
        ):
            submission = factories.submission.build(
                mode=SubmissionModeEnum.LIVE,
                collection__allow_validation=True,
                collection__grant__status=grant_status,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=is_submitted,
            )
            mocker.patch(
                "app.common.helpers.collections.AuthorisationHelper.has_deliver_grant_role",
                return_value=user_can_assess,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_assessed_by_user(user) == expected_result

        def test_when_in_live_mode_and_validation_not_allowed(self, factories, mocker):
            submission = factories.submission.build(
                mode=SubmissionModeEnum.LIVE,
                collection__allow_validation=False,
                collection__grant__status=GrantStatusEnum.LIVE,
            )
            user = factories.user.build()
            mocker.patch(
                "app.common.helpers.collections.SubmissionHelper.is_submitted",
                new_callable=mocker.PropertyMock,
                return_value=True,
            )
            mocker.patch(
                "app.common.helpers.collections.AuthorisationHelper.has_deliver_grant_role",
                return_value=True,
            )
            helper = SubmissionHelper(submission)
            assert helper.can_be_assessed_by_user(user) is False


class TestDeserialiseQuestionType:
    def test_number_integer(self, factories):
        question = factories.question.build(
            data_type=QuestionDataType.NUMBER, data_options=QuestionDataOptions(number_type=NumberTypeEnum.INTEGER)
        )
        result = _deserialise_question_type(question, {"value": 42})
        assert isinstance(result, IntegerAnswer)
        assert result.value == 42

    def test_number_decimal(self, factories):
        question = factories.question.build(
            data_type=QuestionDataType.NUMBER, data_options=QuestionDataOptions(number_type=NumberTypeEnum.DECIMAL)
        )
        result = _deserialise_question_type(question, {"value": "13.3"})
        assert isinstance(result, DecimalAnswer)
        assert result.value == Decimal("13.3")

    def test_file_upload(self, factories):
        question = factories.question.build(data_type=QuestionDataType.FILE_UPLOAD)
        result = _deserialise_question_type(
            question, {"filename": "report.pdf", "size": 0, "mime_type": "application/pdf"}
        )
        assert isinstance(result, FileUploadAnswer)
        assert result.filename == "report.pdf"


class TestFormDataToQuestionType:
    def test_number_integer(self, factories):
        question = factories.question.build(
            id=uuid.UUID("e4bd98ab-41ef-4d23-b1e5-9c0404891e8c"),
            data_type=QuestionDataType.NUMBER,
            data_options=QuestionDataOptions(number_type=NumberTypeEnum.INTEGER),
        )

        _FormClass = build_question_form(
            [question], evaluation_context=ExpressionContext(), interpolation_context=ExpressionContext()
        )
        form = _FormClass(formdata=MultiDict({"q_e4bd98ab41ef4d23b1e59c0404891e8c": str(24)}))
        result = _form_data_to_question_type(question, form)
        assert isinstance(result, IntegerAnswer)
        assert result.value == 24

    def test_number_decimal(self, factories):
        question = factories.question.build(
            id=uuid.UUID("e4bd98ab-41ef-4d23-b1e5-9c0404891e8c"),
            data_type=QuestionDataType.NUMBER,
            data_options=QuestionDataOptions(number_type=NumberTypeEnum.DECIMAL),
        )

        _FormClass = build_question_form(
            [question], evaluation_context=ExpressionContext(), interpolation_context=ExpressionContext()
        )
        form = _FormClass(formdata=MultiDict({"q_e4bd98ab41ef4d23b1e59c0404891e8c": "45.23"}))
        result = _form_data_to_question_type(question, form)
        assert isinstance(result, DecimalAnswer)
        assert result.value == Decimal("45.23")
