from __future__ import annotations

from datetime import date
from types import SimpleNamespace
from unittest.mock import patch
from xml.etree import ElementTree

from django.test import SimpleTestCase, TestCase

from istat.models import IstatMunicipality
from istat.xml_export.builders.payload_builder import build_guest_payload
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.payloads.guest_payload import IstatXmlGuestPayload
from istat.xml_export.services.guest_export_normalization import (
    normalize_guest_for_istat_export,
)
from istat.xml_export.services.residence_resolver import resolve_xml_residence
from istat.xml_export.xml.xml_utils import format_xml_date
from istat.xml_export.xml.serializer import serialize_payloads


class XmlResidenceResolverTests(SimpleTestCase):
    def test_italian_resident_uses_province_and_derives_region(self):
        with patch(
            "istat.xml_export.services.residence_resolver.validate_country_mapping"
        ) as validate_country_mapping:
            result = resolve_xml_residence(country="IT", province="MI")

        self.assertEqual(
            result,
            {
                "is_italian": True,
                "province_code": "015",
                "region_code": "903",
                "country_code": None,
            },
        )
        validate_country_mapping.assert_not_called()

    def test_italian_resident_requires_province(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Province is required for Italian residents",
        ):
            resolve_xml_residence(country="IT", province=None)

    def test_foreign_resident_uses_country_mapping_only(self):
        with patch(
            "istat.xml_export.services.residence_resolver.validate_province_mapping"
        ) as validate_province_mapping:
            result = resolve_xml_residence(country="IN", province=None)

        self.assertEqual(
            result,
            {
                "is_italian": False,
                "country_code": "664",
                "province_code": None,
                "region_code": None,
            },
        )
        validate_province_mapping.assert_not_called()

    def test_foreign_resident_ignores_accidental_province(self):
        with patch(
            "istat.xml_export.services.residence_resolver.validate_province_mapping"
        ) as validate_province_mapping:
            result = resolve_xml_residence(country="FR", province="MI")

        self.assertEqual(
            result,
            {
                "is_italian": False,
                "country_code": "001",
                "province_code": None,
                "region_code": None,
            },
        )
        validate_province_mapping.assert_not_called()

    def test_unknown_foreign_country_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Missing ISTAT country mapping for 'XX'",
        ):
            resolve_xml_residence(country="XX")

    def test_unknown_italian_province_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Missing ISTAT province mapping for 'ZZ'",
        ):
            resolve_xml_residence(country="IT", province="ZZ")


class XmlGuestExportNormalizationTests(SimpleTestCase):
    def test_nationality_falls_back_to_birth_country_and_is_uppercase(self):
        guest = SimpleNamespace(
            country_of_birth=" in ",
            nationality=None,
            country=None,
            tourism_type=None,
            transport_type=None,
            extra_data={},
        )

        normalized = normalize_guest_for_istat_export(guest)

        self.assertEqual(normalized.country_of_birth, "IN")
        self.assertEqual(normalized.nationality, "IN")
        self.assertEqual(normalized.country, "IN")


class XmlGuestPayloadBuilderTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        IstatMunicipality.objects.create(
            code="015146000",
            name="Milano",
            province="MI",
            region="Lombardia",
        )

    def _booking(self):
        return SimpleNamespace(
            id=42,
            check_in_date=date(2026, 5, 1),
            check_out_date=date(2026, 5, 4),
            tourism_type="Cultural",
            transport_type="Car",
        )

    def _guest(self, **overrides):
        data = {
            "id": 7,
            "country": "IT",
            "extra_data": {"province": "MI"},
            "city": "Milano",
            "gender": "male",
            "date_of_birth": date(1990, 1, 1),
            "country_of_birth": "IT",
            "nationality": "IT",
            "guest_type": "16",
            "tourism_type": "Beach",
            "transport_type": "Train",
        }
        data.update(overrides)
        return SimpleNamespace(**data)

    def test_italian_resident_payload(self):
        payload = build_guest_payload(booking=self._booking(), guest=self._guest())

        self.assertIsInstance(payload, IstatXmlGuestPayload)
        self.assertEqual(payload.booking_id, 42)
        self.assertEqual(payload.guest_id, 7)
        self.assertEqual(payload.arrival_date, date(2026, 5, 1))
        self.assertEqual(payload.departure_date, date(2026, 5, 4))
        self.assertEqual(payload.nights, 3)
        self.assertTrue(payload.is_italian_resident)
        self.assertEqual(payload.residence_country_iso2, "IT")
        self.assertIsNone(payload.residence_country_code)
        self.assertEqual(payload.province_sigla, "MI")
        self.assertEqual(payload.province_code, "015")
        self.assertEqual(payload.region_code, "903")
        self.assertEqual(payload.gender, "M")
        self.assertEqual(payload.guest_type, "16")
        self.assertEqual(payload.tourism_type, "Beach")
        self.assertEqual(payload.transport_type, "Train")

    def test_payload_reads_country_and_residence_province_from_extra_data(self):
        payload = build_guest_payload(
            booking=self._booking(),
            guest=self._guest(
                country=None,
                extra_data={
                    "country": "IT",
                    "residence_province": "MI",
                    "city": "Milano",
                },
            ),
        )

        self.assertTrue(payload.is_italian_resident)
        self.assertEqual(payload.residence_country_iso2, "IT")
        self.assertEqual(payload.province_sigla, "MI")
        self.assertEqual(payload.province_code, "015")

    def test_foreign_resident_payload(self):
        payload = build_guest_payload(
            booking=self._booking(),
            guest=self._guest(country="IN", extra_data={}),
        )

        self.assertFalse(payload.is_italian_resident)
        self.assertEqual(payload.residence_country_iso2, "IN")
        self.assertEqual(payload.residence_country_code, "664")
        self.assertIsNone(payload.province_sigla)
        self.assertIsNone(payload.province_code)
        self.assertIsNone(payload.region_code)
        self.assertEqual(payload.gender, "M")

    def test_residence_country_falls_back_to_birth_country(self):
        payload = build_guest_payload(
            booking=self._booking(),
            guest=self._guest(
                country=None,
                nationality=None,
                country_of_birth="IN",
                tourism_type=None,
                transport_type="",
                extra_data={},
            ),
        )

        self.assertFalse(payload.is_italian_resident)
        self.assertEqual(payload.residence_country_iso2, "IN")
        self.assertEqual(payload.residence_country_code, "664")
        self.assertEqual(payload.tourism_type, "Not Specified")
        self.assertEqual(payload.transport_type, "Not Specified")

    def test_missing_residence_fallback_source_still_fails(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "country_of_birth is required for ISTAT XML payload",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(
                    country=None,
                    nationality=None,
                    country_of_birth=None,
                    extra_data={},
                ),
            )

    def test_missing_date_of_birth_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "date_of_birth is required for ISTAT XML payload",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(date_of_birth=None),
            )

    def test_missing_country_of_birth_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "country_of_birth is required for ISTAT XML payload",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(country_of_birth=" "),
            )

    def test_country_codes_are_uppercase_normalized_before_payload(self):
        payload = build_guest_payload(
            booking=self._booking(),
            guest=self._guest(
                country=" in ",
                country_of_birth=" it ",
                nationality=" in ",
                extra_data={},
            ),
        )

        self.assertEqual(payload.residence_country_iso2, "IN")

    def test_missing_province_for_italian_guest_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Province is required for Italian residents",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(extra_data={}),
            )

    def test_unknown_province_mapping_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Province must be a valid Italian province code",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(extra_data={"province": "ZZ"}),
            )

    def test_municipality_province_mismatch_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Province must match the selected municipality",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(extra_data={"province": "AL"}),
            )

    def test_unknown_country_mapping_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Missing ISTAT country mapping for 'XX'",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(country="XX", extra_data={}),
            )

    def test_missing_gender_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "gender is required for ISTAT XML payload",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(gender=None),
            )

    def test_missing_guest_type_raises_validation_error(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "guest_type is required for ISTAT XML payload",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(guest_type=None),
            )

    def test_old_italian_guest_without_province_fails_gracefully(self):
        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "Province is required for Italian residents",
        ):
            build_guest_payload(
                booking=self._booking(),
                guest=self._guest(extra_data=None),
            )

    def test_province_ignored_for_foreign_resident(self):
        with patch(
            "istat.xml_export.builders.payload_builder.IstatLookupService.get_province"
        ) as get_province:
            payload = build_guest_payload(
                booking=self._booking(),
                guest=self._guest(country="FR", extra_data={"province": "MI"}),
            )

        self.assertFalse(payload.is_italian_resident)
        self.assertEqual(payload.residence_country_code, "001")
        self.assertIsNone(payload.province_sigla)
        self.assertIsNone(payload.province_code)
        self.assertIsNone(payload.region_code)
        get_province.assert_not_called()


class XmlSerializerTests(SimpleTestCase):
    def _italian_payload(self, **overrides):
        data = {
            "booking_id": 1,
            "guest_id": 10,
            "arrival_date": date(2026, 5, 1),
            "departure_date": date(2026, 5, 4),
            "nights": 3,
            "is_italian_resident": True,
            "residence_country_iso2": "IT",
            "residence_country_code": None,
            "province_sigla": "MI",
            "province_code": "015",
            "region_code": "903",
            "gender": "M",
            "guest_type": "16",
            "tourism_type": "Cultural",
            "transport_type": "Treno",
        }
        data.update(overrides)
        return IstatXmlGuestPayload(**data)

    def _foreign_payload(self, **overrides):
        data = {
            "booking_id": 2,
            "guest_id": 20,
            "arrival_date": date(2026, 6, 10),
            "departure_date": date(2026, 6, 12),
            "nights": 2,
            "is_italian_resident": False,
            "residence_country_iso2": "IN",
            "residence_country_code": "664",
            "province_sigla": None,
            "province_code": None,
            "region_code": None,
            "gender": "F",
            "guest_type": "17",
            "tourism_type": None,
            "transport_type": None,
        }
        data.update(overrides)
        return IstatXmlGuestPayload(**data)

    def _root(self, xml: str):
        return ElementTree.fromstring(xml.encode("UTF-8"))

    def test_format_xml_date_uses_yyyymmdd(self):
        self.assertEqual(format_xml_date(date(2026, 5, 1)), "20260501")

    def test_single_italian_resident_payload_serialization(self):
        xml = serialize_payloads([self._italian_payload()])
        root = self._root(xml)
        movement = root.find("MOVIMENTO")

        self.assertEqual(root.tag, "MOVIMENTI")
        self.assertEqual(movement.findtext("DATAARRIVO"), "20260501")
        self.assertEqual(movement.findtext("DATAPARTENZA"), "20260504")
        self.assertEqual(movement.findtext("SESSO"), "M")
        self.assertEqual(movement.findtext("TIPOCLIENTE"), "16")
        self.assertEqual(movement.findtext("RESIDENZA_ITALIANA"), "S")
        self.assertEqual(movement.findtext("COD_PROVINCIA"), "015")
        self.assertEqual(movement.findtext("COD_REGIONE"), "903")
        self.assertIsNone(movement.find("COD_STATO_ESTERO"))

    def test_single_foreign_resident_payload_serialization(self):
        xml = serialize_payloads([self._foreign_payload()])
        movement = self._root(xml).find("MOVIMENTO")

        self.assertEqual(movement.findtext("DATAARRIVO"), "20260610")
        self.assertEqual(movement.findtext("DATAPARTENZA"), "20260612")
        self.assertEqual(movement.findtext("SESSO"), "F")
        self.assertEqual(movement.findtext("TIPOCLIENTE"), "17")
        self.assertEqual(movement.findtext("RESIDENZA_ITALIANA"), "N")
        self.assertEqual(movement.findtext("COD_STATO_ESTERO"), "664")
        self.assertIsNone(movement.find("COD_PROVINCIA"))
        self.assertIsNone(movement.find("COD_REGIONE"))

    def test_multiple_payload_serialization(self):
        xml = serialize_payloads([self._italian_payload(), self._foreign_payload()])
        root = self._root(xml)

        self.assertEqual(len(root.findall("MOVIMENTO")), 2)

    def test_correct_xml_encoding(self):
        xml = serialize_payloads([self._italian_payload()])

        self.assertTrue(xml.startswith("<?xml version="))
        self.assertIn("encoding='UTF-8'", xml)

    def test_correct_root_node_generation(self):
        xml = serialize_payloads([])

        self.assertEqual(self._root(xml).tag, "MOVIMENTI")

    def test_correct_xml_ordering(self):
        xml = serialize_payloads([self._italian_payload()])
        movement = self._root(xml).find("MOVIMENTO")
        tags = [child.tag for child in list(movement)]

        self.assertEqual(
            tags,
            [
                "DATAARRIVO",
                "DATAPARTENZA",
                "SESSO",
                "TIPOCLIENTE",
                "RESIDENZA_ITALIANA",
                "COD_PROVINCIA",
                "COD_REGIONE",
            ],
        )

    def test_foreign_xml_ordering(self):
        xml = serialize_payloads([self._foreign_payload()])
        movement = self._root(xml).find("MOVIMENTO")
        tags = [child.tag for child in list(movement)]

        self.assertEqual(
            tags,
            [
                "DATAARRIVO",
                "DATAPARTENZA",
                "SESSO",
                "TIPOCLIENTE",
                "RESIDENZA_ITALIANA",
                "COD_STATO_ESTERO",
            ],
        )

    def test_utf8_serialization(self):
        xml = serialize_payloads(
            [self._italian_payload(gender="È")]
        )

        self.assertIn("È", xml)
        self._root(xml)

    def test_empty_optional_field_handling(self):
        xml = serialize_payloads(
            [self._italian_payload(tourism_type=None, transport_type=None)]
        )
        movement = self._root(xml).find("MOVIMENTO")

        self.assertIsNone(movement.find("COD_STATO_ESTERO"))
        self.assertIsNone(movement.find("TipoTurismo"))
        self.assertIsNone(movement.find("MezzoTrasporto"))
