"""Production integration test for Ross1000 full XML export with guest movements.

This test validates the complete end-to-end XML generation pipeline:
1. Guest payloads with all mandatory fields
2. Movement builder with arrivals/departures on correct dates
3. XML serializer producing valid UTF-8 output
4. SOAP envelope compatibility
5. Client specification compliance

Expected XML structure per client requirements:
<?xml version="1.0" encoding="utf-8"?>
<movimenti struttura="058091-CAV-00001" dal="20260501" al="20260531">
  <movimento data="20260507">
    <struttura>
      <codice>058091-CAV-00001</codice>
      <apertura>1</apertura>
      <camereoccupate>1</camereoccupate>
      <cameredisponibili>6</cameredisponibili>
      <lettidisponibili>20</lettidisponibili>
    </struttura>
    <arrivi>
      <cliente>
        <idswh>c3c4a9a2-12ab</idswh>
        <cognome>ROSSI</cognome>
        <nome>MARIO</nome>
        <sesso>M</sesso>
        <datanascita>19830209</datanascita>
        <cittadinanza>100000100</cittadinanza>
        <statoresidenza>100000100</statoresidenza>
        <statonascita>100000100</statonascita>
      </cliente>
    </arrivi>
    <partenze/>
  </movimento>
</movimenti>
"""

from __future__ import annotations

from datetime import date, timedelta
from xml.etree import ElementTree

import pytest

from istat.ross1000.models.movement_payload import (
    Ross1000GuestPayload,
    Ross1000MovementDayPayload,
    Ross1000StrutturaPayload,
)
from istat.ross1000.soap.envelope_builder import build_soap_envelope
from istat.ross1000.xml.movement_serializer import serialize_movement_days_xml


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_struttura(
    codice="058091-CAV-00001",
    occupied=1,
    available=6,
    beds=20,
) -> Ross1000StrutturaPayload:
    return Ross1000StrutturaPayload(
        codice=codice,
        apertura=1,
        camere_occupate=occupied,
        camere_disponibili=available,
        letti_disponibili=beds,
    )


def _make_guest(
    idswh="c3c4a9a2-12ab",
    cognome="ROSSI",
    nome="MARIO",
    sesso="M",
    dob="19830209",
    statonascita="100000100",
    statoresidenza="100000100",
    cittadinanza="100000100",
) -> Ross1000GuestPayload:
    return Ross1000GuestPayload(
        idswh=idswh,
        cognome=cognome,
        nome=nome,
        sesso=sesso,
        datanascita=dob,
        cittadinanza=cittadinanza,
        statoresidenza=statoresidenza,
        statonascita=statonascita,
    )


def _make_day(
    day: date,
    arrivi=(),
    partenze=(),
    struttura=None,
) -> Ross1000MovementDayPayload:
    return Ross1000MovementDayPayload(
        date=day,
        struttura=struttura or _make_struttura(),
        arrivi=tuple(arrivi),
        partenze=tuple(partenze),
    )


def _parse(xml_str: str) -> ElementTree.Element:
    return ElementTree.fromstring(xml_str.encode("utf-8"))


# ---------------------------------------------------------------------------
# Integration Tests
# ---------------------------------------------------------------------------

class TestFullXmlExportWithGuestMovements:
    """Validate complete XML export matches client specification exactly."""

    def test_xml_declaration_utf8(self):
        """XML must start with <?xml version="1.0" encoding="UTF-8"?>."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        assert result.startswith("<?xml version='1.0' encoding='UTF-8'?>") or \
               result.startswith('<?xml version="1.0" encoding="UTF-8"?>')

    def test_root_element_has_correct_attributes(self):
        """Root <movimenti> must have struttura, dal, al attributes."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        assert root.tag == "movimenti"
        assert root.get("struttura") == "058091-CAV-00001"
        assert root.get("dal") == "20260501"
        assert root.get("al") == "20260531"

    def test_movimento_data_attribute_aaaammgg(self):
        """Each <movimento> must have data attribute in AAAAMMGG format."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        movimento = root.find("movimento[@data='20260507']")
        assert movimento is not None, "Expected <movimento data='20260507'>"

    def test_struttura_block_complete(self):
        """<struttura> must have all 5 fields: codice, apertura, camereoccupate, cameredisponibili, lettiDisponibili."""
        struttura = _make_struttura(
            codice="058091-CAV-00001",
            occupied=1,
            available=6,
            beds=20,
        )
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest], struttura=struttura)]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        s = root.find("movimento/struttura")
        assert s.find("codice").text == "058091-CAV-00001"
        assert s.find("apertura").text == "1"
        assert s.find("camereoccupate").text == "1"
        assert s.find("cameredisponibili").text == "6"
        assert s.find("lettidisponibili").text == "20"

    def test_arrivi_contains_cliente_with_all_mandatory_fields(self):
        """<arrivi> must contain <cliente> with all 8 mandatory fields."""
        guest = _make_guest(
            idswh="c3c4a9a2-12ab",
            cognome="ROSSI",
            nome="MARIO",
            sesso="M",
            dob="19830209",
            statonascita="100000100",
            statoresidenza="100000100",
            cittadinanza="100000100",
        )
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente is not None, "<cliente> element missing from <arrivi>"

        # Verify all 8 mandatory fields present
        assert cliente.find("idswh").text == "c3c4a9a2-12ab"
        assert cliente.find("cognome").text == "ROSSI"
        assert cliente.find("nome").text == "MARIO"
        assert cliente.find("sesso").text == "M"
        assert cliente.find("datanascita").text == "19830209"
        assert cliente.find("cittadinanza").text == "100000100"
        assert cliente.find("statoresidenza").text == "100000100"
        assert cliente.find("statonascita").text == "100000100"

    def test_partenze_empty_when_no_departures(self):
        """<partenze/> must be empty when no guests depart on that day."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        partenze = root.find("movimento/partenze")
        assert partenze is not None
        assert len(list(partenze)) == 0

    def test_arrivi_and_partenze_separate_movements(self):
        """Arrivals and departures must be on separate days.
        
        Checkin: 2026-05-07 → guest in <arrivi>
        Checkout: 2026-05-09 → guest in <partenze>
        """
        arrival = _make_guest(idswh="token-arr", cognome="ROSSI", nome="MARIO")
        departure = _make_guest(idswh="token-dep", cognome="ROSSI", nome="MARIO")
        
        days = [
            _make_day(date(2026, 5, 7), arrivi=[arrival]),
            _make_day(date(2026, 5, 8)),  # overnight - empty
            _make_day(date(2026, 5, 9), partenze=[departure]),
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 9),
        )
        root = _parse(result)
        
        day_map = {m.get("data"): m for m in root.findall("movimento")}
        
        # May 7: arrival only
        assert len(day_map["20260507"].find("arrivi").findall("cliente")) == 1
        assert len(day_map["20260507"].find("partenze").findall("cliente")) == 0
        
        # May 8: empty
        assert len(day_map["20260508"].find("arrivi").findall("cliente")) == 0
        assert len(day_map["20260508"].find("partenze").findall("cliente")) == 0
        
        # May 9: departure only
        assert len(day_map["20260509"].find("arrivi").findall("cliente")) == 0
        assert len(day_map["20260509"].find("partenze").findall("cliente")) == 1

    def test_country_codes_are_9_digits_separate(self):
        """All country/state fields must be separate 9-digit ISTAT codes."""
        guest = _make_guest(
            statonascita="100000100",  # Italy
            statoresidenza="200000004",  # Germany
            cittadinanza="100000100",  # Italy
        )
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        
        for tag in ("cittadinanza", "statoresidenza", "statonascita"):
            code = cliente.find(tag).text
            assert len(code) == 9 and code.isdigit(), (
                f"<{tag}> must be 9 digits, got '{code}'"
            )

    def test_node_ordering_matches_specification(self):
        """Node order inside <cliente> must match spec exactly:
        idswh → cognome → nome → sesso → datanascita →
        cittadinanza → statoresidenza → statonascita
        """
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        tags = [child.tag for child in cliente]
        
        expected = [
            "idswh", "cognome", "nome", "sesso", "datanascita",
            "cittadinanza", "statoresidenza", "statonascita"
        ]
        assert tags == expected, f"Expected {expected}, got {tags}"

    def test_empty_days_still_have_struttura(self):
        """Empty days (no arrivals/departures) must still have <movimento> with <struttura>."""
        days = [
            _make_day(date(2026, 5, 7)),  # empty
            _make_day(date(2026, 5, 8)),  # empty
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 8),
        )
        root = _parse(result)
        
        for movimento in root.findall("movimento"):
            struttura = movimento.find("struttura")
            assert struttura is not None, f"struttura missing on {movimento.get('data')}"
            arrivi = movimento.find("arrivi")
            partenze = movimento.find("partenze")
            assert arrivi is not None
            assert partenze is not None

    def test_multiple_guests_same_day(self):
        """Multiple guests arriving on the same day must all appear in <arrivi>."""
        guests = [
            _make_guest(idswh="guest-1", cognome="ROSSI", nome="MARIO"),
            _make_guest(idswh="guest-2", cognome="BIANCHI", nome="LUCIA"),
            _make_guest(idswh="guest-3", cognome="VERDI", nome="ANNA"),
        ]
        days = [_make_day(date(2026, 5, 7), arrivi=guests)]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        clienti = root.findall("movimento/arrivi/cliente")
        assert len(clienti) == 3

    def test_utf8_characters_in_names(self):
        """Names with UTF-8 characters (umlauts, accents) must serialize correctly."""
        guest = _make_guest(cognome="MÜLLER", nome="HANS-JÜRGEN")
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        # Must be valid UTF-8
        encoded = result.encode("utf-8")
        assert isinstance(encoded, bytes)
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente.find("cognome").text == "MÜLLER"
        assert cliente.find("nome").text == "HANS-JÜRGEN"

    def test_full_month_export_31_days(self):
        """Full month export (31 days) must produce 31 <movimento> blocks."""
        days = [
            _make_day(date(2026, 5, 1) + timedelta(days=i))
            for i in range(31)
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        assert len(root.findall("movimento")) == 31

    def test_deterministic_output(self):
        """Serializing the same data twice must produce identical XML."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        
        result1 = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        result2 = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        assert result1 == result2


class TestSoapCompatibility:
    """Validate SOAP envelope compatibility with guest movements."""

    def test_soap_envelope_wraps_xml_payload(self):
        """SOAP mode must embed the full XML payload correctly."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        xml_payload = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        soap = build_soap_envelope(xml_payload)
        
        assert "Envelope" in soap
        assert "Body" in soap
        # Payload is HTML-escaped inside SOAP, check for escaped version
        assert "&lt;movimenti" in soap or "<movimenti" in soap

    def test_soap_valid_utf8(self):
        """SOAP envelope must be valid UTF-8 XML."""
        guest = _make_guest(cognome="MÜLLER")
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        xml_payload = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        soap = build_soap_envelope(xml_payload)
        encoded = soap.encode("utf-8")
        assert isinstance(encoded, bytes)
        _parse(soap)

    def test_soap_preserves_guest_data(self):
        """SOAP envelope must preserve all guest fields without data loss."""
        guest = _make_guest(
            idswh="c3c4a9a2-12ab",
            cognome="ROSSI",
            nome="MARIO",
        )
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        xml_payload = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        soap = build_soap_envelope(xml_payload)
        
        # Guest data must be present in SOAP (HTML-escaped in payload element)
        # Check for escaped versions
        assert "&lt;idswh&gt;c3c4a9a2-12ab&lt;/idswh&gt;" in soap or \
               "<idswh>c3c4a9a2-12ab</idswh>" in soap
        assert "&lt;cognome&gt;ROSSI&lt;/cognome&gt;" in soap or \
               "<cognome>ROSSI</cognome>" in soap
        assert "&lt;nome&gt;MARIO&lt;/nome&gt;" in soap or \
               "<nome>MARIO</nome>" in soap


class TestClientSpecificationCompliance:
    """Validate all client requirements are met."""

    def test_requirement_1_utf8(self):
        """XML must be valid UTF-8."""
        guest = _make_guest(cognome="MÜLLER", nome="HANS-JÜRGEN")
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        result.encode("utf-8")  # Must not raise
        _parse(result)  # Must be valid XML

    def test_requirement_3_dates_aaaammgg(self):
        """Dates must be AAAAMMGG format."""
        guest = _make_guest(dob="19830209")
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        
        # Check movement date
        movimento = root.find("movimento")
        data_attr = movimento.get("data")
        assert len(data_attr) == 8 and data_attr.isdigit()
        
        # Check birth date
        cliente = root.find("movimento/arrivi/cliente")
        datanascita = cliente.find("datanascita").text
        assert datanascita == "19830209"

    def test_requirement_4_all_guest_fields_present(self):
        """Every guest movement must contain all 8 mandatory fields."""
        guest = _make_guest()
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        
        mandatory_fields = [
            "idswh", "cognome", "nome", "datanascita",
            "sesso", "cittadinanza", "statoresidenza", "statonascita"
        ]
        for field in mandatory_fields:
            el = cliente.find(field)
            assert el is not None, f"<{field}> missing"
            assert el.text is not None and el.text.strip() != "", f"<{field}> empty"

    def test_requirement_5_country_codes_separate_9_digits(self):
        """All country/state fields must be separate 9-digit ISTAT codes."""
        guest = _make_guest(
            statonascita="100000100",
            statoresidenza="200000004",
            cittadinanza="200000001",
        )
        days = [_make_day(date(2026, 5, 7), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 1),
            end_date=date(2026, 5, 31),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        
        for tag in ("cittadinanza", "statoresidenza", "statonascita"):
            code = cliente.find(tag).text
            assert len(code) == 9, f"<{tag}> must be 9 digits, got {len(code)}"
            assert code.isdigit(), f"<{tag}> must be numeric, got '{code}'"

    def test_requirement_6_arrivals_departures_separate(self):
        """Arrivals and departures must be separate movements."""
        arrival = _make_guest(idswh="arr-1")
        departure = _make_guest(idswh="dep-1")
        
        days = [
            _make_day(date(2026, 5, 7), arrivi=[arrival]),
            _make_day(date(2026, 5, 9), partenze=[departure]),
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 9),
        )
        root = _parse(result)
        day_map = {m.get("data"): m for m in root.findall("movimento")}
        
        # Arrival day
        assert len(day_map["20260507"].find("arrivi").findall("cliente")) == 1
        assert len(day_map["20260507"].find("partenze").findall("cliente")) == 0
        
        # Departure day
        assert len(day_map["20260509"].find("arrivi").findall("cliente")) == 0
        assert len(day_map["20260509"].find("partenze").findall("cliente")) == 1

    def test_requirement_8_every_day_has_struttura(self):
        """Every day must include: struttura, apertura, camereoccupate, cameredisponibili, lettidisponibili."""
        days = [
            _make_day(date(2026, 5, 7)),
            _make_day(date(2026, 5, 8)),
            _make_day(date(2026, 5, 9)),
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 9),
        )
        root = _parse(result)
        
        for movimento in root.findall("movimento"):
            struttura = movimento.find("struttura")
            assert struttura is not None
            assert struttura.find("codice") is not None
            assert struttura.find("apertura") is not None
            assert struttura.find("camereoccupate") is not None
            assert struttura.find("cameredisponibili") is not None
            assert struttura.find("lettidisponibili") is not None
