from unittest.mock import patch
from datetime import date
from decimal import Decimal

from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.test import APIClient

from bookings.models import Booking
from guests.serializers import CheckInSerializer
from guests.models import Guest
from istat.models import (
    IstatAuditLog,
    IstatCountry,
    IstatCredential,
    IstatMunicipality,
    IstatProvince,
    IstatReservationPosition,
    IstatSyncHistory,
)
from properties.models import Property, PropertyType
from services.guest_night_service import generate_guest_nights
from services.istat_export_service import (
    FIELD_LENGTHS,
    build_istat_preview,
    generate_istat_export,
)
from structures.city_tax_service import (
    build_configured_exemption_map,
    build_monthly_rate_map,
    calculate_city_tax_report,
    normalize_platform_tokens,
)
from structures.models import (
    CityTaxMonthlyRate,
    Structure,
    StructureCityTaxSettings,
)


class IstatExportPreviewTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="owner",
            password="test-password",
        )
        self.structure = Structure.objects.create(
            user=self.user,
            name="Test Structure",
            structure_type="Hotel",
            zip_code="00100",
            country="Italy",
            istat_code="ISTAT01",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Standard Room",
            num_beds=1,
            num_sofa_beds=0,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 1",
        )
        self.booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 1, 10),
            check_out_date=date(2026, 1, 12),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        self.guest = Guest.objects.create(
            booking=self.booking,
            full_name="Jane Doe",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1990, 1, 1),
            gender="female",
            country_of_birth="",
            nationality="",
            country="",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={},
        )

    def test_preview_uses_guest_field_names_for_country_errors(self):
        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )

        self.assertEqual(preview["summary"]["valid_rows"], 0)
        self.assertEqual(preview["summary"]["excluded_rows"], 1)
        self.assertEqual(len(preview["invalid_records"]), 1)
        self.assertEqual(len(preview["record_payloads"]), 1)

        errors = preview["invalid_records"][0]["errors"]
        self.assertCountEqual(errors, ["country_of_birth", "nationality", "country"])
        self.assertNotIn("birth_country", errors)
        self.assertNotIn("citizenship", errors)
        self.assertNotIn("residence_country", errors)

        record = preview["record_payloads"][0]
        self.assertEqual(record["nationality"], "")
        self.assertNotIn("citizenship", record)

    def test_preview_applies_istat_guest_defaults_without_mutating_guest(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        Guest.objects.filter(id=self.guest.id).update(
            country_of_birth="IN",
            nationality=None,
            country=None,
            tourism_type=None,
            transport_type=" ",
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )

        self.assertEqual(preview["invalid_records"], [])
        self.assertEqual(preview["summary"]["valid_rows"], 1)

        record = preview["record_payloads"][0]
        self.assertEqual(record["status"], "valid")
        self.assertEqual(record["nationality"], "IN")
        self.assertEqual(record["residence_country"], "IN")
        self.assertEqual(record["tourism_type"], "not_specified")
        self.assertEqual(record["transport_type"], "not_specified")

        self.guest.refresh_from_db()
        self.assertIsNone(self.guest.nationality)
        self.assertIsNone(self.guest.country)
        self.assertEqual(self.guest.transport_type, " ")

        export = generate_istat_export(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )
        self.assertEqual(export["invalid_records"], [])
        self.assertIn("Non Specificato", export["content"].decode("ascii"))

    def test_preview_uses_place_of_birth_and_country_reference_data(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )

        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 12),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="John Doe",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1990, 1, 1),
            gender="male",
            country_of_birth=None,
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"place_of_birth": "India"},
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        self.assertEqual(preview["summary"]["valid_rows"], 1)
        self.assertEqual(preview["summary"]["excluded_rows"], 0)

        record = next(
            item for item in preview["record_payloads"] if item["guest_id"] == guest.id
        )
        self.assertEqual(record["status"], "valid")
        self.assertEqual(record["errors"], [])
        self.assertEqual(record["nationality"], "India")
        self.assertEqual(preview["invalid_records"], [])

    def test_preview_supports_separate_birth_country_and_birth_city(self):
        IstatCountry.objects.create(
            code="000000100",
            name="Italy",
            iso_code="IT",
        )
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        IstatProvince.objects.create(code="RM", name="Roma", region="Lazio")
        IstatMunicipality.objects.create(
            code="058091000",
            name="Rome",
            province="RM",
            region="Lazio",
        )

        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 15),
            check_out_date=date(2026, 2, 18),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="Mario Rossi",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1988, 6, 12),
            gender="male",
            country_of_birth="Italy",
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"place_of_birth": "Rome"},
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        self.assertEqual(preview["summary"]["valid_rows"], 1)
        record = next(
            item for item in preview["record_payloads"] if item["guest_id"] == guest.id
        )
        self.assertEqual(record["status"], "valid")
        self.assertEqual(record["errors"], [])

    def test_preview_uses_checkin_place_of_birth_and_province_for_residence(self):
        IstatCountry.objects.create(
            code="000000100",
            name="Italy",
            iso_code="IT",
        )
        IstatProvince.objects.create(code="AL", name="Alessandria", region="Piemonte")
        IstatMunicipality.objects.create(
            code="006003000",
            name="Alessandria",
            province="AL",
            region="Piemonte",
        )

        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 20),
            check_out_date=date(2026, 2, 22),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="Anna Verdi",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1992, 5, 10),
            gender="female",
            country_of_birth="Italy",
            nationality="Italy",
            country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={
                "place_of_birth": "Alessandria",
                "province": "AL",
            },
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        record = next(
            item for item in preview["record_payloads"] if item["guest_id"] == guest.id
        )
        self.assertEqual(record["status"], "valid")
        self.assertNotIn("residence_municipality", record["errors"])
        self.assertNotIn("residence_province", record["errors"])

        export = generate_istat_export(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )
        content = export["content"].decode("ascii")
        self.assertIn("006003000AL", content)

    def test_preview_rejects_arbitrary_italian_residence_municipality(self):
        IstatCountry.objects.create(
            code="000000100",
            name="Italy",
            iso_code="IT",
        )

        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 20),
            check_out_date=date(2026, 2, 22),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="Invalid City",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1992, 5, 10),
            gender="female",
            country_of_birth="Italy",
            nationality="Italy",
            country="Italy",
            city="Thane",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"province": "AL"},
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        record = next(
            item for item in preview["record_payloads"] if item["guest_id"] == guest.id
        )
        self.assertEqual(record["status"], "excluded")
        self.assertIn("residence_municipality", record["errors"])

    def test_preview_flags_municipality_province_mismatch(self):
        IstatCountry.objects.create(
            code="000000100",
            name="Italy",
            iso_code="IT",
        )
        IstatMunicipality.objects.create(
            code="037006000",
            name="Bologna",
            province="BO",
            region="Emilia-Romagna",
        )

        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 20),
            check_out_date=date(2026, 2, 22),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="Mismatch City",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1992, 5, 10),
            gender="female",
            country_of_birth="Italy",
            nationality="Italy",
            country="Italy",
            city="Bologna",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"province": "AL"},
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        record = next(
            item for item in preview["record_payloads"] if item["guest_id"] == guest.id
        )
        self.assertEqual(record["status"], "excluded")
        self.assertIn("residence_municipality", record["errors"])
        self.assertIn("residence_province", record["errors"])

    def test_preview_includes_rows_for_stays_that_have_not_checked_out_yet(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 20),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        Guest.objects.create(
            booking=booking,
            full_name="Future Checkout",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1992, 5, 1),
            gender="female",
            country_of_birth="India",
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
        )

        with patch("services.istat_export_service.timezone.localdate") as mocked_localdate:
            mocked_localdate.return_value = date(2026, 2, 12)
            preview = build_istat_preview(
                structure_id=self.structure.id,
                start_date=date(2026, 2, 1),
                end_date=date(2026, 2, 28),
            )

        self.assertEqual(preview["summary"]["valid_rows"], 1)
        self.assertEqual(len(preview["record_payloads"]), 1)
        self.assertEqual(preview["invalid_records"], [])

    def test_preview_excludes_bookings_without_booking_level_checkin_status(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 22),
            check_out_date=date(2026, 2, 24),
            adults_count=1,
            is_checked_in=False,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        Guest.objects.create(
            booking=booking,
            full_name="Unchecked Guest",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1992, 5, 1),
            gender="female",
            country_of_birth="India",
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        self.assertEqual(preview["summary"]["valid_rows"], 0)
        self.assertEqual(preview["summary"]["total_guests"], 0)
        self.assertEqual(preview["record_payloads"], [])
        self.assertEqual(preview["invalid_records"], [])

    def test_export_keeps_position_code_stable_when_checkin_updates_existing_guest(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 12),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        guest = Guest.objects.create(
            booking=booking,
            full_name="Stable Position",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1990, 1, 1),
            gender="female",
            country_of_birth="India",
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
        )

        before_preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )
        before_record = next(
            item for item in before_preview["record_payloads"] if item["guest_id"] == guest.id
        )

        serializer = CheckInSerializer(
            data={
                "booking_id": booking.id,
                "guests": [
                    {
                        "id": guest.id,
                        "full_name": "Stable Position",
                        "is_main_guest": True,
                        "guest_type": "16",
                        "date_of_birth": "1990-01-01",
                        "gender": "female",
                        "country_of_birth": "India",
                        "nationality": "India",
                        "country": "India",
                        "tourism_type": "Cultural",
                        "transport_type": "Car",
                    }
                ],
            }
        )
        self.assertTrue(serializer.is_valid(), serializer.errors)
        serializer.save()

        after_preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )
        after_record = after_preview["record_payloads"][0]

        self.assertEqual(after_record["guest_id"], guest.id)
        self.assertEqual(after_record["position_id"], before_record["position_id"])
        self.assertEqual(IstatReservationPosition.objects.count(), 1)

    def test_generate_export_produces_fixed_length_ascii_with_crlf(self):
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 12),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )
        Guest.objects.create(
            booking=booking,
            full_name="Export Ready",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=date(1990, 1, 1),
            gender="female",
            country_of_birth="India",
            nationality="India",
            country="India",
            tourism_type="Cultural",
            transport_type="Car",
        )

        with patch("services.istat_export_service.timezone.localdate") as mocked_localdate:
            mocked_localdate.return_value = date(2026, 3, 26)
            payload = generate_istat_export(
                structure_id=self.structure.id,
                start_date=date(2026, 2, 1),
                end_date=date(2026, 2, 28),
            )

        content = payload["content"].decode("ascii")
        records = [line for line in content.split("\r\n") if line]

        self.assertEqual(payload["filename"], "ISTAT01_20260326.txt")
        self.assertTrue(content.endswith("\r\n"))
        self.assertEqual(len(records), 1)
        self.assertEqual(len(records[0]), sum(FIELD_LENGTHS.values()))


class IstatMunicipalityAPITests(TestCase):
    def test_municipality_endpoint_filters_by_province_and_search(self):
        IstatMunicipality.objects.create(
            code="037006000",
            name="Bologna",
            province="BO",
            region="Emilia-Romagna",
        )
        IstatMunicipality.objects.create(
            code="015146000",
            name="Milano",
            province="MI",
            region="Lombardia",
        )

        response = APIClient().get(
            "/api/istat/municipalities/",
            {
                "province": "BO",
                "search": "bol",
            },
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            response.data,
            {
                "results": [
                    {
                        "code": "037006000",
                        "name": "Bologna",
                        "province": "BO",
                        "value": "Bologna",
                        "label": "Bologna (BO)",
                    }
                ]
            },
        )


class IstatIssuesAPITests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="istat-owner",
            password="test-password",
        )
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)
        self.structure = Structure.objects.create(
            user=self.user,
            name="Issue Test Structure",
            structure_type="Hotel",
            zip_code="00100",
            country="Italy",
            istat_code="ISTAT02",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Suite",
            num_beds=1,
            num_sofa_beds=0,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 2",
        )
        IstatCountry.objects.create(
            code="000000330",
            name="India",
            iso_code="IN",
        )

    def _create_booking(self, check_in_day):
        return Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=date(2026, 1, check_in_day),
            check_out_date=date(2026, 1, check_in_day + 2),
            adults_count=1,
            is_checked_in=True,
            guest_group_type="single",
            tourism_type="Cultural",
            transport_type="Car",
        )

    def _create_guest(self, booking, **overrides):
        payload = {
            "booking": booking,
            "full_name": "Guest Example",
            "is_main_guest": True,
            "guest_type": "16",
            "date_of_birth": date(1990, 1, 1),
            "gender": "female",
            "country_of_birth": "India",
            "nationality": "India",
            "country": "India",
            "tourism_type": "Cultural",
            "transport_type": "Car",
            "extra_data": {},
        }
        payload.update(overrides)
        return Guest.objects.create(**payload)

    def test_issues_summary_groups_backend_errors_and_metadata(self):
        valid_booking = self._create_booking(1)
        self._create_guest(valid_booking, full_name="Valid Guest")

        missing_profile_booking = self._create_booking(5)
        missing_profile_guest = self._create_guest(
            missing_profile_booking,
            full_name="Missing Profile",
            gender=None,
            nationality="",
            country="",
        )

        missing_birth_date_booking = self._create_booking(10)
        self._create_guest(
            missing_birth_date_booking,
            full_name="Missing Birth Date",
            date_of_birth=None,
        )

        response = self.client.get(
            f"/api/structures/{self.structure.id}/istat/issues-summary/",
            {
                "year": 2026,
                "from_month": 1,
                "to_month": 1,
            },
        )

        self.assertEqual(response.status_code, 200)
        issues_by_field = {
            issue["field"]: issue for issue in response.data["issues"]
        }

        self.assertEqual(issues_by_field["gender"]["count"], 1)
        self.assertEqual(issues_by_field["gender"]["guest_ids"], [missing_profile_guest.id])
        self.assertEqual(issues_by_field["gender"]["fix_type"], "select")
        self.assertEqual(issues_by_field["gender"]["options"], ["M", "F"])
        self.assertEqual(issues_by_field["gender"]["suggested"], "F")

        self.assertEqual(issues_by_field["nationality"]["fix_type"], "select")
        self.assertEqual(issues_by_field["nationality"]["suggested"], "IN")
        self.assertEqual(issues_by_field["country"]["fix_type"], "select")
        self.assertEqual(issues_by_field["country"]["suggested"], "IN")
        self.assertEqual(issues_by_field["birth_date"]["fix_type"], "manual_only")
        self.assertNotIn("options", issues_by_field["birth_date"])

    def test_fix_issues_bulk_updates_records_and_revalidates(self):
        valid_booking = self._create_booking(1)
        self._create_guest(valid_booking, full_name="Valid Guest")

        invalid_booking = self._create_booking(15)
        guest = self._create_guest(
            invalid_booking,
            full_name="Needs Fix",
            gender=None,
            nationality="",
            country="",
            extra_data={
                "gender": "other",
                "nationality": "Atlantis",
                "country": "Atlantis",
            },
        )

        before_preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )
        self.assertEqual(len(before_preview["invalid_records"]), 1)

        response = self.client.post(
            f"/api/structures/{self.structure.id}/istat/fix-issues/",
            {
                "period": {
                    "year": 2026,
                    "from_month": 1,
                    "to_month": 1,
                },
                "fixes": [
                    {
                        "field": "gender",
                        "value": "F",
                        "guest_ids": [guest.id],
                    },
                    {
                        "field": "nationality",
                        "value": "IN",
                        "guest_ids": [guest.id],
                    },
                    {
                        "field": "country",
                        "value": "IN",
                        "guest_ids": [guest.id],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data["success"], True)
        self.assertEqual(response.data["updated_count"], 1)
        self.assertEqual(response.data["remaining_issues"], 0)

        guest.refresh_from_db()
        self.assertEqual(guest.gender, "female")
        self.assertEqual(guest.nationality, "IN")
        self.assertEqual(guest.country, "IN")
        self.assertEqual(guest.extra_data["gender"], "female")
        self.assertEqual(guest.extra_data["nationality"], "IN")
        self.assertEqual(guest.extra_data["country"], "IN")


class IstatCredentialIntegrationApiTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="istat-owner",
            password="test-password",
        )
        self.other_user = User.objects.create_user(
            username="other-user",
            password="test-password",
        )
        self.structure = Structure.objects.create(
            user=self.user,
            name="Liguria Suites",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
            istat_code="LIG001",
        )
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_create_credentials_encrypts_storage_and_masks_password(self):
        response = self.client.post(
            f"/api/structures/{self.structure.id}/istat/credentials/",
            {
                "username": "ross1000-user",
                "password": "super-secret-password",
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data["connected"], True)
        self.assertEqual(response.data["connection_status"], "connected")
        self.assertEqual(response.data["username"], "ross1000-user")
        self.assertEqual(response.data["password_masked"], "\u2022" * 8)
        self.assertIsNone(response.data["last_sync"])

        credential = IstatCredential.objects.get(structure=self.structure)
        self.assertNotEqual(credential.username_encrypted, "ross1000-user")
        self.assertNotEqual(
            credential.password_encrypted, "super-secret-password"
        )
        self.assertEqual(credential.username, "ross1000-user")
        self.assertEqual(credential.password, "super-secret-password")

        audit_log = IstatAuditLog.objects.get()
        self.assertEqual(
            audit_log.action, IstatAuditLog.Action.CREDENTIAL_CREATED
        )

    def test_manual_sync_stub_requires_credentials_and_records_history(self):
        missing_credentials_response = self.client.post(
            f"/api/istat/sync/{self.structure.id}/",
            {},
            format="json",
        )
        self.assertEqual(missing_credentials_response.status_code, 400)

        credential = IstatCredential.objects.create(
            structure=self.structure,
            created_by=self.user,
            updated_by=self.user,
        )
        credential.username = "ross1000-user"
        credential.password = "super-secret-password"
        credential.save()

        response = self.client.post(
            f"/api/istat/sync/{self.structure.id}/",
            {"year": 2026, "month": 3},
            format="json",
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data["status"], "success")
        self.assertEqual(
            response.data["message"], "Report sent successfully (stub)"
        )
        self.assertEqual(
            response.data["requested_period"], {"year": 2026, "month": 3}
        )
        self.assertIn("timestamp", response.data)

        sync_record = IstatSyncHistory.objects.get(structure=self.structure)
        self.assertEqual(sync_record.status, IstatSyncHistory.Status.SUCCESS)
        self.assertEqual(sync_record.requested_period, {"year": 2026, "month": 3})

        details_response = self.client.get(
            f"/api/structures/{self.structure.id}/istat/credentials/"
        )
        self.assertEqual(details_response.status_code, 200)
        self.assertEqual(details_response.data["last_sync"]["status"], "success")

        self.assertTrue(
            IstatAuditLog.objects.filter(
                action=IstatAuditLog.Action.SYNC_TRIGGERED
            ).exists()
        )

    def test_patch_and_delete_credentials_updates_connection_state(self):
        credential = IstatCredential.objects.create(
            structure=self.structure,
            created_by=self.user,
            updated_by=self.user,
        )
        credential.username = "initial-user"
        credential.password = "initial-password"
        credential.save()

        patch_response = self.client.patch(
            f"/api/structures/{self.structure.id}/istat/credentials/",
            {
                "username": "updated-user",
                "password": "updated-password",
            },
            format="json",
        )
        self.assertEqual(patch_response.status_code, 200)
        self.assertEqual(patch_response.data["username"], "updated-user")

        credential.refresh_from_db()
        self.assertEqual(credential.username, "updated-user")
        self.assertEqual(credential.password, "updated-password")

        delete_response = self.client.delete(
            f"/api/structures/{self.structure.id}/istat/credentials/"
        )
        self.assertEqual(delete_response.status_code, 200)
        self.assertEqual(delete_response.data["connected"], False)
        self.assertEqual(delete_response.data["connection_status"], "not_connected")
        self.assertIsNone(delete_response.data["username"])
        self.assertFalse(
            IstatCredential.objects.filter(structure=self.structure).exists()
        )

        self.assertEqual(
            list(
                IstatAuditLog.objects.values_list("action", flat=True).order_by(
                    "created_at"
                )
            ),
            [
                IstatAuditLog.Action.CREDENTIAL_UPDATED,
                IstatAuditLog.Action.CREDENTIAL_DELETED,
            ],
        )

    def test_structure_owner_scope_blocks_other_users(self):
        credential = IstatCredential.objects.create(
            structure=self.structure,
            created_by=self.user,
            updated_by=self.user,
        )
        credential.username = "locked-user"
        credential.password = "locked-password"
        credential.save()

        other_client = APIClient()
        other_client.force_authenticate(self.other_user)

        response = other_client.get(
            f"/api/structures/{self.structure.id}/istat/credentials/"
        )
        self.assertEqual(response.status_code, 404)


class IstatCityTaxAlignmentTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="istat-alignment-owner",
            password="test-password",
        )
        self.structure = Structure.objects.create(
            user=self.user,
            name="Alignment Structure",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
            istat_code="ALIGN01",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Apartment",
            max_guests=6,
            num_beds=2,
        )
        self.property = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 101",
        )
        self.city_tax_settings = StructureCityTaxSettings.objects.create(
            structure=self.structure,
            default_rate=Decimal("3.00"),
            max_taxable_nights=8,
            minor_age_limit=14,
            exemption_reasons=["resident", "medical"],
            platform_exemptions=["airbnb"],
            is_active=True,
        )

        for year in (2025, 2026):
            for month in range(1, 13):
                CityTaxMonthlyRate.objects.create(
                    structure=self.structure,
                    settings=self.city_tax_settings,
                    year=year,
                    month=month,
                    rate=Decimal("3.00"),
                )

    def create_booking(
        self,
        *,
        check_in_date,
        check_out_date,
        adults_count=1,
        children_count=0,
        platform="direct",
        guest_specs=None,
    ):
        booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property,
            check_in_date=check_in_date,
            check_out_date=check_out_date,
            adults_count=adults_count,
            children_count=children_count,
            is_checked_in=True,
            platform=platform,
            guest_group_type="group",
            tourism_type="Cultural",
            transport_type="Car",
            total_price=Decimal("100.00"),
        )

        for index, spec in enumerate(guest_specs or []):
            Guest.objects.create(
                booking=booking,
                full_name=spec.get("full_name", f"Guest {index + 1}"),
                is_main_guest=spec.get("is_main_guest", index == 0),
                date_of_birth=spec.get("date_of_birth"),
                gender=spec.get("gender"),
                guest_type=spec.get("guest_type"),
                nationality=spec.get("nationality"),
                country=spec.get("country"),
                country_of_birth=spec.get("country_of_birth"),
                tourism_type=spec.get("tourism_type", "Cultural"),
                transport_type=spec.get("transport_type", "Car"),
                is_city_tax_exempt=spec.get("is_city_tax_exempt", False),
                city_tax_exemption_reason=spec.get("city_tax_exemption_reason"),
            )

        return booking

    def calculate_city_tax(self, *, period):
        queryset = (
            Booking.objects.filter(structure=self.structure)
            .select_related("property")
            .prefetch_related("guests")
            .order_by("check_in_date", "id")
        )
        return calculate_city_tax_report(
            bookings=queryset,
            period=period,
            rates_override={},
            default_rate=self.city_tax_settings.default_rate,
            monthly_rate_map=build_monthly_rate_map(self.structure, period["year"]),
            max_taxable_nights=self.city_tax_settings.max_taxable_nights,
            minor_age_limit=self.city_tax_settings.minor_age_limit,
            configured_exemptions=build_configured_exemption_map(
                self.city_tax_settings.exemption_reasons
            ),
            platform_exemptions=normalize_platform_tokens(
                self.city_tax_settings.platform_exemptions
            ),
        )

    def test_shared_guest_night_generator_splits_december_january_stay(self):
        self.create_booking(
            check_in_date=date(2025, 12, 31),
            check_out_date=date(2026, 1, 4),
            adults_count=2,
            guest_specs=[
                {"full_name": "Alice"},
                {"full_name": "Bob"},
            ],
        )

        guest_nights = generate_guest_nights(
            Booking.objects.filter(structure=self.structure).prefetch_related("guests"),
            period_start=date(2026, 1, 1),
            period_end_exclusive=date(2026, 2, 1),
            minor_age_limit=14,
        )

        self.assertEqual(len(guest_nights), 6)
        self.assertEqual(
            sorted({guest_night.date for guest_night in guest_nights}),
            [date(2026, 1, 1), date(2026, 1, 2), date(2026, 1, 3)],
        )

    def test_istat_preview_counts_cross_month_guest_nights_by_actual_month(self):
        self.create_booking(
            check_in_date=date(2025, 12, 31),
            check_out_date=date(2026, 1, 4),
            adults_count=2,
            guest_specs=[
                {"full_name": "Alice"},
                {"full_name": "Bob"},
            ],
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 1, 1),
            end_date=date(2026, 1, 31),
        )

        self.assertEqual(preview["summary"]["total_guests"], 2)
        self.assertEqual(preview["summary"]["nights_stayed"], 6)
        self.assertEqual(preview["summary"]["arrivals"], 0)
        self.assertEqual(preview["summary"]["departures"], 2)
        self.assertEqual(preview["summary"]["monthly"][0]["nights_stayed"], 6)

    def test_istat_counts_same_guest_across_multiple_bookings_as_multiple_occurrences(self):
        self.create_booking(
            check_in_date=date(2026, 2, 1),
            check_out_date=date(2026, 2, 3),
            adults_count=1,
            guest_specs=[{"full_name": "Repeat Guest"}],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 12),
            adults_count=1,
            guest_specs=[{"full_name": "Repeat Guest"}],
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )

        self.assertEqual(preview["summary"]["total_guests"], 2)
        self.assertEqual(preview["summary"]["nights_stayed"], 4)
        self.assertEqual(preview["summary"]["arrivals"], 2)
        self.assertEqual(preview["summary"]["departures"], 2)

    def test_istat_includes_child_guest_nights_even_when_city_tax_exempts_them(self):
        self.create_booking(
            check_in_date=date(2026, 3, 6),
            check_out_date=date(2026, 3, 8),
            adults_count=1,
            children_count=1,
            guest_specs=[
                {"full_name": "Adult Guest", "date_of_birth": date(1990, 5, 1)},
                {"full_name": "Child Guest", "date_of_birth": date(2020, 5, 1)},
            ],
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 3, 1),
            end_date=date(2026, 3, 31),
        )
        city_tax = self.calculate_city_tax(
            period={"from_month": 3, "to_month": 3, "year": 2026}
        )

        self.assertEqual(preview["summary"]["total_guests"], 2)
        self.assertEqual(preview["summary"]["nights_stayed"], 4)
        self.assertEqual(city_tax.preview_payload["totals"]["nights"], 4)
        self.assertEqual(
            preview["summary"]["nights_stayed"],
            city_tax.preview_payload["totals"]["nights"],
        )

    def test_istat_february_guest_nights_match_city_tax_total_for_partial_month_stays(self):
        self.create_booking(
            check_in_date=date(2026, 2, 10),
            check_out_date=date(2026, 2, 12),
            adults_count=2,
            guest_specs=[
                {"full_name": "Guest A1"},
                {"full_name": "Guest A2"},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 14),
            check_out_date=date(2026, 2, 18),
            adults_count=2,
            guest_specs=[
                {"full_name": "Guest B1"},
                {"full_name": "Guest B2"},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 27),
            check_out_date=date(2026, 3, 1),
            adults_count=2,
            guest_specs=[
                {"full_name": "Guest C1"},
                {"full_name": "Guest C2"},
            ],
        )

        preview = build_istat_preview(
            structure_id=self.structure.id,
            start_date=date(2026, 2, 1),
            end_date=date(2026, 2, 28),
        )
        city_tax = self.calculate_city_tax(
            period={"from_month": 2, "to_month": 2, "year": 2026}
        )

        self.assertEqual(preview["summary"]["nights_stayed"], 16)
        self.assertEqual(preview["summary"]["monthly"][0]["nights_stayed"], 16)
        self.assertEqual(city_tax.preview_payload["totals"]["nights"], 16)
        self.assertEqual(
            preview["summary"]["nights_stayed"],
            city_tax.preview_payload["totals"]["nights"],
        )
