"""Tests for C59 multi-day ZIP export.

Covers:
- Single-day range returns ZIP with exactly 1 XML
- Multi-day range returns correct file count
- Empty days produce valid XML (no rigac59 rows)
- Correct per-day filenames (C59_YYYYMMDD.xml)
- Correct ZIP archive filename (C59_YYYYMMDD_YYYYMMDD.zip)
- Inclusive date iteration (both start and end days included)
- Booking overlap correctness across day boundaries
- ZIP is valid and extractable
- Content-Type is application/zip
- Existing single-day generate_c59_xml_export is unaffected
- TXT export unaffected (no cross-contamination)
- Invalid inputs raise XmlPayloadValidationError
- Missing ISTAT code raises XmlPayloadValidationError
- Multiple property types / bed counts preserved per day
"""

from __future__ import annotations

import io
import zipfile
from datetime import date, timedelta

from bookings.models import Booking
from django.test import TestCase
from guests.models import Guest
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.files.naming import (
    build_c59_daily_xml_filename,
    build_c59_zip_filename,
)
from istat.xml_export.models.zip_export_result import (
    ISTAT_ZIP_CONTENT_TYPE,
    IstatZipExportResult,
)
from istat.xml_export.services.xml_export_service import (
    generate_c59_xml_export,
    generate_c59_zip_export,
)
from properties.models import Property, PropertyType
from structures.models import Structure


# ── Helpers ──────────────────────────────────────────────────────────────────


def _open_zip(result: IstatZipExportResult) -> zipfile.ZipFile:
    """Return an open ZipFile from the result bytes (caller must close)."""
    return zipfile.ZipFile(io.BytesIO(result.content), "r")


def _xml_names_in_zip(result: IstatZipExportResult) -> list[str]:
    with _open_zip(result) as zf:
        return sorted(zf.namelist())


def _read_xml_from_zip(result: IstatZipExportResult, filename: str) -> str:
    with _open_zip(result) as zf:
        return zf.read(filename).decode("utf-8")


# ── Base test setup ───────────────────────────────────────────────────────────


class C59ZipExportBaseTestCase(TestCase):
    """Shared setUp for C59 ZIP export tests."""

    def setUp(self):
        from django.contrib.auth.models import User

        self.user = User.objects.create_user(username="zipexportuser", password="pass123")
        self.structure = Structure.objects.create(
            user=self.user,
            name="Hotel Liguria ZIP",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
            istat_code="LIG_ZIP",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Standard Room",
            max_guests=2,
            num_beds=2,
            num_sofa_beds=0,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 101",
            availability=Property.Availability.AVAILABLE,
        )

    def _make_booking(
        self,
        *,
        check_in: date,
        check_out: date,
        country: str = "IT",
        province: str | None = "MI",
        is_checked_in: bool = True,
    ) -> Booking:
        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,
            is_checked_in=is_checked_in,
        )
        Guest.objects.create(
            booking=booking,
            full_name="Test Guest",
            is_main_guest=True,
            guest_type="16",
            gender="male",
            date_of_birth=date(1990, 1, 1),
            country_of_birth=country,
            nationality=country,
            country=country,
            extra_data={"province": province} if country == "IT" and province else {},
        )
        return booking


# ── Naming helpers ────────────────────────────────────────────────────────────


class C59NamingTests(TestCase):
    def test_daily_xml_filename_format(self):
        self.assertEqual(
            build_c59_daily_xml_filename(report_date=date(2026, 6, 1)),
            "C59_20260601.xml",
        )
        self.assertEqual(
            build_c59_daily_xml_filename(report_date=date(2026, 12, 31)),
            "C59_20261231.xml",
        )

    def test_zip_filename_format(self):
        self.assertEqual(
            build_c59_zip_filename(
                start_date=date(2026, 6, 1),
                end_date=date(2026, 6, 30),
            ),
            "C59_20260601_20260630.zip",
        )

    def test_zip_filename_single_day(self):
        self.assertEqual(
            build_c59_zip_filename(
                start_date=date(2026, 6, 15),
                end_date=date(2026, 6, 15),
            ),
            "C59_20260615_20260615.zip",
        )

    def test_zip_filename_rejects_inverted_range(self):
        with self.assertRaises(XmlPayloadValidationError):
            build_c59_zip_filename(
                start_date=date(2026, 6, 30),
                end_date=date(2026, 6, 1),
            )

    def test_daily_filename_rejects_none(self):
        with self.assertRaises(XmlPayloadValidationError):
            build_c59_daily_xml_filename(report_date=None)


# ── Core ZIP generation ───────────────────────────────────────────────────────


class C59ZipExportSingleDayTests(C59ZipExportBaseTestCase):
    """Single-day range: start == end → ZIP with exactly 1 XML."""

    def test_single_day_returns_zip_result(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertIsInstance(result, IstatZipExportResult)

    def test_single_day_content_type_is_zip(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertEqual(result.content_type, ISTAT_ZIP_CONTENT_TYPE)

    def test_single_day_zip_contains_exactly_one_file(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertEqual(result.file_count, 1)
        self.assertEqual(len(_xml_names_in_zip(result)), 1)

    def test_single_day_correct_filename_inside_zip(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertIn("C59_20260615.xml", _xml_names_in_zip(result))

    def test_single_day_zip_archive_filename(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertEqual(result.filename, "C59_20260615_20260615.zip")

    def test_single_day_zip_is_valid_and_extractable(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertTrue(zipfile.is_zipfile(io.BytesIO(result.content)))

    def test_single_day_xml_content_is_valid_c59(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        xml = _read_xml_from_zip(result, "C59_20260615.xml")
        self.assertTrue(xml.startswith("<?xml"))
        self.assertIn('xmlns:rim="http://www.regione.liguria.it/turismo/rimovcli"', xml)
        self.assertIn('idstruttura="LIG_ZIP"', xml)

    def test_single_day_byte_size_matches_content(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertEqual(result.byte_size, len(result.content))


class C59ZipExportMultiDayTests(C59ZipExportBaseTestCase):
    """Multi-day range: correct file count, filenames, and content."""

    def test_three_day_range_produces_three_files(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        self.assertEqual(result.file_count, 3)
        self.assertEqual(len(_xml_names_in_zip(result)), 3)

    def test_three_day_range_correct_filenames(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        names = _xml_names_in_zip(result)
        self.assertIn("C59_20260601.xml", names)
        self.assertIn("C59_20260602.xml", names)
        self.assertIn("C59_20260603.xml", names)

    def test_full_month_produces_thirty_files(self):
        """June 2026 has 30 days → 30 XML files."""
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 30),
        )
        self.assertEqual(result.file_count, 30)
        self.assertEqual(len(_xml_names_in_zip(result)), 30)

    def test_full_month_zip_filename(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 30),
        )
        self.assertEqual(result.filename, "C59_20260601_20260630.zip")

    def test_inclusive_iteration_both_endpoints_present(self):
        """start_date and end_date must both appear in the ZIP."""
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 30),
        )
        names = _xml_names_in_zip(result)
        self.assertIn("C59_20260601.xml", names)
        self.assertIn("C59_20260630.xml", names)

    def test_no_off_by_one_at_month_boundary(self):
        """Verify exact count for a known month length."""
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )
        self.assertEqual(result.file_count, 31)


# ── Empty days ────────────────────────────────────────────────────────────────


class C59ZipExportEmptyDaysTests(C59ZipExportBaseTestCase):
    """Days with no activity must still produce a valid XML file."""

    def test_empty_day_produces_valid_xml(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 10),
            end_date=date(2026, 6, 10),
        )
        xml = _read_xml_from_zip(result, "C59_20260610.xml")
        self.assertTrue(xml.startswith("<?xml"))
        self.assertIn("<rim:c59", xml)
        self.assertIn("<rim:giornaliero", xml)

    def test_empty_day_has_no_rigac59_rows(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 10),
            end_date=date(2026, 6, 10),
        )
        xml = _read_xml_from_zip(result, "C59_20260610.xml")
        self.assertNotIn("<rim:rigac59", xml)

    def test_empty_day_has_zero_occupied_rooms(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 10),
            end_date=date(2026, 6, 10),
        )
        xml = _read_xml_from_zip(result, "C59_20260610.xml")
        self.assertIn('numcamereoccupate="0"', xml)

    def test_all_days_empty_still_produces_correct_count(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        self.assertEqual(result.file_count, 3)
        for day in ["C59_20260601.xml", "C59_20260602.xml", "C59_20260603.xml"]:
            xml = _read_xml_from_zip(result, day)
            self.assertIn("<rim:c59", xml)
            self.assertNotIn("<rim:rigac59", xml)


# ── Booking overlap correctness ───────────────────────────────────────────────


class C59ZipExportBookingOverlapTests(C59ZipExportBaseTestCase):
    """Per-day aggregation must follow check_in <= day < check_out."""

    def test_arrival_day_has_arrivi_row(self):
        """Booking arriving Jun 4 → Jun 4 XML must have arrivi=1."""
        self._make_booking(check_in=date(2026, 6, 4), check_out=date(2026, 6, 6))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 4),
            end_date=date(2026, 6, 6),
        )
        xml_jun4 = _read_xml_from_zip(result, "C59_20260604.xml")
        self.assertIn('arrivi="1"', xml_jun4)

    def test_departure_day_has_partenze_row(self):
        """Booking departing Jun 6 → Jun 6 XML must have partenze=1."""
        self._make_booking(check_in=date(2026, 6, 4), check_out=date(2026, 6, 6))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 4),
            end_date=date(2026, 6, 6),
        )
        xml_jun6 = _read_xml_from_zip(result, "C59_20260606.xml")
        self.assertIn('partenze="1"', xml_jun6)

    def test_middle_day_has_presenze_only(self):
        """Jun 5 (middle night) → presenze=1, arrivi=0, partenze=0."""
        self._make_booking(check_in=date(2026, 6, 4), check_out=date(2026, 6, 6))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 4),
            end_date=date(2026, 6, 6),
        )
        xml_jun5 = _read_xml_from_zip(result, "C59_20260605.xml")
        self.assertIn('presenze="1"', xml_jun5)
        self.assertIn('arrivi="0"', xml_jun5)
        self.assertIn('partenze="0"', xml_jun5)

    def test_booking_outside_range_not_included(self):
        """A booking entirely outside the export range must not appear."""
        self._make_booking(check_in=date(2026, 7, 1), check_out=date(2026, 7, 3))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        for day in ["C59_20260601.xml", "C59_20260602.xml", "C59_20260603.xml"]:
            xml = _read_xml_from_zip(result, day)
            self.assertNotIn("<rim:rigac59", xml)

    def test_non_checked_in_booking_excluded(self):
        """Non-checked-in bookings must not appear in any daily XML."""
        self._make_booking(
            check_in=date(2026, 6, 1),
            check_out=date(2026, 6, 3),
            is_checked_in=False,
        )
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        for day in ["C59_20260601.xml", "C59_20260602.xml", "C59_20260603.xml"]:
            xml = _read_xml_from_zip(result, day)
            self.assertNotIn("<rim:rigac59", xml)

    def test_multiple_bookings_aggregated_per_day(self):
        """Two bookings present on the same day → presenze=2."""
        self._make_booking(check_in=date(2026, 6, 1), check_out=date(2026, 6, 3))
        self._make_booking(check_in=date(2026, 6, 1), check_out=date(2026, 6, 3))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 2),
            end_date=date(2026, 6, 2),
        )
        xml = _read_xml_from_zip(result, "C59_20260602.xml")
        self.assertIn('presenze="2"', xml)


# ── Capacity metrics ──────────────────────────────────────────────────────────


class C59ZipExportCapacityTests(C59ZipExportBaseTestCase):
    """Room and bed counts must appear correctly in each daily XML."""

    def test_available_rooms_in_mensile(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 1),
        )
        xml = _read_xml_from_zip(result, "C59_20260601.xml")
        # 1 available room created in setUp
        self.assertIn('numcameredisp="1"', xml)

    def test_available_beds_in_mensile(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 1),
        )
        xml = _read_xml_from_zip(result, "C59_20260601.xml")
        # 1 room × 2 beds = 2
        self.assertIn('numlettidisp="2"', xml)

    def test_occupied_rooms_zero_on_empty_day(self):
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 1),
        )
        xml = _read_xml_from_zip(result, "C59_20260601.xml")
        self.assertIn('numcamereoccupate="0"', xml)

    def test_occupied_rooms_nonzero_on_active_day(self):
        self._make_booking(check_in=date(2026, 6, 1), check_out=date(2026, 6, 3))
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 1),
        )
        xml = _read_xml_from_zip(result, "C59_20260601.xml")
        self.assertIn('numcamereoccupate="1"', xml)

    def test_capacity_metrics_consistent_across_all_days(self):
        """numcameredisp and numlettidisp must be the same on every day."""
        result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        for day in ["C59_20260601.xml", "C59_20260602.xml", "C59_20260603.xml"]:
            xml = _read_xml_from_zip(result, day)
            self.assertIn('numcameredisp="1"', xml)
            self.assertIn('numlettidisp="2"', xml)


# ── Validation / error handling ───────────────────────────────────────────────


class C59ZipExportValidationTests(C59ZipExportBaseTestCase):
    def test_inverted_date_range_raises(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_zip_export(
                structure_id=self.structure.id,
                start_date=date(2026, 6, 30),
                end_date=date(2026, 6, 1),
            )

    def test_none_start_date_raises(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_zip_export(
                structure_id=self.structure.id,
                start_date=None,
                end_date=date(2026, 6, 30),
            )

    def test_none_end_date_raises(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_zip_export(
                structure_id=self.structure.id,
                start_date=date(2026, 6, 1),
                end_date=None,
            )

    def test_missing_structure_raises(self):
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_zip_export(
                structure_id=999999,
                start_date=date(2026, 6, 1),
                end_date=date(2026, 6, 3),
            )

    def test_missing_istat_code_raises(self):
        self.structure.istat_code = None
        self.structure.save(update_fields=["istat_code"])
        with self.assertRaises(XmlPayloadValidationError) as ctx:
            generate_c59_zip_export(
                structure_id=self.structure.id,
                start_date=date(2026, 6, 1),
                end_date=date(2026, 6, 3),
            )
        self.assertIn("missing ISTAT structure code", str(ctx.exception))

    def test_blank_istat_code_raises(self):
        self.structure.istat_code = "   "
        self.structure.save(update_fields=["istat_code"])
        with self.assertRaises(XmlPayloadValidationError):
            generate_c59_zip_export(
                structure_id=self.structure.id,
                start_date=date(2026, 6, 1),
                end_date=date(2026, 6, 1),
            )


# ── Backward compatibility ────────────────────────────────────────────────────


class C59ZipExportBackwardCompatTests(C59ZipExportBaseTestCase):
    """Existing single-day generate_c59_xml_export must be unaffected."""

    def test_single_day_xml_export_still_works(self):
        from istat.xml_export.models.export_result import IstatXmlExportResult

        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 6, 15),
        )
        self.assertIsInstance(result, IstatXmlExportResult)
        self.assertTrue(result.content.startswith("<?xml"))
        self.assertIn("<rim:c59", result.content)

    def test_single_day_xml_export_filename_unchanged(self):
        result = generate_c59_xml_export(
            structure_id=self.structure.id,
            report_date=date(2026, 6, 15),
        )
        self.assertEqual(
            result.filename,
            f"istat_c59_{self.structure.id}_2026_06_15.xml",
        )

    def test_zip_result_is_not_xml_result(self):
        from istat.xml_export.models.export_result import IstatXmlExportResult

        zip_result = generate_c59_zip_export(
            structure_id=self.structure.id,
            start_date=date(2026, 6, 15),
            end_date=date(2026, 6, 15),
        )
        self.assertNotIsInstance(zip_result, IstatXmlExportResult)
        self.assertIsInstance(zip_result, IstatZipExportResult)


# ── Structure isolation ───────────────────────────────────────────────────────


class C59ZipExportStructureIsolationTests(TestCase):
    """Bookings from other structures must not bleed into the export."""

    def setUp(self):
        from django.contrib.auth.models import User

        self.user = User.objects.create_user(username="isolationuser", password="pass123")

        self.structure_a = Structure.objects.create(
            user=self.user,
            name="Structure A",
            istat_code="ISO_A",
        )
        self.structure_b = Structure.objects.create(
            user=self.user,
            name="Structure B",
            istat_code="ISO_B",
        )

        pt_a = PropertyType.objects.create(
            structure=self.structure_a, name="Room A", max_guests=2, num_beds=1
        )
        pt_b = PropertyType.objects.create(
            structure=self.structure_b, name="Room B", max_guests=2, num_beds=1
        )
        prop_a = Property.objects.create(
            structure=self.structure_a,
            property_type=pt_a,
            name="A-101",
            availability=Property.Availability.AVAILABLE,
        )
        prop_b = Property.objects.create(
            structure=self.structure_b,
            property_type=pt_b,
            name="B-101",
            availability=Property.Availability.AVAILABLE,
        )

        # Booking in structure B — must NOT appear in structure A export
        booking_b = Booking.objects.create(
            structure=self.structure_b,
            property_type=pt_b,
            property=prop_b,
            check_in_date=date(2026, 6, 1),
            check_out_date=date(2026, 6, 3),
            adults_count=1,
            children_count=0,
            is_checked_in=True,
        )
        Guest.objects.create(
            booking=booking_b,
            full_name="B Guest",
            is_main_guest=True,
            guest_type="16",
            gender="male",
            date_of_birth=date(1990, 1, 1),
            country_of_birth="US",
            nationality="US",
            country="US",
            extra_data={},
        )

    def test_structure_a_export_excludes_structure_b_bookings(self):
        result = generate_c59_zip_export(
            structure_id=self.structure_a.id,
            start_date=date(2026, 6, 1),
            end_date=date(2026, 6, 3),
        )
        for day in ["C59_20260601.xml", "C59_20260602.xml", "C59_20260603.xml"]:
            xml = _read_xml_from_zip(result, day)
            self.assertNotIn("<rim:rigac59", xml)
