"""Tests for the isolated ISTAT XML export orchestration layer."""

from __future__ import annotations

from dataclasses import FrozenInstanceError
from datetime import date

from bookings.models import Booking
from django.test import TestCase
from guests.models import Guest
from istat.models import IstatMunicipality
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.files.file_builder import (
    ISTAT_XML_CONTENT_TYPE,
    build_xml_export_result,
    calculate_utf8_byte_size,
    encode_xml_content,
)
from istat.xml_export.files.naming import (
    build_c59_xml_filename,
    build_guest_xml_filename,
)
from istat.xml_export.models.export_result import IstatXmlExportResult
from istat.xml_export.services.xml_export_service import (
    generate_c59_xml_export,
    generate_guest_xml_export,
)
from properties.models import Property, PropertyType
from structures.models import Structure


class XmlExportServiceTestCase(TestCase):
    def setUp(self):
        from django.contrib.auth.models import User
        self.user = User.objects.create_user(username="testuser", password="pass123")
        self.structure = Structure.objects.create(
            user=self.user,
            name="Hotel Liguria",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
            istat_code="LIG001",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Double Room",
            max_guests=2,
            num_beds=2,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 101",
        )
        IstatMunicipality.objects.create(
            code="015146000",
            name="Milano",
            province="MI",
            region="Lombardia",
        )

    def _create_booking_with_guest(
        self,
        *,
        check_in=date(2026, 5, 13),
        check_out=date(2026, 5, 14),
        country="IT",
        province="MI",
        is_checked_in=True,
    ):
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=check_in,
            check_out_date=check_out,
            adults_count=1,
            children_count=0,
            tourism_type="Business",
            transport_type="Car",
            is_checked_in=is_checked_in,
        )
        Guest.objects.create(
            booking=booking,
            full_name="Mario Rossi",
            is_main_guest=True,
            guest_type="16",
            gender="male",
            date_of_birth=date(1990, 1, 1),
            country_of_birth=country,
            nationality=country,
            country=country,
            city="Milano" if country == "IT" else "",
            extra_data={"province": province} if province else {},
        )
        return booking

    def test_guest_export_generates_valid_result(self):
        self._create_booking_with_guest()

        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )

        self.assertIsInstance(result, IstatXmlExportResult)
        self.assertEqual(
            result.filename,
            f"istat_movimenti_{self.structure.id}_2026_05_13.xml",
        )
        self.assertEqual(result.content_type, ISTAT_XML_CONTENT_TYPE)
        self.assertTrue(result.content.startswith("<?xml"))
        self.assertIn("<MOVIMENTO>", result.content)
        self.assertEqual(result.byte_size, len(result.content.encode("utf-8")))

    def test_guest_export_empty_period_still_returns_xml_document(self):
        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )

        self.assertEqual(
            result.filename,
            f"istat_movimenti_{self.structure.id}_2026_05_13.xml",
        )
        self.assertTrue(result.content.startswith("<?xml"))
        self.assertNotIn("<MOVIMENTO>", result.content)

    def test_guest_export_excludes_stays_that_arrived_before_period(self):
        self._create_booking_with_guest(
            check_in=date(2026, 5, 10),
            check_out=date(2026, 5, 15),
        )

        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )

        self.assertNotIn("<MOVIMENTO>", result.content)

    def test_guest_export_rejects_invalid_dates(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_guest_xml_export(
                structure_id=self.structure.id,
                start_date=date(2026, 5, 14),
                end_date=date(2026, 5, 13),
            )

    def test_guest_export_rejects_invalid_structure(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_guest_xml_export(
                structure_id=999999,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            )

    def test_guest_export_rejects_missing_persisted_guests(self):
        Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 5, 13),
            check_out_date=date(2026, 5, 14),
            adults_count=3,
            children_count=0,
            tourism_type="Business",
            transport_type="Car",
            is_checked_in=True,
        )

        with self.assertRaises(XmlPayloadValidationError) as context:
            generate_guest_xml_export(
                structure_id=self.structure.id,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            )

        self.assertIn("occupancy exceeds saved guest records", str(context.exception))

    def test_c59_export_generates_valid_result(self):
        self._create_booking_with_guest()

        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 5, 13),
        )

        self.assertEqual(
            result.filename,
            f"istat_c59_{self.structure.id}_2026_05_13.xml",
        )
        self.assertTrue(result.content.startswith("<?xml"))
        self.assertIn('xmlns:rim="http://www.regione.liguria.it/turismo/rimovcli"', result.content)
        self.assertIn('idstruttura="LIG001"', result.content)
        self.assertIn("<rim:rigac59", result.content)

    def test_c59_export_empty_rows_still_returns_namespaced_document(self):
        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 5, 13),
        )

        self.assertEqual(
            result.filename,
            f"istat_c59_{self.structure.id}_2026_05_13.xml",
        )
        self.assertIn("<rim:c59", result.content)
        self.assertIn("<rim:giornaliero", result.content)
        self.assertNotIn("<rim:rigac59", result.content)

    def test_c59_export_rejects_missing_report_date(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_xml_export(
                structure_id=self.structure.id,
                report_date=None,
            )

    def test_c59_export_rejects_missing_istat_code(self):
        self.structure.istat_code = None
        self.structure.save(update_fields=["istat_code"])

        with self.assertRaises(XmlPayloadValidationError) as context:
            generate_c59_xml_export(
                structure_id=self.structure.id,
                report_date=date(2026, 5, 13),
            )

        self.assertIn("missing ISTAT structure code", str(context.exception))

    def test_c59_export_rejects_blank_istat_code(self):
        self.structure.istat_code = "   "
        self.structure.save(update_fields=["istat_code"])

        with self.assertRaises(XmlPayloadValidationError) as context:
            generate_c59_xml_export(
                structure_id=self.structure.id,
                report_date=date(2026, 5, 13),
            )

        self.assertIn("missing ISTAT structure code", str(context.exception))


class XmlExportCheckedInFilterTests(TestCase):
    """Verify that only checked-in bookings are included in XML exports.

    The XML export dataset must exactly match the ISTAT page dataset, which
    filters to is_checked_in=True.
    """

    def setUp(self):
        from django.contrib.auth.models import User
        self.user = User.objects.create_user(username="filteruser", password="pass123")
        self.structure = Structure.objects.create(
            user=self.user,
            name="Hotel Filter",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
            istat_code="FLT001",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Standard Room",
            max_guests=2,
            num_beds=2,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 1",
        )
        IstatMunicipality.objects.create(
            code="015146000",
            name="Milano",
            province="MI",
            region="Lombardia",
        )

    def _make_booking(self, *, is_checked_in, country="IT", province="MI", gender="male"):
        """Create a booking+guest pair with the given checked-in state."""
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 5, 13),
            check_out_date=date(2026, 5, 14),
            adults_count=1,
            children_count=0,
            tourism_type="Business",
            transport_type="Car",
            is_checked_in=is_checked_in,
        )
        Guest.objects.create(
            booking=booking,
            full_name="Test Guest",
            is_main_guest=True,
            guest_type="16",
            gender=gender,
            date_of_birth=date(1990, 1, 1),
            country_of_birth=country,
            nationality=country,
            country=country,
            city="Milano" if country == "IT" else "",
            extra_data={"province": province} if country == "IT" else {},
        )
        return booking

    # ── Guest XML export ────────────────────────────────────────────────────

    def test_guest_xml_excludes_non_checked_in_bookings(self):
        """Non-checked-in bookings must not appear in the guest XML export."""
        self._make_booking(is_checked_in=True)   # valid, checked in
        self._make_booking(is_checked_in=False)  # not checked in — must be ignored

        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )

        # Only the checked-in booking produces a MOVIMENTO element.
        self.assertEqual(result.content.count("<MOVIMENTO>"), 1)

    def test_guest_xml_non_checked_in_with_invalid_guest_does_not_fail_export(self):
        """A non-checked-in booking with invalid guest data must not cause a
        validation error — it should simply be ignored by the export."""
        # Valid checked-in booking.
        self._make_booking(is_checked_in=True)
        # Non-checked-in booking whose guest is missing gender — would fail if
        # it were included in the export.
        self._make_booking(is_checked_in=False, gender=None)

        # Must succeed without raising XmlPayloadValidationError.
        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )
        self.assertIn("<MOVIMENTO>", result.content)

    def test_guest_xml_checked_in_with_invalid_guest_still_fails(self):
        """A checked-in booking with invalid guest data must still raise a
        validation error — the filter does not bypass validation."""
        self._make_booking(is_checked_in=True, gender=None)

        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "gender is required for ISTAT XML payload",
        ):
            generate_guest_xml_export(
                structure_id=self.structure.id,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            )

    def test_guest_xml_missing_date_of_birth_fails_with_field_message(self):
        self._make_booking(is_checked_in=True)
        Guest.objects.update(date_of_birth=None)

        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "date_of_birth is required for ISTAT XML payload",
        ):
            generate_guest_xml_export(
                structure_id=self.structure.id,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            )

    def test_guest_xml_missing_country_of_birth_fails_with_field_message(self):
        self._make_booking(is_checked_in=True)
        Guest.objects.update(country_of_birth=None)

        with self.assertRaisesMessage(
            XmlPayloadValidationError,
            "country_of_birth is required for ISTAT XML payload",
        ):
            generate_guest_xml_export(
                structure_id=self.structure.id,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            )

    def test_guest_xml_all_non_checked_in_returns_empty_document(self):
        """When all bookings in the period are non-checked-in the export
        returns a valid but empty XML document."""
        self._make_booking(is_checked_in=False)

        result = generate_guest_xml_export(
            structure_id=self.structure.id,
            start_date=date(2026, 5, 13),
            end_date=date(2026, 5, 13),
        )
        self.assertTrue(result.content.startswith("<?xml"))
        self.assertNotIn("<MOVIMENTO>", result.content)

    # ── C59 XML export ──────────────────────────────────────────────────────

    def test_c59_xml_excludes_non_checked_in_bookings(self):
        """Non-checked-in bookings must not appear in the C59 XML export."""
        self._make_booking(is_checked_in=True)   # contributes a rigac59 row
        self._make_booking(is_checked_in=False)  # must be ignored

        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 5, 13),
        )

        # Only the checked-in booking produces a rigac59 element.
        self.assertEqual(result.content.count("<rim:rigac59"), 1)

    def test_c59_xml_non_checked_in_with_invalid_guest_does_not_fail_export(self):
        """A non-checked-in booking with invalid guest data must not cause a
        validation error in the C59 export."""
        self._make_booking(is_checked_in=True)
        # Non-checked-in booking with a country that has no ISTAT mapping.
        self._make_booking(is_checked_in=False, country="XX", province=None)

        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 5, 13),
        )
        self.assertIn("<rim:rigac59", result.content)

    def test_c59_xml_all_non_checked_in_returns_empty_document(self):
        """When all bookings in the period are non-checked-in the C59 export
        returns a valid but empty namespaced document."""
        self._make_booking(is_checked_in=False)

        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 5, 13),
        )
        self.assertIn("<rim:c59", result.content)
        self.assertNotIn("<rim:rigac59", result.content)


class XmlExportFileBuilderTests(TestCase):
    def test_export_result_is_immutable_and_byte_size_is_accurate(self):
        content = '<?xml version="1.0" encoding="UTF-8"?><root>è</root>'

        result = build_xml_export_result(
            filename="istat_movimenti_2_2026_05_13.xml",
            content=content,
        )

        self.assertEqual(result.byte_size, calculate_utf8_byte_size(content))
        self.assertEqual(result.byte_size, len(encode_xml_content(content)))
        with self.assertRaises(FrozenInstanceError):
            result.filename = "changed.xml"

    def test_filename_helpers_are_deterministic_and_safe(self):
        self.assertEqual(
            build_guest_xml_filename(
                structure_id=2,
                start_date=date(2026, 5, 13),
                end_date=date(2026, 5, 13),
            ),
            "istat_movimenti_2_2026_05_13.xml",
        )
        self.assertEqual(
            build_guest_xml_filename(
                structure_id=2,
                start_date=date(2026, 5, 1),
                end_date=date(2026, 5, 31),
            ),
            "istat_movimenti_2_2026_05_01_to_2026_05_31.xml",
        )
        self.assertEqual(
            build_c59_xml_filename(structure_id=2, export_date=date(2026, 5, 13)),
            "istat_c59_2_2026_05_13.xml",
        )

    def test_guest_filename_single_day_and_range_do_not_collide(self):
        single_day = build_guest_xml_filename(
            structure_id=2,
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 1),
        )
        date_range = build_guest_xml_filename(
            structure_id=2,
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )

        self.assertNotEqual(single_day, date_range)
