"""Tests for C59 XML serializer.

Tests cover:
1. Italian row serialization
2. Foreign row serialization
3. Multiple rows
4. XML namespace presence
5. XML declaration presence
6. Correct root attributes
7. Correct mensile attributes
8. Correct giornaliero attributes
9. Correct rigac59 attributes
10. Deterministic ordering
11. UTF-8 output
12. Invalid residenza validation
13. Invalid nazione validation
14. Missing structure_code
15. Negative counts
16. Empty rows handling
17. Optional diurni handling
18. Proper omission rules
19. Pretty formatting
20. XSD-compatible structure
"""

from datetime import date

from django.test import TestCase
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.models.c59_payload import IstatC59RowPayload
from istat.xml_export.serializers.c59_xml_serializer import build_c59_xml_document


class C59XmlSerializerTestCase(TestCase):
    """Test suite for C59 XML serializer."""

    def test_italian_row_serialization(self):
        """Test that Italian resident rows are serialized correctly."""
        rows = [
            IstatC59RowPayload(
                nazione="i",
                residenza="015",
                arrivi=5,
                partenze=2,
                presenze=10,
                diurni=0,
            ),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Verify Italian row attributes
        self.assertIn('nazione="i"', xml)
        self.assertIn('residenza="015"', xml)
        self.assertIn('arrivi="5"', xml)
        self.assertIn('partenze="2"', xml)
        self.assertIn('presenze="10"', xml)
        self.assertIn('diurni="0"', xml)

    def test_foreign_row_serialization(self):
        """Test that foreign resident rows are serialized correctly."""
        rows = [
            IstatC59RowPayload(
                nazione="e",
                residenza="664",
                arrivi=1,
                partenze=0,
                presenze=1,
                diurni=0,
            ),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Verify foreign row attributes
        self.assertIn('nazione="e"', xml)
        self.assertIn('residenza="664"', xml)
        self.assertIn('arrivi="1"', xml)
        self.assertIn('partenze="0"', xml)
        self.assertIn('presenze="1"', xml)

    def test_multiple_rows(self):
        """Test that multiple rows are serialized correctly."""
        rows = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=5, partenze=2, presenze=10),
            IstatC59RowPayload(nazione="e", residenza="664", arrivi=1, partenze=0, presenze=1),
            IstatC59RowPayload(nazione="i", residenza="058", arrivi=3, partenze=1, presenze=5),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # All rows should be present
        self.assertEqual(xml.count("<rim:rigac59"), 3)
        self.assertIn('residenza="015"', xml)
        self.assertIn('residenza="664"', xml)
        self.assertIn('residenza="058"', xml)

    def test_xml_namespace_presence(self):
        """Test that XML namespace is correctly declared."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Namespace should be declared with rim prefix
        self.assertIn('xmlns:rim="http://www.regione.liguria.it/turismo/rimovcli"', xml)
        # Elements should use namespace prefix
        self.assertIn("<rim:c59", xml)
        self.assertIn("<rim:mensile", xml)
        self.assertIn("<rim:giornaliero", xml)

    def test_xml_declaration_presence(self):
        """Test that XML declaration is present."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # XML declaration should be present
        self.assertIn('<?xml version="1.0" encoding="utf-8"?>', xml)

    def test_correct_root_attributes(self):
        """Test that root element has correct attributes."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Root attributes
        self.assertIn('idstruttura="HOTEL123"', xml)
        self.assertIn('data="20260501"', xml)

    def test_correct_mensile_attributes(self):
        """Test that mensile node has correct attributes."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
            available_rooms=20,
            available_beds=45,
            software_name="Aimantis",
        )
        
        # Mensile attributes
        self.assertIn('numcameredisp="20"', xml)
        self.assertIn('numlettidisp="45"', xml)
        self.assertIn('softwaregestionale="Aimantis"', xml)

    def test_correct_giornaliero_attributes(self):
        """Test that giornaliero node has correct attributes."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
            occupied_rooms=12,
        )
        
        # Giornaliero attributes
        self.assertIn('numcamereoccupate="12"', xml)

    def test_correct_rigac59_attributes(self):
        """Test that rigac59 nodes have all required attributes."""
        rows = [
            IstatC59RowPayload(
                nazione="i",
                residenza="015",
                arrivi=5,
                partenze=2,
                presenze=10,
                diurni=1,
            ),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # All attributes should be present
        self.assertIn('nazione="i"', xml)
        self.assertIn('residenza="015"', xml)
        self.assertIn('arrivi="5"', xml)
        self.assertIn('partenze="2"', xml)
        self.assertIn('presenze="10"', xml)
        self.assertIn('diurni="1"', xml)

    def test_deterministic_ordering(self):
        """Test that rows maintain deterministic ordering (input order preserved)."""
        rows = [
            IstatC59RowPayload(nazione="i", residenza="058", arrivi=3, partenze=1, presenze=5),
            IstatC59RowPayload(nazione="e", residenza="664", arrivi=1, partenze=0, presenze=1),
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=5, partenze=2, presenze=10),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Find positions of residenza attributes
        pos_015 = xml.find('residenza="015"')
        pos_058 = xml.find('residenza="058"')
        pos_664 = xml.find('residenza="664"')
        
        # Serializer preserves input order: 058 → 664 → 015
        self.assertLess(pos_058, pos_664)  # 058 comes before 664 (input order)
        self.assertLess(pos_664, pos_015)  # 664 comes before 015 (input order)

    def test_utf8_output(self):
        """Test that output is valid UTF-8."""
        rows = [
            IstatC59RowPayload(
                nazione="i",
                residenza="015",
                arrivi=5,
                partenze=2,
                presenze=10,
            ),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Should be a string (already decoded from UTF-8)
        self.assertIsInstance(xml, str)
        
        # Should be encodable back to UTF-8
        xml.encode("utf-8")

    def test_invalid_residenza_validation(self):
        """Test that invalid residenza raises validation error."""
        # Test non-numeric
        with self.assertRaises(XmlPayloadValidationError) as context:
            build_c59_xml_document(
                structure_code="HOTEL123",
                report_date=date(2026, 5, 1),
                rows=[
                    IstatC59RowPayload(nazione="i", residenza="ABC", arrivi=1, partenze=0, presenze=1),
                ],
            )
        self.assertIn("3 digits", str(context.exception))
        
        # Test wrong length
        with self.assertRaises(XmlPayloadValidationError):
            build_c59_xml_document(
                structure_code="HOTEL123",
                report_date=date(2026, 5, 1),
                rows=[
                    IstatC59RowPayload(nazione="i", residenza="15", arrivi=1, partenze=0, presenze=1),
                ],
            )

    def test_invalid_nazione_validation(self):
        """Test that invalid nazione raises validation error."""
        with self.assertRaises(XmlPayloadValidationError) as context:
            build_c59_xml_document(
                structure_code="HOTEL123",
                report_date=date(2026, 5, 1),
                rows=[
                    IstatC59RowPayload(nazione="x", residenza="015", arrivi=1, partenze=0, presenze=1),
                ],
            )
        self.assertIn("nazione must be 'i' or 'e'", str(context.exception))

    def test_missing_structure_code(self):
        """Test that missing structure_code raises validation error."""
        with self.assertRaises(XmlPayloadValidationError) as context:
            build_c59_xml_document(
                structure_code="",
                report_date=date(2026, 5, 1),
                rows=[],
            )
        self.assertIn("structure_code is required", str(context.exception))
        
        # Test None
        with self.assertRaises(XmlPayloadValidationError):
            build_c59_xml_document(
                structure_code=None,
                report_date=date(2026, 5, 1),
                rows=[],
            )

    def test_negative_counts(self):
        """Test that negative counts raise validation errors."""
        test_cases = [
            {"arrivi": -1, "partenze": 0, "presenze": 0},
            {"arrivi": 0, "partenze": -1, "presenze": 0},
            {"arrivi": 0, "partenze": 0, "presenze": -1},
            {"arrivi": 0, "partenze": 0, "presenze": 0, "diurni": -1},
        ]
        
        for counts in test_cases:
            with self.subTest(counts=counts):
                with self.assertRaises(XmlPayloadValidationError):
                    build_c59_xml_document(
                        structure_code="HOTEL123",
                        report_date=date(2026, 5, 1),
                        rows=[
                            IstatC59RowPayload(
                                nazione="i",
                                residenza="015",
                                **counts,
                            ),
                        ],
                    )

    def test_empty_rows_handling(self):
        """Test that empty rows list is handled correctly."""
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=[],
        )
        
        # Should still generate valid XML structure
        self.assertIn("<?xml", xml)
        self.assertIn("<rim:c59", xml)
        self.assertIn("<rim:mensile", xml)
        self.assertIn("<rim:giornaliero", xml)
        # No rigac59 nodes
        self.assertNotIn("<rim:rigac59", xml)

    def test_optional_diurni_handling(self):
        """Test that diurni is handled correctly when zero or non-zero."""
        # Test with diurni = 0
        rows_zero = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=1, partenze=0, presenze=1, diurni=0),
        ]
        xml_zero = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows_zero,
        )
        self.assertIn('diurni="0"', xml_zero)
        
        # Test with diurni > 0
        rows_positive = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=1, partenze=0, presenze=1, diurni=3),
        ]
        xml_positive = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows_positive,
        )
        self.assertIn('diurni="3"', xml_positive)

    def test_pretty_formatting(self):
        """Test that XML output is pretty-printed."""
        rows = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=1, partenze=0, presenze=1),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        # Should have indentation (multiple lines)
        lines = xml.split('\n')
        self.assertGreater(len(lines), 5, "XML should be multi-line (pretty-printed)")
        
        # Should have indentation whitespace
        indented_lines = [line for line in lines if line.startswith('    ')]
        self.assertGreater(len(indented_lines), 0, "XML should have indented lines")

    def test_xsd_compatible_structure(self):
        """Test that XML structure matches XSD requirements."""
        rows = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=5, partenze=2, presenze=10, diurni=0),
            IstatC59RowPayload(nazione="e", residenza="664", arrivi=1, partenze=0, presenze=1, diurni=0),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
            available_rooms=20,
            available_beds=45,
            occupied_rooms=12,
        )
        
        # Verify complete structure
        self.assertIn('<?xml version="1.0" encoding="utf-8"?>', xml)
        self.assertIn('xmlns:rim="http://www.regione.liguria.it/turismo/rimovcli"', xml)
        self.assertIn('idstruttura="HOTEL123"', xml)
        self.assertIn('data="20260501"', xml)
        self.assertIn('numcameredisp="20"', xml)
        self.assertIn('numlettidisp="45"', xml)
        self.assertIn('softwaregestionale="Aimantis"', xml)
        self.assertIn('numcamereoccupate="12"', xml)
        self.assertEqual(xml.count("<rim:rigac59"), 2)

    def test_default_software_name(self):
        """Test that default software name is 'Aimantis'."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
        )
        
        self.assertIn('softwaregestionale="Aimantis"', xml)

    def test_custom_software_name(self):
        """Test that custom software name is used."""
        rows = []
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
            software_name="CustomSoftware",
        )
        
        self.assertIn('softwaregestionale="CustomSoftware"', xml)

    def test_missing_report_date(self):
        """Test that missing report_date raises validation error."""
        with self.assertRaises(XmlPayloadValidationError) as context:
            build_c59_xml_document(
                structure_code="HOTEL123",
                report_date=None,
                rows=[],
            )
        self.assertIn("report_date is required", str(context.exception))

    def test_numeric_values_as_strings(self):
        """Test that all numeric values are serialized as strings."""
        rows = [
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=5, partenze=2, presenze=10, diurni=0),
        ]
        
        xml = build_c59_xml_document(
            structure_code="HOTEL123",
            report_date=date(2026, 5, 1),
            rows=rows,
            available_rooms=20,
            available_beds=45,
            occupied_rooms=12,
        )
        
        # All numeric values should be in quotes (strings)
        self.assertIn('arrivi="5"', xml)
        self.assertIn('partenze="2"', xml)
        self.assertIn('presenze="10"', xml)
        self.assertIn('numcameredisp="20"', xml)
        self.assertIn('numlettidisp="45"', xml)
        self.assertIn('numcamereoccupate="12"', xml)

    def test_end_to_end_with_aggregation_payloads(self):
        """Test complete flow: create payloads → serialize to XML."""
        # Simulate payloads from aggregation builder
        rows = [
            IstatC59RowPayload(nazione="e", residenza="400", arrivi=3, partenze=1, presenze=5),  # US
            IstatC59RowPayload(nazione="e", residenza="664", arrivi=2, partenze=0, presenze=2),  # IN
            IstatC59RowPayload(nazione="i", residenza="009", arrivi=5, partenze=2, presenze=8),  # SV
            IstatC59RowPayload(nazione="i", residenza="010", arrivi=4, partenze=1, presenze=6),  # GE
            IstatC59RowPayload(nazione="i", residenza="015", arrivi=10, partenze=3, presenze=15),  # MI
        ]
        
        xml = build_c59_xml_document(
            structure_code="LIGURIA001",
            report_date=date(2026, 5, 1),
            rows=rows,
            available_rooms=50,
            available_beds=100,
            occupied_rooms=35,
            software_name="Aimantis",
        )
        
        # Verify complete document
        self.assertIn('<?xml version="1.0" encoding="utf-8"?>', xml)
        self.assertIn('idstruttura="LIGURIA001"', xml)
        self.assertIn('data="20260501"', xml)
        self.assertIn('numcameredisp="50"', xml)
        self.assertIn('numlettidisp="100"', xml)
        self.assertIn('numcamereoccupate="35"', xml)
        self.assertEqual(xml.count("<rim:rigac59"), 5)
        
        # Verify all rows are present
        self.assertIn('residenza="400"', xml)
        self.assertIn('residenza="664"', xml)
        self.assertIn('residenza="009"', xml)
        self.assertIn('residenza="010"', xml)
        self.assertIn('residenza="015"', xml)
