from datetime import date, timedelta

import pytest
from psycopg.errors import ForeignKeyViolation
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError

from app import QuestionDataType
from app.common.collections.types import (
    TextSingleLineAnswer,
)
from app.common.data.models import ComponentReference, DataSource, Expression, Group, Submission
from app.common.data.types import (
    DataSourceType,
    ExpressionType,
    GrantRecipientModeEnum,
    QuestionPresentationOptions,
    RoleEnum,
    SubmissionEventType,
    SubmissionModeEnum,
    SubmissionStatusEnum,
)
from app.common.expressions.managed import GreaterThan, Specifically
from app.common.expressions.references import (
    EvaluationStatement,
    ExpressionReference,
    InterpolationStatement,
)
from tests.models import FactoryAnswer


class TestSubmissionModel:
    def test_test_submission_property_only_includes_test_submissions(self, factories):
        # what a test name
        collection = factories.collection.create()
        test_submission = factories.submission.create(collection=collection, mode=SubmissionModeEnum.TEST)
        live_submission = factories.submission.create(collection=collection, mode=SubmissionModeEnum.LIVE)

        assert collection.test_submissions == [test_submission]
        assert collection.live_submissions == [live_submission]

    def test_preview_submissions_can_be_created_without_grant_recipient(self, factories):
        collection = factories.collection.create()
        preview_submissions = factories.submission.create(collection=collection, mode=SubmissionModeEnum.PREVIEW)

        assert preview_submissions.grant_recipient_id is None
        assert preview_submissions.grant_recipient is None

    def test_live_submissions_cannot_be_created_without_grant_recipient(self, factories, db_session):
        from sqlalchemy.exc import IntegrityError

        from app.common.data.models import Submission

        collection = factories.collection.create()
        user = factories.user.create()

        submission = Submission(
            collection_id=collection.id,
            mode=SubmissionModeEnum.LIVE,
            reference="TEST-R123456",
            created_by_id=user.id,
            grant_recipient_id=None,
            status=SubmissionStatusEnum.NOT_STARTED,
        )
        db_session.add(submission)

        with pytest.raises(IntegrityError, match="ck_grant_recipient_if_live"):
            db_session.flush()

    def test_live_submissions_can_be_created_with_grant_recipient(self, factories):
        collection = factories.collection.create()
        grant_recipient = factories.grant_recipient.create()
        live_submission = factories.submission.create(
            collection=collection, mode=SubmissionModeEnum.LIVE, grant_recipient=grant_recipient
        )

        assert live_submission.grant_recipient_id == grant_recipient.id
        assert live_submission.grant_recipient == grant_recipient

    def test_grant_recipient_submissions_relationship(self, factories):
        collection = factories.collection.create()
        grant_recipient = factories.grant_recipient.create()
        live_submission_1 = factories.submission.create(
            collection=collection, mode=SubmissionModeEnum.LIVE, grant_recipient=grant_recipient
        )
        live_submission_2 = factories.submission.create(
            collection=collection, mode=SubmissionModeEnum.LIVE, grant_recipient=grant_recipient
        )
        test_submission = factories.submission.create(collection=collection, mode=SubmissionModeEnum.TEST)

        assert set(grant_recipient.submissions) == {live_submission_1, live_submission_2}
        assert test_submission not in grant_recipient.submissions

    def test_submission_is_not_overdue_when_no_deadline(self, factories):
        submission = factories.submission.build(collection__submission_period_end_date=None)
        assert submission.is_overdue is False

    @pytest.mark.freeze_time("2025-01-10 12:00:00")
    def test_submission_is_overdue_when_past_deadline(self, factories):
        submission = factories.submission.build(collection__submission_period_end_date=date(2020, 1, 1))
        assert submission.is_overdue is True

    @pytest.mark.freeze_time("2025-01-10 12:00:00")
    def test_is_overdue_when_before_deadline(self, factories):
        submission = factories.submission.build(collection__submission_period_end_date=date(2030, 1, 1))
        assert submission.is_overdue is False

    @pytest.mark.freeze_time("2026-07-17 12:00:00")
    @pytest.mark.parametrize(
        "offset_days, expected_overdue",
        [
            pytest.param(-1, True, id="day-after-end-date-is-overdue"),
            pytest.param(0, False, id="end-date-itself-is-not-overdue"),
            pytest.param(1, False, id="before-end-date-is-not-overdue"),
        ],
    )
    def test_is_overdue_is_inclusive_of_end_date(self, factories, offset_days, expected_overdue):
        end_date = date(2026, 7, 17) + timedelta(days=offset_days)
        submission = factories.submission.build(collection__submission_period_end_date=end_date)
        assert submission.collection.is_overdue is expected_overdue
        assert submission.is_overdue is expected_overdue

    @pytest.mark.freeze_time("2026-07-17 23:30:00")
    def test_is_overdue_uses_europe_london_not_process_timezone(self, factories):
        submission = factories.submission.build(collection__submission_period_end_date=date(2026, 7, 17))
        assert submission.collection.is_overdue is True

    @pytest.mark.freeze_time("2025-01-10 12:00:00")
    @pytest.mark.parametrize("status", [SubmissionStatusEnum.SUBMITTED, SubmissionStatusEnum.SUBMITTED_WITH_CHANGES])
    def test_submission_is_not_overdue_when_completed(self, factories, status):
        collection = factories.collection.build(
            submission_period_end_date=date(2020, 1, 1),
            requires_certification=False,
        )
        question = factories.question.build(form__collection=collection)
        submission = factories.submission.build(
            collection=collection,
            answers=[FactoryAnswer(question, TextSingleLineAnswer("test answer"))],
        )
        submission.events = [
            factories.submission_event.build(
                submission=submission,
                event_type=SubmissionEventType.FORM_RUNNER_FORM_COMPLETED,
                related_entity_id=question.form.id,
            ),
            factories.submission_event.build(
                submission=submission,
                event_type=SubmissionEventType.SUBMISSION_SUBMITTED,
                related_entity_id=submission.id,
            ),
        ]
        submission.status = status

        assert submission.is_overdue is False

    @pytest.mark.parametrize(
        "status, expected_locked",
        [
            (SubmissionStatusEnum.NOT_STARTED, False),
            (SubmissionStatusEnum.IN_PROGRESS, False),
            (SubmissionStatusEnum.READY_TO_SUBMIT, False),
            (SubmissionStatusEnum.AWAITING_SIGN_OFF, True),
            (SubmissionStatusEnum.SUBMITTED, True),
            (SubmissionStatusEnum.NOT_SUBMITTED, False),
            (SubmissionStatusEnum.PARTIALLY_SUBMITTED, False),
            (SubmissionStatusEnum.CHANGES_REQUESTED, False),
            (SubmissionStatusEnum.SUBMITTED_WITH_CHANGES, True),
        ],
    )
    def test_submission_has_answers_locked_status(self, factories, status, expected_locked):
        submission = factories.submission.build(status=status)

        assert submission.has_answers_locked_status is expected_locked

    def test_submission_has_answers_locked_status_can_be_queried(self, factories, db_session):
        locked_submission = factories.submission.create()
        locked_submission.status = SubmissionStatusEnum.AWAITING_SIGN_OFF
        unlocked_submission = factories.submission.create()
        unlocked_submission.status = SubmissionStatusEnum.IN_PROGRESS
        db_session.commit()

        result = db_session.scalars(select(Submission).where(Submission.has_answers_locked_status)).all()

        assert locked_submission in result
        assert unlocked_submission not in result


class TestGrantModel:
    def test_grant_recipients_relationship(self, factories):
        grant = factories.grant.create()
        grant_recipient_1 = factories.grant_recipient.create(grant=grant)
        grant_recipient_2 = factories.grant_recipient.create(grant=grant)
        other_grant = factories.grant.create()
        other_grant_recipient = factories.grant_recipient.create(grant=other_grant)

        assert set(grant.grant_recipients) == {grant_recipient_1, grant_recipient_2}
        assert other_grant_recipient not in grant.grant_recipients


class TestComponentModel:
    def test_interpolation_fields_round_trip_as_interpolation_statements(self, db_session, factories):
        question = factories.question.create(
            text="What is your name?",
            hint="Your hint here",
            guidance_body="Some guidance body text",
        )
        db_session.expire_all()
        reloaded = db_session.get(type(question), question.id)
        assert isinstance(reloaded.text, InterpolationStatement)
        assert isinstance(reloaded.hint, InterpolationStatement)
        assert isinstance(reloaded.guidance_body, InterpolationStatement)

    def test_expression_statement_round_trips_as_evaluation_statement(self, db_session, factories):
        expression = factories.expression.create(statement="q_abc > 1", type_=ExpressionType.CONDITION)
        db_session.expire_all()
        reloaded = db_session.get(Expression, expression.id)
        assert isinstance(reloaded.statement, EvaluationStatement)
        assert reloaded.statement == "q_abc > 1"

    def test_is_descendant_of(self, factories):
        parent_group = factories.group.create()
        child_group = factories.group.create(parent=parent_group)
        child_question = factories.question.create(parent=child_group)
        top_level_question = factories.question.create(form=parent_group.form)
        other_form_question = factories.question.create()

        assert child_group.is_descendant_of(parent_group)
        assert child_question.is_descendant_of(parent_group)
        assert child_question.is_descendant_of(child_group)

        assert not parent_group.is_descendant_of(child_group)
        assert not child_question.is_descendant_of(top_level_question)
        assert not child_question.is_descendant_of(other_form_question)
        assert not other_form_question.is_descendant_of(parent_group)
        assert not other_form_question.is_descendant_of(child_group)
        assert not other_form_question.is_descendant_of(child_question)


class TestQuestionModel:
    def test_question_property_selects_expressions(self, factories):
        question = factories.question.create()
        condition_expression = factories.expression.create(
            question=question, type_=ExpressionType.CONDITION, statement=""
        )
        validation_expression = factories.expression.create(
            question=question, type_=ExpressionType.VALIDATION, statement=""
        )
        assert question.conditions == [condition_expression]
        assert question.validations == [validation_expression]

    def test_question_gets_a_valid_expression_that_belongs_to_it(self, factories):
        question = factories.question.create()
        expression = factories.expression.create(question=question, type_=ExpressionType.CONDITION, statement="")
        assert question.get_expression(expression.id) == expression

    def test_question_does_not_get_a_valid_expression_that_does_not_belong_to_it(self, factories):
        question = factories.question.create()
        expression_on_other_question = factories.expression.create(type_=ExpressionType.CONDITION, statement="")

        with pytest.raises(ValueError) as e:
            question.get_expression(expression_on_other_question.id)

        assert (
            str(e.value)
            == f"Could not find an expression with id={expression_on_other_question.id} in question={question.id}"
        )

    def test_data_source_items(self, factories):
        factories.data_source_item.reset_sequence()
        question = factories.question.create(
            data_type=QuestionDataType.RADIOS,
            presentation_options=QuestionPresentationOptions(last_data_source_item_is_distinct_from_others=False),
        )
        other_question = factories.question.create(data_type=QuestionDataType.TEXT_MULTI_LINE)

        assert question.data_source_items == "Option 0\nOption 1\nOption 2"
        assert other_question.data_source_items is None

        assert question.separate_option_if_no_items_match is False
        assert other_question.separate_option_if_no_items_match is None
        assert question.none_of_the_above_item_text == "Other"
        assert other_question.none_of_the_above_item_text is None

    def test_data_source_items_last_item_is_distinct(self, factories):
        factories.data_source_item.reset_sequence()
        question = factories.question.create(
            data_type=QuestionDataType.RADIOS,
            presentation_options=QuestionPresentationOptions(last_data_source_item_is_distinct_from_others=True),
        )
        assert question.data_source_items == "Option 0\nOption 1"
        assert question.separate_option_if_no_items_match is True
        assert question.none_of_the_above_item_text == "Option 2"


class TestFormModel:
    def test_questions_property_filters_nested_questions(self, factories):
        form = factories.form.create()
        # asserting to a depth of 2
        question1 = factories.question.create(form=form, order=0)
        question2 = factories.question.create(form=form, order=1)
        group = factories.group.create(form=form, order=2)
        question3 = factories.question.create(form_id=form.id, parent=group, order=0)
        sub_group = factories.group.create(form_id=form.id, parent=group, order=1)
        question4 = factories.question.create(form_id=form.id, parent=sub_group, order=0)

        assert form.cached_questions == [question1, question2, question3, question4]


class TestGroupModel:
    def test_questions_property_filters_nested_questions(self, factories):
        form = factories.form.create()
        _question1 = factories.question.create(form=form, order=0)
        group = factories.group.create(form_id=form.id, order=1)
        question2 = factories.question.create(form_id=group.form_id, parent=group, order=0)
        question3 = factories.question.create(form_id=group.form_id, parent=group, order=1)
        sub_group = factories.group.create(form_id=group.form_id, parent=group, order=2)
        question4 = factories.question.create(form_id=group.form_id, parent=sub_group, order=0)

        assert group.cached_questions == [question2, question3, question4]
        assert sub_group.cached_questions == [question4]

    @pytest.mark.parametrize("show_questions_on_the_same_page", [True, False])
    def test_same_page_property(self, factories, show_questions_on_the_same_page):
        form = factories.form.create()
        group = factories.group.create(
            form_id=form.id,
            presentation_options=QuestionPresentationOptions(
                show_questions_on_the_same_page=show_questions_on_the_same_page
            ),
        )

        assert group.same_page is show_questions_on_the_same_page

    def test_max_levels_of_nesting_not_changed(self, app):
        assert app.config["MAX_NESTED_GROUP_LEVELS"] == 1, (
            "If changing the max level of nested groups, ensure you add tests to that level of nesting"
        )

    def test_count_nested_group_levels(self, factories):
        top_group = factories.group.create()
        middle_group = factories.group.create(parent=top_group)
        bottom_group = factories.group.create(parent=middle_group)

        assert Group._count_nested_group_levels(group=top_group) == 0
        assert Group._count_nested_group_levels(group=middle_group) == 1
        assert Group._count_nested_group_levels(group=bottom_group) == 2

    def test_contains_add_another_components(self, factories):
        g1 = factories.group.create()
        g2 = factories.group.create()
        g3 = factories.group.create()
        g4 = factories.group.create(parent=g3, add_another=True)
        factories.question.create(parent=g1, add_another=True)
        assert g1.contains_add_another_components is True
        assert g2.contains_add_another_components is False
        assert g3.contains_add_another_components is True
        assert g4.contains_add_another_components is False

    def test_add_another_summary_questions_none_selected(self, factories):
        top_group = factories.group.create(add_another=True)
        q1 = factories.question.create(parent=top_group)
        sub_group = factories.group.create(parent=top_group)
        q2 = factories.question.create(parent=sub_group)
        factories.question.create(form=q1.form, order=0)

        result = top_group.questions_in_add_another_summary
        assert result == [q1, q2]

    def test_add_another_summary_questions_some_selected(self, factories):
        top_group = factories.group.create(add_another=True)
        q1 = factories.question.create(parent=top_group)
        sub_group = factories.group.create(parent=top_group)
        q2 = factories.question.create(parent=sub_group)
        _ = factories.question.create(parent=sub_group)
        q4 = factories.question.create(parent=top_group)
        factories.question.create(form=q1.form, order=0)
        top_group.presentation_options = QuestionPresentationOptions(
            add_another_summary_line_question_ids=[q1.id, q2.id, q4.id]
        )

        result = top_group.questions_in_add_another_summary
        assert result == [q1, q2, q4]

    def test_add_another_summary_questions_group_is_not_add_another(self, factories):
        top_group = factories.group.create(add_another=False)
        q1 = factories.question.create(parent=top_group)
        sub_group = factories.group.create(parent=top_group)
        q2 = factories.question.create(parent=sub_group)
        _ = factories.question.create(parent=sub_group)
        q4 = factories.question.create(parent=top_group)
        factories.question.create(form=q1.form, order=0)
        top_group.presentation_options = QuestionPresentationOptions(
            add_another_summary_line_question_ids=[q1.id, q2.id, q4.id]
        )

        result = top_group.questions_in_add_another_summary
        assert result == []


class TestGrantRecipientModel:
    def test_certifiers_returns_empty_list_when_no_certifiers(self, factories):
        grant_recipient = factories.grant_recipient.create()

        assert grant_recipient.certifiers == []

    def test_certifiers_returns_global_certifiers_only(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()
        factories.user_role.create(
            user=user, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.CERTIFIER]
        )

        assert len(grant_recipient.certifiers) == 1
        assert grant_recipient.certifiers[0].id == user.id

    def test_certifiers_returns_grant_specific_certifiers_only(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()
        factories.user_role.create(
            user=user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.CERTIFIER],
        )

        assert len(grant_recipient.certifiers) == 1
        assert grant_recipient.certifiers[0].id == user.id

    def test_certifiers_prefers_grant_specific_over_global(self, factories):
        grant_recipient = factories.grant_recipient.create()
        global_certifier = factories.user.create()
        grant_specific_certifier = factories.user.create()

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

        assert len(grant_recipient.certifiers) == 1
        assert grant_recipient.certifiers[0].id == grant_specific_certifier.id

    def test_certifiers_returns_all_global_certifiers_when_no_grant_specific(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user1 = factories.user.create()
        user2 = factories.user.create()
        user3 = factories.user.create()

        factories.user_role.create(
            user=user1, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.CERTIFIER]
        )
        factories.user_role.create(
            user=user2, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.CERTIFIER]
        )
        factories.user_role.create(
            user=user3, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.CERTIFIER]
        )

        assert len(grant_recipient.certifiers) == 3
        assert {u.id for u in (grant_recipient.certifiers)} == {user1.id, user2.id, user3.id}

    def test_certifiers_returns_all_grant_specific_certifiers(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user1 = factories.user.create()
        user2 = factories.user.create()

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

        assert len(grant_recipient.certifiers) == 2
        assert {u.id for u in (grant_recipient.certifiers)} == {user1.id, user2.id}

    def test_certifiers_excludes_certifiers_from_different_organisation(self, factories):
        grant_recipient = factories.grant_recipient.create()
        other_org = factories.organisation.create()
        other_org_certifier = factories.user.create()

        factories.user_role.create(
            user=other_org_certifier, organisation=other_org, grant=None, permissions=[RoleEnum.CERTIFIER]
        )

        assert len(grant_recipient.certifiers) == 0

    def test_certifiers_excludes_certifiers_from_different_grant(self, factories):
        grant_recipient = factories.grant_recipient.create()
        other_grant = factories.grant.create()
        other_grant_certifier = factories.user.create()

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

        assert len(grant_recipient.certifiers) == 0

    def test_certifiers_excludes_non_certifier_roles(self, factories):
        grant_recipient = factories.grant_recipient.create()
        member_user = factories.user.create()
        admin_user = factories.user.create()

        factories.user_role.create(
            user=member_user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.MEMBER],
        )
        factories.user_role.create(
            user=admin_user, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.ADMIN]
        )

        assert len(grant_recipient.certifiers) == 0

    def test_certifiers_with_multiple_permissions_including_certifier(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()

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

        assert len(grant_recipient.certifiers) == 1
        assert grant_recipient.certifiers[0].id == user.id

    def test_certifiers_user_with_both_global_and_grant_specific_roles(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()

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

        assert len(grant_recipient.certifiers) == 1
        assert grant_recipient.certifiers[0].id == user.id

    def test_data_providers_returns_empty_list_when_no_data_providers(self, factories):
        grant_recipient = factories.grant_recipient.create()

        assert grant_recipient.data_providers == []

    def test_data_providers_returns_global_data_providers_only(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()
        factories.user_role.create(
            user=user, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )

        assert len(grant_recipient.data_providers) == 1
        assert grant_recipient.data_providers[0].id == user.id

    def test_data_providers_returns_grant_specific_data_providers_only(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()
        factories.user_role.create(
            user=user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 1
        assert grant_recipient.data_providers[0].id == user.id

    def test_data_providers_returns_both_global_and_grant_specific(self, factories):
        grant_recipient = factories.grant_recipient.create()
        global_data_provider = factories.user.create()
        grant_specific_data_provider = factories.user.create()

        factories.user_role.create(
            user=global_data_provider,
            organisation=grant_recipient.organisation,
            grant=None,
            permissions=[RoleEnum.DATA_PROVIDER],
        )
        factories.user_role.create(
            user=grant_specific_data_provider,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 2
        assert {u.id for u in (grant_recipient.data_providers)} == {
            global_data_provider.id,
            grant_specific_data_provider.id,
        }

    def test_data_providers_returns_all_global_data_providers(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user1 = factories.user.create()
        user2 = factories.user.create()
        user3 = factories.user.create()

        factories.user_role.create(
            user=user1, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )
        factories.user_role.create(
            user=user2, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )
        factories.user_role.create(
            user=user3, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )

        assert len(grant_recipient.data_providers) == 3
        assert {u.id for u in (grant_recipient.data_providers)} == {user1.id, user2.id, user3.id}

    def test_data_providers_returns_all_grant_specific_data_providers(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user1 = factories.user.create()
        user2 = factories.user.create()

        factories.user_role.create(
            user=user1,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )
        factories.user_role.create(
            user=user2,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 2
        assert {u.id for u in (grant_recipient.data_providers)} == {user1.id, user2.id}

    def test_data_providers_excludes_data_providers_from_different_organisation(self, factories):
        grant_recipient = factories.grant_recipient.create()
        other_org = factories.organisation.create()
        other_org_data_provider = factories.user.create()

        factories.user_role.create(
            user=other_org_data_provider, organisation=other_org, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )

        assert len(grant_recipient.data_providers) == 0

    def test_data_providers_excludes_data_providers_from_different_grant(self, factories):
        grant_recipient = factories.grant_recipient.create()
        other_grant = factories.grant.create()
        other_grant_data_provider = factories.user.create()

        factories.user_role.create(
            user=other_grant_data_provider,
            organisation=grant_recipient.organisation,
            grant=other_grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 0

    def test_data_providers_excludes_non_data_provider_roles(self, factories):
        grant_recipient = factories.grant_recipient.create()
        member_user = factories.user.create()
        admin_user = factories.user.create()

        factories.user_role.create(
            user=member_user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.MEMBER],
        )
        factories.user_role.create(
            user=admin_user, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.CERTIFIER]
        )

        assert len(grant_recipient.data_providers) == 0

    def test_data_providers_with_multiple_permissions_including_data_provider(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()

        factories.user_role.create(
            user=user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.MEMBER, RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 1
        assert grant_recipient.data_providers[0].id == user.id

    def test_data_providers_user_with_both_global_and_grant_specific_roles(self, factories):
        grant_recipient = factories.grant_recipient.create()
        user = factories.user.create()

        factories.user_role.create(
            user=user, organisation=grant_recipient.organisation, grant=None, permissions=[RoleEnum.DATA_PROVIDER]
        )
        factories.user_role.create(
            user=user,
            organisation=grant_recipient.organisation,
            grant=grant_recipient.grant,
            permissions=[RoleEnum.DATA_PROVIDER],
        )

        assert len(grant_recipient.data_providers) == 1
        assert grant_recipient.data_providers[0].id == user.id

    @pytest.mark.parametrize(
        "certifier_names, expected",
        (
            ([], "Your certifier"),
            (["Certifier One"], "Certifier One"),
            (["Certifier One", "Certifier Two"], "Certifier One or Certifier Two"),
            (["Certifier One", "Certifier Two", "Certifier Three"], "Certifier One, Certifier Two or Certifier Three"),
        ),
    )
    def test_certifier_names_property(self, factories, certifier_names: list[str], expected: str):
        grant_recipient = factories.grant_recipient.create()
        for name in certifier_names:
            user = factories.user.create(name=name)
            factories.user_role.create(
                user=user,
                organisation=grant_recipient.organisation,
                grant=grant_recipient.grant,
                permissions=[RoleEnum.CERTIFIER],
            )

        assert grant_recipient.certifier_names == expected

    def test_grant_recipient_submission_mode(self, factories):
        live_grant_recipient = factories.grant_recipient.create(mode=GrantRecipientModeEnum.LIVE)
        test_grant_recipient = factories.grant_recipient.create(mode=GrantRecipientModeEnum.TEST)

        assert len(GrantRecipientModeEnum) == 2
        assert len(SubmissionModeEnum) == 3
        assert live_grant_recipient.submission_mode == SubmissionModeEnum.LIVE
        assert test_grant_recipient.submission_mode == SubmissionModeEnum.TEST


class TestComponentReferenceModel:
    def test_deleting_a_component_with_a_reference_is_blocked(self, factories, db_session):
        q1 = factories.question.create()
        factories.question.create(form=q1.form, text=f"Reference to (({q1.safe_qid}))")

        with pytest.raises(IntegrityError) as e:
            db_session.delete(q1)
            db_session.commit()

        assert isinstance(e.value.__cause__, ForeignKeyViolation)
        assert 'update or delete on table "component" violates foreign key constraint' in str(e.value.__cause__)

    def test_deleting_a_component_holding_a_reference_is_allowed(self, factories, db_session):
        q1 = factories.question.create()
        q2 = factories.question.create(form=q1.form, text=f"Reference to (({q1.safe_qid}))")

        db_session.delete(q2)
        db_session.commit()

        assert db_session.query(ComponentReference).count() == 0

    def test_deleting_a_component_with_an_expression_reference_is_blocked(self, factories, db_session):
        user = factories.user.create()
        q1 = factories.question.create()
        factories.question.create(
            form=q1.form,
            expressions=[
                Expression.from_evaluatable_expression(
                    GreaterThan(subject_reference=ExpressionReference.from_question(q1), minimum_value=3000),
                    ExpressionType.CONDITION,
                    user,
                )
            ],
        )

        with pytest.raises(IntegrityError) as e:
            db_session.delete(q1)
            db_session.commit()

        assert isinstance(e.value.__cause__, ForeignKeyViolation)
        assert 'update or delete on table "component" violates foreign key constraint' in str(e.value.__cause__)

    def test_deleting_an_expression_holding_a_reference_is_allowed(self, factories, db_session):
        user = factories.user.create()
        q1 = factories.question.create()
        q2 = factories.question.create(
            form=q1.form,
            expressions=[
                Expression.from_evaluatable_expression(
                    GreaterThan(subject_reference=ExpressionReference.from_question(q1), minimum_value=3000),
                    ExpressionType.CONDITION,
                    user,
                )
            ],
        )

        db_session.delete(q2.expressions[0])
        db_session.commit()

        assert db_session.query(ComponentReference).count() == 0

    def test_deleting_a_data_source_item_with_an_expression_reference_is_blocked(self, factories, db_session):
        user = factories.user.create()
        q1 = factories.question.create(data_type=QuestionDataType.RADIOS)
        factories.question.create(
            form=q1.form,
            expressions=[
                Expression.from_evaluatable_expression(
                    Specifically(
                        subject_reference=ExpressionReference.from_question(q1),
                        item={
                            "key": q1.data_source.items[0].key,
                            "label": q1.data_source.items[0].label,
                        },
                    ),
                    ExpressionType.CONDITION,
                    user,
                ),
            ],
        )

        with pytest.raises(IntegrityError) as e:
            db_session.delete(q1.data_source.items[0])
            db_session.commit()

        assert isinstance(e.value.__cause__, ForeignKeyViolation)
        assert 'update or delete on table "data_source_item" violates foreign key constraint' in str(e.value.__cause__)

    def test_deleting_an_expression_holding_a_data_source_item_reference_is_allowed(self, factories, db_session):
        user = factories.user.create()
        q1 = factories.question.create(data_type=QuestionDataType.RADIOS)
        q2 = factories.question.create(
            form=q1.form,
            expressions=[
                Expression.from_evaluatable_expression(
                    Specifically(
                        subject_reference=ExpressionReference.from_question(q1),
                        item={
                            "key": q1.data_source.items[0].key,
                            "label": q1.data_source.items[0].label,
                        },
                    ),
                    ExpressionType.CONDITION,
                    user,
                ),
            ],
        )

        db_session.delete(q2.expressions[0])
        db_session.commit()

        assert db_session.query(ComponentReference).count() == 0

    def test_column_reference_round_trips(self, factories, db_session):
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant, collection=collection, type=DataSourceType.GRANT_RECIPIENT
        )
        question = factories.question.create(form__collection=collection)

        cr = ComponentReference(
            component=question,
            depends_on_data_source=data_source,
            depends_on_column_name="c_allocation",
        )
        db_session.add(cr)
        db_session.commit()

        loaded = db_session.get(ComponentReference, cr.id)
        assert loaded is not None
        assert loaded.depends_on_component_id is None
        assert loaded.depends_on_data_source == data_source
        assert loaded.depends_on_column_name == "c_allocation"
        assert data_source.depended_on_by_columns == [loaded]

    def test_reference_with_no_target_is_rejected(self, factories, db_session):
        question = factories.question.create()

        with pytest.raises(IntegrityError):
            db_session.add(ComponentReference(component=question))
            db_session.commit()

    def test_reference_with_both_component_and_data_source_is_rejected(self, factories, db_session):
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=collection,
            type=DataSourceType.GRANT_RECIPIENT,
        )
        q1 = factories.question.create(form__collection=collection)
        q2 = factories.question.create(form=q1.form)

        with pytest.raises(IntegrityError):
            db_session.add(
                ComponentReference(
                    component=q2,
                    depends_on_component=q1,
                    depends_on_data_source=data_source,
                    depends_on_column_name="c_allocation",
                )
            )
            db_session.commit()

    def test_data_source_reference_without_column_name_is_rejected(self, factories, db_session):
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=collection,
            type=DataSourceType.GRANT_RECIPIENT,
        )
        question = factories.question.create(form__collection=collection)

        with pytest.raises(IntegrityError):
            db_session.add(ComponentReference(component=question, depends_on_data_source=data_source))
            db_session.commit()

    def test_column_name_without_data_source_reference_is_rejected(self, factories, db_session):
        question = factories.question.create()

        with pytest.raises(IntegrityError):
            db_session.add(ComponentReference(component=question, depends_on_column_name="c_allocation"))
            db_session.commit()

    def test_deleting_a_data_source_with_a_column_reference_is_blocked(self, factories, db_session):
        grant = factories.grant.create()
        collection = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=collection,
            type=DataSourceType.GRANT_RECIPIENT,
        )
        question = factories.question.create(form__collection=collection)
        db_session.add(
            ComponentReference(
                component=question,
                depends_on_data_source=data_source,
                depends_on_column_name="c_allocation",
            )
        )
        db_session.commit()

        with pytest.raises(IntegrityError) as e:
            db_session.delete(data_source)
            db_session.commit()

        assert isinstance(e.value.__cause__, ForeignKeyViolation)


class TestDataSourceModel:
    def test_filtering_organisation_items(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        gr1, gr2, gr3 = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )
        gr4 = factories.grant_recipient.create(grant=grant)

        assert data_source.get_filtered_organisation_item(gr1.organisation.external_id)._data["c_allocation"] == 111
        assert data_source.get_filtered_organisation_item(gr2.organisation.external_id)._data["c_allocation"] == 222
        assert data_source.get_filtered_organisation_item(gr3.organisation.external_id)._data["c_allocation"] == 333
        assert data_source.get_filtered_organisation_item(gr4.organisation.external_id) is None

    def test_has_missing_data_python_true_when_null_values(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, None],
        )

        assert data_source.has_missing_data(grant_recipients) is True

    def test_has_missing_data_python_true_when_empty_data(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
        )
        factories.data_source_organisation_item.create(
            data_source=data_source,
            external_id=grant_recipient.organisation.external_id,
            _data={},
        )

        assert data_source.has_missing_data([grant_recipient]) is True

    def test_has_missing_data_python_false_when_all_values_present(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        assert data_source.has_missing_data(grant_recipients) is False

    def test_has_missing_data_python_true_when_no_org_items_but_grant_recipients(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        assert data_source.has_missing_data(grant_recipients) is True

    def test_has_missing_data_python_true_when_no_org_items_and_no_grant_recipients(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        assert data_source.has_missing_data([]) is False

    def test_has_missing_data_python_true_when_grant_recipient_added_since_upload(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
        )
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)

        assert data_source.has_missing_data(grant_recipients) is True

    def test_has_missing_data_sql_true_when_null_values(self, factories, db_session, track_sql_queries):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )
        data_source_2 = factories.data_source.create(
            name="Missing data set",
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, None],
        )

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source_2 in results
        assert data_source not in results

    def test_has_missing_data_sql_true_when_empty_data(self, factories, db_session, track_sql_queries):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )
        data_source_2 = factories.data_source.create(
            name="Missing data set",
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )
        data_source_2.organisation_items[0]._data = {}
        db_session.flush()

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source_2 in results
        assert data_source not in results

    def test_has_missing_data_sql_true_when_grant_recipients_and_no_org_items(self, factories, db_session):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source in results

    def test_has_missing_data_sql_false_when_no_org_items_and_no_grant_recipients(self, factories, db_session):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        assert data_source.has_missing_data([]) is False

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source not in results

    def test_has_missing_data_sql_true_when_grant_recipient_added_since_upload(
        self, factories, db_session, track_sql_queries
    ):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )
        factories.grant_recipient.create(grant=grant)

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source in results

    def test_has_missing_data_sql_false_for_custom_type(self, factories, db_session):
        data_source = factories.data_source.create(type=DataSourceType.CUSTOM)

        results = db_session.scalars(select(DataSource).where(DataSource.has_missing_data())).all()
        assert data_source not in results

    def test_get_missing_data_organisations_returns_empty_for_custom_type(self, factories):
        grant = factories.grant.create()
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(type=DataSourceType.CUSTOM)

        assert data_source.get_missing_data_organisations(grant_recipients) == []

    def test_get_missing_data_organisations_returns_empty_when_no_grant_recipients_passed(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        assert data_source.get_missing_data_organisations([]) == []

    def test_get_missing_data_organisations_returns_empty_when_all_values_present(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        assert data_source.get_missing_data_organisations(grant_recipients) == []

    def test_get_missing_data_organisations_returns_organisations_with_null_column_values(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, None],
        )

        result = data_source.get_missing_data_organisations(grant_recipients)

        expected = [
            gr.organisation
            for gr in grant_recipients
            if data_source.get_filtered_organisation_item(gr.organisation.external_id)._data["c_allocation"] is None
        ]
        assert len(expected) == 1
        assert result == expected

    def test_get_missing_data_organisations_returns_organisations_missing_entirely(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        existing_grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
        )
        new_grant_recipients = factories.grant_recipient.create_batch(2, grant=grant)

        result = data_source.get_missing_data_organisations([existing_grant_recipient, *new_grant_recipients])

        assert set(result) == {gr.organisation for gr in new_grant_recipients}

    def test_get_missing_data_organisations_checks_all_schema_columns(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        gr1, gr2 = factories.grant_recipient.create_batch(2, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            has_column_of_each_type=True,
        )
        complete_data = {
            "c_british_pounds": 1.23,
            "c_decimal_number": 4.567,
            "c_just_text": "some text",
            "c_whole_number": 1,
            "c_whole_number_prefix": 2,
            "c_whole_number_suffix": 3,
        }
        factories.data_source_organisation_item.create(
            data_source=data_source,
            external_id=gr1.organisation.external_id,
            _data={**complete_data, "c_just_text": None},
        )
        factories.data_source_organisation_item.create(
            data_source=data_source,
            external_id=gr2.organisation.external_id,
            _data=complete_data,
        )

        result = data_source.get_missing_data_organisations([gr1, gr2])

        assert result == [gr1.organisation]

    def test_get_removed_organisation_external_ids_returns_empty_when_no_organisations_removed(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        assert data_source.get_removed_organisation_external_ids(grant_recipients) == []

    def test_get_removed_organisation_external_ids_returns_empty_when_no_org_items(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipients = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        assert data_source.get_removed_organisation_external_ids(grant_recipients) == []

    def test_get_removed_organisation_external_ids_returns_ids_for_organisations_no_longer_recipients(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        gr1, gr2, gr3 = factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        result = data_source.get_removed_organisation_external_ids([gr1, gr2])

        assert result == [gr3.organisation.external_id]

    def test_get_removed_organisation_external_ids_returns_all_when_no_grant_recipients_passed(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        factories.grant_recipient.create_batch(3, grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[111, 222, 333],
        )

        result = data_source.get_removed_organisation_external_ids([])

        assert set(result) == {item.external_id for item in data_source.organisation_items}
        assert len(result) == 3

    def test_get_referenced_column_names_returns_empty_when_no_references(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)

        assert data_source.get_referenced_column_names() == set()

    def test_get_referenced_column_names_returns_referenced_columns(self, factories, db_session):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)
        factories.question.create(
            form__collection=report,
            hint=InterpolationStatement(
                "You were allocated " + ExpressionReference.from_data_source_column(data_source, "c_allocation").wrapped
            ),
        )

        assert data_source.get_referenced_column_names() == {"c_allocation"}

    def test_has_missing_referenced_data_for_organisation_false_when_no_columns_referenced(self, factories):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[None],
        )

        assert (
            data_source.has_missing_referenced_data_for_organisation(grant_recipient.organisation.external_id) is False
        )

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

        assert (
            data_source.has_missing_referenced_data_for_organisation(grant_recipient.organisation.external_id) is True
        )

    def test_has_missing_referenced_data_for_organisation_true_when_no_organisation_item(self, factories, db_session):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(grant=grant, collection=report, type=DataSourceType.GRANT_RECIPIENT)
        factories.question.create(
            form__collection=report,
            hint=InterpolationStatement(
                "You were allocated " + ExpressionReference.from_data_source_column(data_source, "c_allocation").wrapped
            ),
        )

        assert (
            data_source.has_missing_referenced_data_for_organisation(grant_recipient.organisation.external_id) is True
        )

    def test_has_missing_referenced_data_for_organisation_false_when_referenced_column_present(
        self, factories, db_session
    ):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            create_gr_org_items=True,
            create_gr_org_items__data=[123],
        )
        factories.question.create(
            form__collection=report,
            hint=InterpolationStatement(
                "You were allocated " + ExpressionReference.from_data_source_column(data_source, "c_allocation").wrapped
            ),
        )

        assert (
            data_source.has_missing_referenced_data_for_organisation(grant_recipient.organisation.external_id) is False
        )

    def test_has_missing_referenced_data_for_organisation_ignores_unreferenced_null_columns(
        self, factories, db_session
    ):
        grant = factories.grant.create()
        report = factories.collection.create(grant=grant)
        grant_recipient = factories.grant_recipient.create(grant=grant)
        data_source = factories.data_source.create(
            grant=grant,
            collection=report,
            type=DataSourceType.GRANT_RECIPIENT,
            has_column_of_each_type=True,
        )
        factories.data_source_organisation_item.create(
            data_source=data_source,
            external_id=grant_recipient.organisation.external_id,
            _data={"c_just_text": "hello", "c_whole_number": None},
        )
        factories.question.create(
            form__collection=report,
            hint=InterpolationStatement(
                "You were allocated " + ExpressionReference.from_data_source_column(data_source, "c_just_text").wrapped
            ),
        )

        assert (
            data_source.has_missing_referenced_data_for_organisation(grant_recipient.organisation.external_id) is False
        )
