from datetime import date
from decimal import Decimal

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

from bookings.models import Booking
from guests.models import Guest
from properties.models import Property, PropertyType
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 CityTaxCalculationServiceTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="city-tax-owner",
            password="test-password",
        )
        self.structure = Structure.objects.create(
            user=self.user,
            name="Genova Test Structure",
            structure_type="Hotel",
            zip_code="16121",
            country="Italy",
        )
        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.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 month in range(1, 13):
            CityTaxMonthlyRate.objects.create(
                structure=self.structure,
                settings=self.settings,
                year=2026,
                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,
            platform=platform,
            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"),
                is_city_tax_exempt=spec.get("is_city_tax_exempt", False),
                city_tax_exemption_reason=spec.get("city_tax_exemption_reason"),
                email=spec.get("email"),
                phone=spec.get("phone"),
            )
        return booking

    def calculate(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.settings.default_rate,
            monthly_rate_map=build_monthly_rate_map(self.structure, period["year"]),
            max_taxable_nights=self.settings.max_taxable_nights,
            minor_age_limit=self.settings.minor_age_limit,
            configured_exemptions=build_configured_exemption_map(
                self.settings.exemption_reasons
            ),
            platform_exemptions=normalize_platform_tokens(
                self.settings.platform_exemptions
            ),
        )

    def test_cross_month_reservation_counts_only_january_nights(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"},
            ],
        )

        result = self.calculate(period={"from_month": 1, "to_month": 1, "year": 2026})

        self.assertEqual(result.preview_payload["summary"]["taxable_nights"], 6)
        self.assertEqual(result.preview_payload["section_a"]["rows"][0]["nights"], 6)
        self.assertEqual(result.preview_payload["totals"]["nights"], 6)

    def test_cross_month_split_distributes_nights_to_each_month(self):
        self.create_booking(
            check_in_date=date(2026, 2, 27),
            check_out_date=date(2026, 3, 3),
            adults_count=2,
            guest_specs=[
                {"full_name": "Alice"},
                {"full_name": "Bob"},
            ],
        )

        result = self.calculate(period={"from_month": 2, "to_month": 3, "year": 2026})

        rows = {row["month"]: row for row in result.preview_payload["section_a"]["rows"]}
        self.assertEqual(rows["February"]["nights"], 4)
        self.assertEqual(rows["March"]["nights"], 4)
        self.assertEqual(result.preview_payload["summary"]["taxable_nights"], 8)
        self.assertEqual(len(result.export_rows), 2)
        export_rows = {row["month"]: row for row in result.export_rows}
        self.assertEqual(export_rows[2]["all_guest_nights"], 4)
        self.assertEqual(export_rows[3]["all_guest_nights"], 4)

    def test_max_nights_overflow_splits_between_section_a_and_b(self):
        self.create_booking(
            check_in_date=date(2026, 3, 1),
            check_out_date=date(2026, 3, 11),
            adults_count=1,
            guest_specs=[{"full_name": "Alice"}],
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})

        self.assertEqual(result.preview_payload["section_a"]["total"]["nights"], 8)
        self.assertEqual(result.preview_payload["section_b"]["total"]["nights"], 2)
        self.assertEqual(result.preview_payload["summary"]["taxable_nights"], 8)
        self.assertEqual(result.preview_payload["summary"]["exempt_nights"], 2)

    def test_exempt_child_uses_exclusive_checkout_nights(self):
        self.create_booking(
            check_in_date=date(2026, 3, 6),
            check_out_date=date(2026, 3, 8),
            adults_count=0,
            children_count=1,
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})

        minors_rows = result.preview_payload["section_c"]["Minors"]
        self.assertEqual(minors_rows[0]["guests"], 1)
        self.assertEqual(minors_rows[0]["nights"], 2)
        self.assertEqual(result.preview_payload["summary"]["taxable_nights"], 0)
        self.assertEqual(result.preview_payload["summary"]["exempt_nights"], 2)

    def test_platform_booking_is_fully_exempt_with_zero_tax(self):
        self.create_booking(
            check_in_date=date(2026, 3, 24),
            check_out_date=date(2026, 3, 26),
            adults_count=2,
            platform="Airbnb",
            guest_specs=[
                {"full_name": "Alice"},
                {"full_name": "Bob"},
            ],
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})

        self.assertEqual(result.preview_payload["section_d"]["guests"], 2)
        self.assertEqual(result.preview_payload["section_d"]["nights"], 4)
        self.assertEqual(result.preview_payload["section_d"]["tax"], 0)
        self.assertEqual(result.preview_payload["section_c"], {})
        self.assertEqual(result.preview_payload["summary"]["taxable_nights"], 0)
        self.assertEqual(result.preview_payload["summary"]["exempt_nights"], 4)
        self.assertEqual(result.preview_payload["summary"]["total_to_pay"], 0.0)

    def test_mixed_platform_and_taxable_data_stays_in_separate_sections(self):
        self.create_booking(
            check_in_date=date(2026, 3, 10),
            check_out_date=date(2026, 3, 12),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Taxed 1"},
                {"full_name": "Taxed 2"},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 24),
            check_out_date=date(2026, 3, 26),
            adults_count=2,
            platform="Airbnb",
            guest_specs=[
                {"full_name": "Platform 1"},
                {"full_name": "Platform 2"},
            ],
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})

        self.assertEqual(result.preview_payload["section_a"]["total"]["guests"], 2)
        self.assertEqual(result.preview_payload["section_a"]["total"]["nights"], 4)
        self.assertEqual(result.preview_payload["section_d"]["guests"], 2)
        self.assertEqual(result.preview_payload["section_d"]["nights"], 4)
        self.assertEqual(result.preview_payload["section_c"], {})

    def test_total_nights_matches_sections_and_exempt_total_includes_platform(self):
        self.create_booking(
            check_in_date=date(2026, 3, 1),
            check_out_date=date(2026, 3, 11),
            adults_count=1,
            guest_specs=[{"full_name": "Overflow Guest"}],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 6),
            check_out_date=date(2026, 3, 8),
            adults_count=0,
            children_count=1,
        )
        self.create_booking(
            check_in_date=date(2026, 3, 24),
            check_out_date=date(2026, 3, 26),
            adults_count=2,
            platform="Airbnb",
            guest_specs=[
                {"full_name": "Platform 1"},
                {"full_name": "Platform 2"},
            ],
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})
        preview = result.preview_payload
        section_c_nights = sum(
            row["nights"]
            for rows in preview["section_c"].values()
            for row in rows
        )

        self.assertEqual(
            preview["totals"]["nights"],
            preview["section_a"]["total"]["nights"]
            + preview["section_b"]["total"]["nights"]
            + section_c_nights
            + preview["section_d"]["nights"],
        )
        self.assertEqual(
            preview["summary"]["exempt_nights"],
            preview["section_b"]["total"]["nights"]
            + section_c_nights
            + preview["section_d"]["nights"],
        )

    def test_client_quarter_scenario_matches_report_expectations(self):
        self.create_booking(
            check_in_date=date(2025, 12, 31),
            check_out_date=date(2026, 1, 4),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Simona Rebasti", "is_main_guest": True},
                {"full_name": "Companion Rebasti", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 14),
            check_out_date=date(2026, 2, 16),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Giorgia Sciuto", "is_main_guest": True},
                {"full_name": "Giorgia Companion", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 18),
            check_out_date=date(2026, 2, 22),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Davide Marcantognini", "is_main_guest": True},
                {"full_name": "Davide Companion", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 2, 27),
            check_out_date=date(2026, 3, 3),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Nedelina Naydenova", "is_main_guest": True},
                {"full_name": "Nedelina Companion", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 6),
            check_out_date=date(2026, 3, 8),
            adults_count=1,
            children_count=1,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Claudia Salvemini", "is_main_guest": True},
                {
                    "full_name": "Claudia Child",
                    "is_main_guest": False,
                    "date_of_birth": date(2015, 6, 1),
                },
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 24),
            check_out_date=date(2026, 3, 26),
            adults_count=2,
            platform="Airbnb",
            guest_specs=[
                {"full_name": "Delia Notario", "is_main_guest": True},
                {"full_name": "Delia Companion", "is_main_guest": False},
            ],
        )

        # Extra taxable reservations to make the quarter totals match the client's
        # reported header expectations without affecting the February/Chapter D checks.
        self.create_booking(
            check_in_date=date(2026, 1, 10),
            check_out_date=date(2026, 1, 12),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Marco Rossi", "is_main_guest": True},
                {"full_name": "Giulia Rossi", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 1, 20),
            check_out_date=date(2026, 1, 22),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Luca Bianchi", "is_main_guest": True},
                {"full_name": "Sara Bianchi", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 10),
            check_out_date=date(2026, 3, 12),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Paolo Verdi", "is_main_guest": True},
                {"full_name": "Elisa Verdi", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 14),
            check_out_date=date(2026, 3, 17),
            adults_count=2,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Matteo Neri", "is_main_guest": True},
                {"full_name": "Chiara Neri", "is_main_guest": False},
            ],
        )
        self.create_booking(
            check_in_date=date(2026, 3, 20),
            check_out_date=date(2026, 3, 24),
            adults_count=1,
            platform="Booking.com",
            guest_specs=[
                {"full_name": "Francesco Gallo", "is_main_guest": True},
            ],
        )

        result = self.calculate(period={"from_month": 1, "to_month": 3, "year": 2026})
        preview = result.preview_payload

        section_a_rows = {
            row["month"]: row
            for row in preview["section_a"]["rows"]
        }
        section_b_rows = {
            row["month"]: row
            for row in preview["section_b"]["rows"]
        }
        export_rows = {
            (row["client_full_name"], row["month"]): row
            for row in result.export_rows
        }

        # Chapter A: reservation split by overlapping month only.
        self.assertEqual(
            export_rows[("Simona Rebasti", 1)]["estimated_ordinary_nights"],
            6,
        )
        self.assertEqual(
            export_rows[("Simona Rebasti", 1)]["all_guest_nights"],
            6,
        )

        self.assertEqual(section_a_rows["February"]["guests"], 6)
        self.assertEqual(section_a_rows["February"]["nights"], 16)
        self.assertEqual(section_a_rows["February"]["tax"], 48.0)
        self.assertEqual(export_rows[("Nedelina Naydenova", 2)]["all_guest_nights"], 4)
        self.assertEqual(export_rows[("Nedelina Naydenova", 3)]["all_guest_nights"], 4)

        # Chapter B: no overflow nights in February or March.
        self.assertEqual(section_b_rows["February"]["guests"], 0)
        self.assertEqual(section_b_rows["February"]["nights"], 0)
        self.assertEqual(section_b_rows["March"]["guests"], 0)
        self.assertEqual(section_b_rows["March"]["nights"], 0)

        # Chapter C: child stay from 03/06 to 03/08 means two exempt nights.
        minors_rows = {
            row["month"]: row
            for row in preview["section_c"]["Minors"]
        }
        self.assertEqual(minors_rows["March"]["guests"], 1)
        self.assertEqual(minors_rows["March"]["nights"], 2)

        # Chapter D: Airbnb booking contributes platform-exempt guest nights only.
        self.assertEqual(preview["section_d"]["guests"], 2)
        self.assertEqual(preview["section_d"]["nights"], 4)
        self.assertEqual(preview["section_d"]["tax"], 0)

        # Top headers / summary.
        self.assertEqual(preview["summary"]["guests_subject_to_tax"], 18)
        self.assertEqual(preview["summary"]["taxable_nights"], 50)
        self.assertEqual(preview["summary"]["total_to_pay"], 150.0)

    def test_section_a_total_guests_sums_per_month_not_global_set(self):
        """
        Regression: section_a.total.guests was using len(taxable_guest_keys),
        a global set that deduplicates guests appearing in multiple months.
        A cross-month booking (e.g. Jan->Feb) has the same guest_key in both
        months, so the global set counted them once instead of twice.
        The total must equal the sum of per-month guest counts.
        """
        # 1 booking spanning Jan and Feb, 2 guests
        # Jan nights = 2, Feb nights = 2
        # Per-month: Jan guests=2, Feb guests=2 -> total should be 4, not 2
        self.create_booking(
            check_in_date=date(2026, 1, 30),
            check_out_date=date(2026, 2, 3),
            adults_count=2,
            guest_specs=[
                {"full_name": "Alice"},
                {"full_name": "Bob"},
            ],
        )

        result = self.calculate(period={"from_month": 1, "to_month": 2, "year": 2026})
        preview = result.preview_payload

        rows = {row["month"]: row for row in preview["section_a"]["rows"]}
        self.assertEqual(rows["January"]["guests"], 2)
        self.assertEqual(rows["February"]["guests"], 2)
        # total must be sum of per-month counts, not the deduplicated global set
        self.assertEqual(preview["section_a"]["total"]["guests"], 4)

    def test_same_guest_two_bookings_both_exempt_counts_as_two_in_section_c(self):
        """
        Client report: 'Marco Romani has two reservations so counts as 2.'

        When the same physical person has two separate bookings and BOTH bookings
        have a Guest record with is_city_tax_exempt=True, the engine must count
        them as 2 occurrences in Section C — not deduplicate to 1.

        The guest_key is 'booking-{booking.id}-guest-{guest.id}'. Because
        booking.id differs between the two bookings, the two keys are always
        distinct, so the set-based counter correctly yields 2.
        """
        # Booking 1 — Marco, resident exemption set on Guest record
        self.create_booking(
            check_in_date=date(2026, 3, 1),
            check_out_date=date(2026, 3, 3),
            adults_count=1,
            guest_specs=[
                {
                    "full_name": "Marco Romani",
                    "is_main_guest": True,
                    "is_city_tax_exempt": True,
                    "city_tax_exemption_reason": "resident",
                },
            ],
        )
        # Booking 2 — same person, second reservation, exemption also set
        self.create_booking(
            check_in_date=date(2026, 3, 10),
            check_out_date=date(2026, 3, 12),
            adults_count=1,
            guest_specs=[
                {
                    "full_name": "Marco Romani",
                    "is_main_guest": True,
                    "is_city_tax_exempt": True,
                    "city_tax_exemption_reason": "resident",
                },
            ],
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})
        preview = result.preview_payload

        resident_rows = {row["month"]: row for row in preview["section_c"]["Resident"]}
        # 2 bookings × 1 guest each → 2 distinct guest_keys in the set → guests = 2
        self.assertEqual(resident_rows["March"]["guests"], 2)
        # booking 1: 2 nights, booking 2: 2 nights → 4 exempt nights total
        self.assertEqual(resident_rows["March"]["nights"], 4)
        # nothing taxable
        self.assertEqual(preview["summary"]["taxable_nights"], 0)
        self.assertEqual(preview["summary"]["total_to_pay"], 0.0)

    def test_same_guest_two_bookings_missing_exemption_on_second_goes_to_section_a(self):
        """
        Documents the root cause of the client bug:

        If Marco's second booking has NO Guest record (or the Guest record does
        not have is_city_tax_exempt=True), the engine cannot know he is a
        resident. That booking produces a synthetic-adult subject with no
        exemption label and is correctly routed to Section A (taxable).

        The fix is data-entry: every booking for an exempt guest must have a
        Guest record with is_city_tax_exempt=True and the correct reason.
        """
        # Booking 1 — exemption correctly set
        self.create_booking(
            check_in_date=date(2026, 3, 1),
            check_out_date=date(2026, 3, 3),
            adults_count=1,
            guest_specs=[
                {
                    "full_name": "Marco Romani",
                    "is_main_guest": True,
                    "is_city_tax_exempt": True,
                    "city_tax_exemption_reason": "resident",
                },
            ],
        )
        # Booking 2 — NO Guest record linked (adults_count=1, guest_specs=[])
        # Engine creates a synthetic-adult with no exemption → goes to Section A
        self.create_booking(
            check_in_date=date(2026, 3, 10),
            check_out_date=date(2026, 3, 12),
            adults_count=1,
            guest_specs=[],  # no Guest record → synthetic adult, no exemption
        )

        result = self.calculate(period={"from_month": 3, "to_month": 3, "year": 2026})
        preview = result.preview_payload

        resident_rows = {row["month"]: row for row in preview["section_c"]["Resident"]}
        # Only booking 1 reaches Section C
        self.assertEqual(resident_rows["March"]["guests"], 1)
        self.assertEqual(resident_rows["March"]["nights"], 2)
        # Booking 2 (synthetic adult, 2 nights) goes to Section A
        self.assertEqual(preview["section_a"]["rows"][0]["guests"], 1)
        self.assertEqual(preview["section_a"]["rows"][0]["nights"], 2)
