import datetime

import pytest

from app.common.data.interfaces.organisations import get_organisation_count, get_organisations, upsert_organisations
from app.common.data.models import Organisation
from app.common.data.types import (
    OrganisationData,
    OrganisationModeEnum,
    OrganisationStatus,
    OrganisationType,
)


class TestGetOrganisations:
    def test_returns_grant_managing_organisations(self, factories, db_session):
        from tests.models import _get_grant_managing_organisation

        grant_managing_org = _get_grant_managing_organisation()
        factories.organisation.create(name="Regular Org 1", can_manage_grants=False)
        factories.organisation.create(name="Regular Org 2", can_manage_grants=False)

        result = get_organisations(can_manage_grants=True)

        assert len(result) == 1
        assert result[0].id == grant_managing_org.id

    def test_returns_non_grant_managing_organisations(self, factories, db_session):
        from tests.models import _get_grant_managing_organisation

        _get_grant_managing_organisation()
        org1 = factories.organisation.create(name="Regular Org 1", can_manage_grants=False)
        org2 = factories.organisation.create(name="Regular Org 2", can_manage_grants=False)
        org3 = factories.organisation.create(name="Regular Org 3", can_manage_grants=False)

        result = get_organisations(can_manage_grants=False)

        assert len(result) == 3
        assert {org.id for org in result} == {org1.id, org2.id, org3.id}

    def test_returns_empty_list_when_no_matches(self, factories, db_session):
        from tests.models import _get_grant_managing_organisation

        _get_grant_managing_organisation()

        result = get_organisations(can_manage_grants=False)

        assert result == []

    def test_returns_all_organisations_when_can_manage_grants_is_none(self, factories, db_session):
        from tests.models import _get_grant_managing_organisation

        grant_managing_org = _get_grant_managing_organisation()
        org1 = factories.organisation.create(name="Regular Org 1", can_manage_grants=False)
        org2 = factories.organisation.create(name="Regular Org 2", can_manage_grants=False)

        result = get_organisations(can_manage_grants=None)

        assert len(result) == 3
        assert {org.id for org in result} == {grant_managing_org.id, org1.id, org2.id}

    def test_filters_by_ids(self, factories, db_session):
        org1 = factories.organisation.create(name="Org 1")
        factories.organisation.create(name="Org 2")
        org3 = factories.organisation.create(name="Org 3")

        result = get_organisations(with_ids=[org1.id, org3.id])

        assert len(result) == 2
        assert {org.id for org in result} == {org1.id, org3.id}

    def test_filters_by_external_ids(self, factories, db_session):
        org1 = factories.organisation.create(name="Org 1", external_id="E06000001")
        factories.organisation.create(name="Org 2", external_id="E06000002")
        org3 = factories.organisation.create(name="Org 3", external_id="E06000003")

        result = get_organisations(with_external_ids=["E06000001", "E06000003"])

        assert len(result) == 2
        assert {org.id for org in result} == {org1.id, org3.id}

    def test_raises_when_both_with_ids_and_with_external_ids_specified(self, factories, db_session):
        org = factories.organisation.create()

        with pytest.raises(ValueError, match="Cannot specify both with_ids and with_external_ids"):
            get_organisations(with_ids=[org.id], with_external_ids=["E06000001"])

    def test_with_ids_respects_mode_filter(self, factories, db_session):
        live_org = factories.organisation.create(name="Live Org", mode=OrganisationModeEnum.LIVE)
        test_org = factories.organisation.create(name="Test Org", mode=OrganisationModeEnum.TEST)

        result = get_organisations(mode=OrganisationModeEnum.LIVE, with_ids=[live_org.id, test_org.id])

        assert len(result) == 1
        assert result[0].id == live_org.id

    def test_with_external_ids_respects_mode_filter(self, factories, db_session):
        live_org = factories.organisation.create(
            name="Live Org", external_id="E06000001", mode=OrganisationModeEnum.LIVE, with_matching_test_org=True
        )

        result = get_organisations(mode=OrganisationModeEnum.TEST, with_external_ids=["E06000001"])

        assert len(result) == 1
        assert result[0].id == live_org.matching_test_organisation.id

    def test_with_ids_returns_empty_when_no_matches(self, factories, db_session):
        from uuid import uuid4

        factories.organisation.create(name="Org 1")

        result = get_organisations(with_ids=[uuid4()])

        assert result == []

    def test_with_external_ids_returns_empty_when_no_matches(self, factories, db_session):
        factories.organisation.create(name="Org 1", external_id="E06000001")

        result = get_organisations(with_external_ids=["NONEXISTENT"])

        assert result == []

    def test_domain_returns_empty_when_no_matches(self, factories, db_session):
        factories.organisation.create(name="Org 1", domains=["a-domain.com"])

        result = get_organisations(domain="b-domain.com")

        assert result == []

    def test_domain_filters_organisations_by_domain(self, factories, db_session):
        org1 = factories.organisation.create(name="Org 1", domains=["a-domain.com", "b-domain.com"])
        org2 = factories.organisation.create(name="Org 2", domains=["b-domain.com"])
        factories.organisation.create(name="Org 3", domains=["a-domain.com"])
        factories.organisation.create(name="Org 4", domains=["c-domain.com"])

        result = get_organisations(domain="b-domain.com")

        assert len(result) == 2
        assert result == [org1, org2]

    def test_domain_filters_organisations_by_domain_case_insensitive(self, factories, db_session):
        org1 = factories.organisation.create(name="Org 1", domains=["a-domain.com"])
        org2 = factories.organisation.create(name="Org 2", domains=["A-DoMaIn.COM"])
        factories.organisation.create(name="Org 3", domains=["b-domain.com"])

        result = get_organisations(domain="A-DOMAIN.COM")

        assert len(result) == 2
        assert result == [org1, org2]


class TestGetOrganisationCount:
    def test_returns_count_of_non_grant_managing_organisations(self, factories, db_session):
        factories.organisation.create(name="Regular Org 1", can_manage_grants=False)
        factories.organisation.create(name="Regular Org 2", can_manage_grants=False)
        factories.organisation.create(name="Regular Org 3", can_manage_grants=False)

        assert get_organisation_count() == 3

    def test_counts_only_non_grant_managing_organisations(self, factories, db_session):
        from tests.models import _get_grant_managing_organisation

        _get_grant_managing_organisation()
        factories.organisation.create(name="Regular Org 1", can_manage_grants=False)
        factories.organisation.create(name="Regular Org 2", can_manage_grants=False)

        assert get_organisation_count() == 2


class TestUpsertOrganisations:
    def test_inserts_new_organisation(self, db_session):
        new_org = OrganisationData(
            external_id="GB-GOV-123",
            name="Test Department",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=datetime.date(2020, 1, 1),
            retirement_date=None,
            iati_id="GB-GOV-123",
            domains=["test.gov.uk"],
        )

        upsert_organisations([new_org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.name == "Test Department"
        assert org_from_db.type == OrganisationType.CENTRAL_GOVERNMENT
        assert org_from_db.can_manage_grants is False
        assert org_from_db.status == OrganisationStatus.ACTIVE
        assert org_from_db.active_date == datetime.date(2020, 1, 1)
        assert org_from_db.retirement_date is None
        assert org_from_db.iati_id == "GB-GOV-123"
        assert org_from_db.domains == ["test.gov.uk"]

    def test_inserts_multiple_new_organisations(self, db_session):
        orgs = [
            OrganisationData(
                external_id="GB-GOV-123",
                name="Department A",
                type=OrganisationType.CENTRAL_GOVERNMENT,
                active_date=None,
                retirement_date=None,
                iati_id="GB-GOV-123",
            ),
            OrganisationData(
                external_id="E06000001",
                name="Council A",
                type=OrganisationType.UNITARY_AUTHORITY,
                active_date=None,
                retirement_date=None,
                ons_lad_id="E06000001",
            ),
            OrganisationData(
                external_id="E07000001",
                name="Council B",
                type=OrganisationType.SHIRE_DISTRICT,
                active_date=None,
                retirement_date=None,
                ons_lad_id="E07000001",
            ),
        ]

        upsert_organisations(orgs)

        db_session.expire_all()
        assert db_session.query(Organisation).filter_by(external_id="GB-GOV-123").count() == 1
        assert db_session.query(Organisation).filter_by(external_id="E06000001").count() == 1
        assert db_session.query(Organisation).filter_by(external_id="E07000001").count() == 1

    def test_inserts_organisation_with_typed_ids(self, db_session):
        orgs = [
            OrganisationData(
                external_id="CC-12345678",
                name="Test Charity",
                type=OrganisationType.CHARITY,
                active_date=None,
                retirement_date=None,
                charity_commission_number="12345678",
            ),
            OrganisationData(
                external_id="CH-98765432",
                name="Test Company",
                type=OrganisationType.COMPANY,
                active_date=None,
                retirement_date=None,
                companies_house_number="98765432",
            ),
            OrganisationData(
                external_id="FS-CUSTOM1",
                name="Test Other",
                type=OrganisationType.OTHER,
                active_date=None,
                retirement_date=None,
                custom_code="CUSTOM1",
            ),
        ]

        upsert_organisations(orgs)

        db_session.expire_all()
        charity = db_session.query(Organisation).filter_by(external_id="CC-12345678").one()
        assert charity.charity_commission_number == "12345678"

        company = db_session.query(Organisation).filter_by(external_id="CH-98765432").one()
        assert company.companies_house_number == "98765432"

        other = db_session.query(Organisation).filter_by(external_id="FS-CUSTOM1").one()
        assert other.custom_code == "CUSTOM1"

    def test_updates_existing_organisation_by_external_id(self, factories, db_session):
        existing_org = factories.organisation.create(
            external_id="GB-GOV-123",
            name="Old Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            can_manage_grants=False,
            domains=["old-domain.gov.uk"],
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="New Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=datetime.date(2021, 5, 15),
            retirement_date=None,
            iati_id="GB-GOV-123",
            domains=["new-domain.gov.uk"],
        )

        upsert_organisations([updated_org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.id == existing_org.id
        assert org_from_db.name == "New Name"
        assert org_from_db.active_date == datetime.date(2021, 5, 15)
        assert org_from_db.domains == ["new-domain.gov.uk"]

    def test_sets_status_to_active_when_no_retirement_date(self, db_session):
        org = OrganisationData(
            external_id="GB-GOV-123",
            name="Active Org",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=None,
            iati_id="GB-GOV-123",
        )

        upsert_organisations([org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.status == OrganisationStatus.ACTIVE

    def test_sets_status_to_retired_when_retirement_date_present(self, db_session):
        org = OrganisationData(
            external_id="GB-GOV-123",
            name="Retired Org",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=datetime.date(2010, 1, 1),
            retirement_date=datetime.date(2020, 12, 31),
            iati_id="GB-GOV-123",
        )

        upsert_organisations([org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.status == OrganisationStatus.RETIRED
        assert org_from_db.retirement_date == datetime.date(2020, 12, 31)

    def test_updates_organisation_status_from_active_to_retired(self, factories, db_session, caplog):
        existing_org = factories.organisation.create(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            status=OrganisationStatus.ACTIVE,
            can_manage_grants=False,
            retirement_date=None,
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=datetime.date(2023, 6, 30),
            iati_id="GB-GOV-123",
        )

        upsert_organisations([updated_org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.id == existing_org.id
        assert org_from_db.status == OrganisationStatus.RETIRED
        assert org_from_db.retirement_date == datetime.date(2023, 6, 30)

        assert len(caplog.messages) == 1
        assert "Active organisation Org Name [GB-GOV-123] has been retired as of 2023-06-30" in caplog.messages[0]

    def test_updates_organisation_status_from_retired_to_active(self, factories, db_session):
        existing_org = factories.organisation.create(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            status=OrganisationStatus.RETIRED,
            can_manage_grants=False,
            retirement_date=datetime.date(2020, 12, 31),
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=None,
            iati_id="GB-GOV-123",
        )

        upsert_organisations([updated_org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.id == existing_org.id
        assert org_from_db.status == OrganisationStatus.ACTIVE
        assert org_from_db.retirement_date is None

    def test_upserts_mix_of_new_and_existing_organisations(self, factories, db_session):
        factories.organisation.create(
            external_id="GB-GOV-123",
            name="Existing Org",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            can_manage_grants=False,
        )

        orgs = [
            OrganisationData(
                external_id="GB-GOV-123",
                name="Updated Existing Org",
                type=OrganisationType.CENTRAL_GOVERNMENT,
                active_date=None,
                retirement_date=None,
                iati_id="GB-GOV-123",
            ),
            OrganisationData(
                external_id="E06000001",
                name="New Org",
                type=OrganisationType.UNITARY_AUTHORITY,
                active_date=None,
                retirement_date=None,
                ons_lad_id="E06000001",
            ),
        ]

        upsert_organisations(orgs)

        db_session.expire_all()
        existing_org = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert existing_org.name == "Updated Existing Org"

        new_org = db_session.query(Organisation).filter_by(external_id="E06000001").one()
        assert new_org.name == "New Org"

    def test_logs_multiple_organisations_retired(self, factories, db_session, caplog):
        factories.organisation.create(
            external_id="GB-GOV-123",
            name="Department A",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            status=OrganisationStatus.ACTIVE,
            can_manage_grants=False,
        )
        factories.organisation.create(
            external_id="E06000001",
            name="Council B",
            type=OrganisationType.UNITARY_AUTHORITY,
            status=OrganisationStatus.ACTIVE,
            can_manage_grants=False,
        )

        updated_orgs = [
            OrganisationData(
                external_id="GB-GOV-123",
                name="Department A",
                type=OrganisationType.CENTRAL_GOVERNMENT,
                active_date=None,
                retirement_date=datetime.date(2023, 6, 30),
                iati_id="GB-GOV-123",
            ),
            OrganisationData(
                external_id="E06000001",
                name="Council B",
                type=OrganisationType.UNITARY_AUTHORITY,
                active_date=None,
                retirement_date=datetime.date(2023, 12, 31),
                ons_lad_id="E06000001",
            ),
        ]

        upsert_organisations(updated_orgs)

        db_session.expire_all()
        assert len(caplog.messages) == 2
        assert "Active organisation Department A [GB-GOV-123] has been retired as of 2023-06-30" in caplog.messages
        assert "Active organisation Council B [E06000001] has been retired as of 2023-12-31" in caplog.messages

    def test_does_not_log_when_inserting_new_retired_organisation(self, db_session, caplog):
        new_org = OrganisationData(
            external_id="GB-GOV-123",
            name="Already Retired Org",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=datetime.date(2020, 12, 31),
            iati_id="GB-GOV-123",
        )

        upsert_organisations([new_org])

        db_session.expire_all()
        assert len(caplog.messages) == 0

    def test_does_not_log_when_active_organisation_remains_active(self, factories, db_session, caplog):
        factories.organisation.create(
            external_id="GB-GOV-123",
            name="Old Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            status=OrganisationStatus.ACTIVE,
            can_manage_grants=False,
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="New Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=None,
            iati_id="GB-GOV-123",
        )

        upsert_organisations([updated_org])

        db_session.expire_all()
        assert len(caplog.messages) == 0

    def test_does_not_log_when_retired_organisation_becomes_active(self, factories, db_session, caplog):
        factories.organisation.create(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            status=OrganisationStatus.RETIRED,
            can_manage_grants=False,
            retirement_date=datetime.date(2020, 12, 31),
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="Org Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=None,
            retirement_date=None,
            iati_id="GB-GOV-123",
        )

        upsert_organisations([updated_org])

        db_session.expire_all()
        assert len(caplog.messages) == 0

    def test_handles_empty_list(self, db_session):
        initial_count = db_session.query(Organisation).count()

        upsert_organisations([])

        db_session.expire_all()
        final_count = db_session.query(Organisation).count()
        assert final_count == initial_count

    @pytest.mark.parametrize(
        "new_domains, expected_domains",
        [(None, ["old-domain.gov.uk"]), ([], []), (["new-domain.gov.uk"], ["new-domain.gov.uk"])],
    )
    def test_does_not_override_existing_domains_if_not_provided(
        self, factories, db_session, new_domains, expected_domains
    ):
        factories.organisation.create(
            external_id="GB-GOV-123",
            name="Old Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            can_manage_grants=False,
            domains=["old-domain.gov.uk"],
        )

        updated_org = OrganisationData(
            external_id="GB-GOV-123",
            name="New Name",
            type=OrganisationType.CENTRAL_GOVERNMENT,
            active_date=datetime.date(2021, 5, 15),
            retirement_date=None,
            iati_id="GB-GOV-123",
        )

        if new_domains is not None:
            updated_org.domains = new_domains

        upsert_organisations([updated_org])

        db_session.expire_all()
        org_from_db = db_session.query(Organisation).filter_by(external_id="GB-GOV-123").one()
        assert org_from_db.domains == expected_domains
