"""Tests for Ross1000 XML movement serializer.

Validates:
- UTF-8 encoding and XML declaration
- Root element structure
- struttura block on every day (including empty days)
- Arrivals and departures in separate blocks
- Date format AAAAMMGG
- 9-digit country codes
- Empty movement days still produce valid XML
- Multiple days produce multiple <movimento> blocks
"""

from __future__ import annotations

from datetime import date
from xml.etree import ElementTree

import pytest

from istat.ross1000.models.movement_payload import (
    Ross1000GuestPayload,
    Ross1000MovementDayPayload,
    Ross1000StrutturaPayload,
)
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):
    return Ross1000StrutturaPayload(
        codice=codice,
        apertura=1,
        camere_occupate=occupied,
        camere_disponibili=available,
        letti_disponibili=beds,
    )


def _make_guest(
    idswh="abc123",
    cognome="ROSSI",
    nome="MARIO",
    sesso="M",
    dob="19830209",
    statonascita="100000100",
    statoresidenza="100000100",
    cittadinanza="100000100",
):
    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,
):
    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"))


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestSerializeMovementDaysXml:

    def test_returns_string(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        assert isinstance(result, str)

    def test_xml_declaration_present(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        assert result.startswith("<?xml")

    def test_utf8_encoding_declared(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        assert "UTF-8" in result or "utf-8" in result.lower()

    def test_root_element_is_movimenti(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        assert root.tag == "movimenti"

    def test_root_has_struttura_attribute(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        assert root.get("struttura") == "058091-CAV-00001"

    def test_root_has_dal_and_al_attributes(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 30),
        )
        root = _parse(result)
        assert root.get("dal") == "20260401"
        assert root.get("al") == "20260430"

    def test_one_movimento_per_day(self):
        days = [
            _make_day(date(2026, 4, 1)),
            _make_day(date(2026, 4, 2)),
            _make_day(date(2026, 4, 3)),
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 3),
        )
        root = _parse(result)
        movimenti = root.findall("movimento")
        assert len(movimenti) == 3

    def test_movimento_data_attribute_is_aaaammgg(self):
        days = [_make_day(date(2026, 4, 4))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 4),
            end_date=date(2026, 4, 4),
        )
        root = _parse(result)
        movimento = root.find("movimento")
        assert movimento.get("data") == "20260404"

    def test_struttura_block_present_on_every_day(self):
        days = [
            _make_day(date(2026, 4, 1)),
            _make_day(date(2026, 4, 2)),
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 2),
        )
        root = _parse(result)
        for movimento in root.findall("movimento"):
            struttura = movimento.find("struttura")
            assert struttura is not None, "struttura block missing on a movement day"

    def test_struttura_fields_correct(self):
        struttura = _make_struttura(
            codice="058091-CAV-00001",
            occupied=2,
            available=6,
            beds=20,
        )
        days = [_make_day(date(2026, 4, 1), struttura=struttura)]
        result = serialize_movement_days_xml(
            days,
            structure_code="058091-CAV-00001",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        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 == "2"
        assert s.find("cameredisponibili").text == "6"
        assert s.find("lettidisponibili").text == "20"

    def test_empty_day_has_empty_arrivi_and_partenze(self):
        days = [_make_day(date(2026, 4, 1))]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        movimento = root.find("movimento")
        arrivi = movimento.find("arrivi")
        partenze = movimento.find("partenze")
        assert arrivi is not None
        assert partenze is not None
        assert len(list(arrivi)) == 0
        assert len(list(partenze)) == 0

    def test_arrival_guest_in_arrivi_block(self):
        guest = _make_guest(idswh="abc123", cognome="ROSSI", nome="MARIO")
        days = [_make_day(date(2026, 4, 4), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 4),
            end_date=date(2026, 4, 4),
        )
        root = _parse(result)
        arrivi = root.find("movimento/arrivi")
        clienti = arrivi.findall("cliente")
        assert len(clienti) == 1
        assert clienti[0].find("idswh").text == "abc123"
        assert clienti[0].find("cognome").text == "ROSSI"
        assert clienti[0].find("nome").text == "MARIO"

    def test_departure_guest_in_partenze_block(self):
        guest = _make_guest(idswh="xyz789", cognome="BIANCHI", nome="LUCIA")
        days = [_make_day(date(2026, 4, 6), partenze=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 6),
            end_date=date(2026, 4, 6),
        )
        root = _parse(result)
        partenze = root.find("movimento/partenze")
        clienti = partenze.findall("cliente")
        assert len(clienti) == 1
        assert clienti[0].find("idswh").text == "xyz789"

    def test_arrivals_and_departures_are_separate(self):
        arrival = _make_guest(idswh="arr1", cognome="ROSSI", nome="MARIO")
        departure = _make_guest(idswh="dep1", cognome="VERDI", nome="ANNA")
        days = [_make_day(date(2026, 4, 5), arrivi=[arrival], partenze=[departure])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 5),
            end_date=date(2026, 4, 5),
        )
        root = _parse(result)
        arrivi_ids = [c.find("idswh").text for c in root.findall("movimento/arrivi/cliente")]
        partenze_ids = [c.find("idswh").text for c in root.findall("movimento/partenze/cliente")]
        assert "arr1" in arrivi_ids
        assert "dep1" in partenze_ids
        assert "dep1" not in arrivi_ids
        assert "arr1" not in partenze_ids

    def test_guest_country_codes_are_9_digits(self):
        guest = _make_guest(
            statonascita="100000100",
            statoresidenza="200000004",
            cittadinanza="100000100",
        )
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        for tag in ("statonascita", "statoresidenza", "cittadinanza"):
            code = cliente.find(tag).text
            assert len(code) == 9 and code.isdigit(), (
                f"<{tag}> value '{code}' is not 9 digits"
            )

    def test_guest_fields_are_separate_xml_elements(self):
        """All mandatory fields must be present as separate XML elements."""
        guest = _make_guest()
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        # All mandatory fields must be present as separate elements
        for tag in ("idswh", "cognome", "nome", "sesso", "datanascita",
                    "cittadinanza", "statoresidenza", "statonascita"):
            el = cliente.find(tag)
            assert el is not None, f"<{tag}> element missing from <cliente>"
            assert el.text is not None and el.text.strip() != "", (
                f"<{tag}> element is empty"
            )

    def test_sesso_element_present_in_cliente(self):
        guest = _make_guest(sesso="M")
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente.find("sesso").text == "M"

    def test_sesso_female_serialized_correctly(self):
        guest = _make_guest(sesso="F")
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente.find("sesso").text == "F"

    def test_node_order_matches_spec(self):
        """Node order: idswh, cognome, nome, sesso, datanascita,
        cittadinanza, statoresidenza, statonascita."""
        guest = _make_guest()
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        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[:len(expected)] == expected

    def test_optional_fields_omitted_when_none(self):
        guest = _make_guest()  # no optional fields
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente.find("comuneresidenza") is None
        assert cliente.find("provinciaresidenza") is None
        assert cliente.find("comunenascita") is None

    def test_optional_fields_present_when_set(self):
        guest = Ross1000GuestPayload(
            idswh="abc",
            cognome="ROSSI",
            nome="MARIO",
            sesso="M",
            datanascita="19830209",
            cittadinanza="100000100",
            statoresidenza="100000100",
            statonascita="100000100",
            comune_residenza="Roma",
            provincia_residenza="RM",
            comune_nascita="Napoli",
        )
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        root = _parse(result)
        cliente = root.find("movimento/arrivi/cliente")
        assert cliente.find("comuneresidenza").text == "Roma"
        assert cliente.find("provinciaresidenza").text == "RM"
        assert cliente.find("comunenascita").text == "Napoli"

    def test_empty_days_list_produces_valid_xml(self):
        result = serialize_movement_days_xml(
            [],
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 30),
        )
        root = _parse(result)
        assert root.tag == "movimenti"
        assert len(root.findall("movimento")) == 0

    def test_30_day_range_produces_30_movimento_blocks(self):
        days = [
            _make_day(date(2026, 4, 1) + __import__("datetime").timedelta(days=i))
            for i in range(30)
        ]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 30),
        )
        root = _parse(result)
        assert len(root.findall("movimento")) == 30

    def test_xml_is_valid_utf8(self):
        guest = _make_guest(cognome="MÜLLER", nome="HANS")
        days = [_make_day(date(2026, 4, 1), arrivi=[guest])]
        result = serialize_movement_days_xml(
            days,
            structure_code="TEST",
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 1),
        )
        encoded = result.encode("utf-8")
        assert isinstance(encoded, bytes)
        _parse(encoded.decode("utf-8"))
