"""Tests for GuestStay synchronization service.

Validates:
- GuestStay creation for new bookings
- Idempotency (running sync multiple times doesn't duplicate)
- Date synchronization when booking dates change
- Guest removal and orphan cleanup
- idswh preservation across syncs
- Split booking flow
"""

from __future__ import annotations

from datetime import date

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

from bookings.models import Booking
from guests.models import Guest, GuestStay
from guests.services.guest_stay_service import sync_guest_stays_for_booking
from properties.models import Property, PropertyType
from structures.models import Structure


class GuestStaySyncTests(TestCase):
    """Test GuestStay synchronization service."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(username="testuser", password="pass")
        self.structure = Structure.objects.create(
            user=self.user,
            name="Test Hotel",
            istat_code="TST001",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Standard",
            max_guests=2,
            num_beds=1,
        )
        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, 6, 1),
            check_out_date=date(2026, 6, 3),
            adults_count=2,
            children_count=0,
        )
        self.guest1 = Guest.objects.create(
            booking=self.booking,
            full_name="Mario Rossi",
            is_main_guest=True,
        )
        self.guest2 = Guest.objects.create(
            booking=self.booking,
            full_name="Luca Bianchi",
            is_main_guest=False,
        )

    # ── Creation tests ─────────────────────────────────────────────────────

    def test_creates_guest_stays_for_all_guests(self):
        """Sync should create GuestStay rows for all guests."""
        sync_guest_stays_for_booking(self.booking)

        stays = list(GuestStay.objects.filter(booking=self.booking))
        self.assertEqual(len(stays), 2)

        guest_ids = {stay.guest_id for stay in stays}
        self.assertIn(self.guest1.id, guest_ids)
        self.assertIn(self.guest2.id, guest_ids)

    def test_idswh_generated_on_creation(self):
        """idswh should be auto-generated when GuestStay is created."""
        sync_guest_stays_for_booking(self.booking)

        stays = GuestStay.objects.filter(booking=self.booking)
        for stay in stays:
            self.assertIsNotNone(stay.idswh)
            self.assertEqual(len(stay.idswh), 32)  # 32-char hex

    def test_dates_synced_from_booking(self):
        """GuestStay dates should match booking dates."""
        sync_guest_stays_for_booking(self.booking)

        stays = GuestStay.objects.filter(booking=self.booking)
        for stay in stays:
            self.assertEqual(stay.check_in_date, date(2026, 6, 1))
            self.assertEqual(stay.check_out_date, date(2026, 6, 3))

    def test_empty_booking_removes_all_stays(self):
        """If booking has no guests, all GuestStay rows should be removed."""
        # Create stays first
        sync_guest_stays_for_booking(self.booking)
        self.assertEqual(GuestStay.objects.filter(booking=self.booking).count(), 2)

        # Remove all guests
        self.booking.guests.all().delete()
        sync_guest_stays_for_booking(self.booking)

        # All stays should be deleted
        self.assertEqual(GuestStay.objects.filter(booking=self.booking).count(), 0)

    # ── Idempotency tests ──────────────────────────────────────────────────

    def test_idempotent_no_duplicates_on_resync(self):
        """Running sync multiple times should not create duplicate rows."""
        sync_guest_stays_for_booking(self.booking)
        sync_guest_stays_for_booking(self.booking)
        sync_guest_stays_for_booking(self.booking)

        stays = GuestStay.objects.filter(booking=self.booking)
        self.assertEqual(stays.count(), 2)

    def test_idempotent_idswh_unchanged_on_resync(self):
        """idswh values should never change after initial creation."""
        sync_guest_stays_for_booking(self.booking)

        original_idswh = {
            stay.guest_id: stay.idswh
            for stay in GuestStay.objects.filter(booking=self.booking)
        }

        # Run sync multiple times
        sync_guest_stays_for_booking(self.booking)
        sync_guest_stays_for_booking(self.booking)

        # idswh should be identical
        for stay in GuestStay.objects.filter(booking=self.booking):
            self.assertEqual(stay.idswh, original_idswh[stay.guest_id])

    # ── Date synchronization tests ─────────────────────────────────────────

    def test_updates_dates_when_booking_dates_change(self):
        """GuestStay dates should update when booking dates change."""
        sync_guest_stays_for_booking(self.booking)

        # Change booking dates
        self.booking.check_in_date = date(2026, 7, 1)
        self.booking.check_out_date = date(2026, 7, 5)
        self.booking.save()

        # Re-sync
        sync_guest_stays_for_booking(self.booking)

        # All stays should have new dates
        stays = GuestStay.objects.filter(booking=self.booking)
        for stay in stays:
            self.assertEqual(stay.check_in_date, date(2026, 7, 1))
            self.assertEqual(stay.check_out_date, date(2026, 7, 5))

    def test_updates_dates_preserves_idswh(self):
        """Date updates must not change idswh."""
        sync_guest_stays_for_booking(self.booking)

        original_idswh = {
            stay.guest_id: stay.idswh
            for stay in GuestStay.objects.filter(booking=self.booking)
        }

        # Change booking dates
        self.booking.check_in_date = date(2026, 8, 1)
        self.booking.check_out_date = date(2026, 8, 10)
        self.booking.save()

        # Re-sync
        sync_guest_stays_for_booking(self.booking)

        # idswh should be unchanged
        for stay in GuestStay.objects.filter(booking=self.booking):
            self.assertEqual(stay.idswh, original_idswh[stay.guest_id])

    # ── Guest removal tests ────────────────────────────────────────────────

    def test_removes_orphan_stay_when_guest_removed(self):
        """GuestStay should be deleted when guest is removed from booking."""
        sync_guest_stays_for_booking(self.booking)
        self.assertEqual(GuestStay.objects.filter(booking=self.booking).count(), 2)

        # Remove guest2
        self.guest2.delete()
        sync_guest_stays_for_booking(self.booking)

        # Only guest1's stay should remain
        stays = GuestStay.objects.filter(booking=self.booking)
        self.assertEqual(stays.count(), 1)
        self.assertEqual(stays.first().guest_id, self.guest1.id)

    def test_removes_correct_orphan_when_multiple_guests(self):
        """Only orphan stays should be deleted, not all stays."""
        sync_guest_stays_for_booking(self.booking)

        # Remove guest1
        self.guest1.delete()
        sync_guest_stays_for_booking(self.booking)

        # Only guest2's stay should remain
        stays = GuestStay.objects.filter(booking=self.booking)
        self.assertEqual(stays.count(), 1)
        self.assertEqual(stays.first().guest_id, self.guest2.id)

    def test_adds_new_stay_when_guest_added(self):
        """New GuestStay should be created when guest is added to booking."""
        sync_guest_stays_for_booking(self.booking)
        initial_count = GuestStay.objects.filter(booking=self.booking).count()

        # Add a new guest
        guest3 = Guest.objects.create(
            booking=self.booking,
            full_name="Anna Verdi",
            is_main_guest=False,
        )

        # Re-sync
        sync_guest_stays_for_booking(self.booking)

        # Should have one more stay
        self.assertEqual(
            GuestStay.objects.filter(booking=self.booking).count(),
            initial_count + 1,
        )

        # New guest should have a stay with idswh
        stay3 = GuestStay.objects.get(booking=self.booking, guest=guest3)
        self.assertIsNotNone(stay3.idswh)

    # ── idswh preservation tests ───────────────────────────────────────────

    def test_idswh_never_regenerated(self):
        """idswh must be generated once and never regenerated."""
        sync_guest_stays_for_booking(self.booking)

        stay1 = GuestStay.objects.get(booking=self.booking, guest=self.guest1)
        original_idswh = stay1.idswh

        # Force multiple syncs
        for _ in range(5):
            sync_guest_stays_for_booking(self.booking)
            stay1.refresh_from_db()
            self.assertEqual(stay1.idswh, original_idswh)

    def test_idswh_unique_across_stays(self):
        """Each GuestStay must have a unique idswh."""
        sync_guest_stays_for_booking(self.booking)

        stays = list(GuestStay.objects.filter(booking=self.booking))
        idswh_values = [stay.idswh for stay in stays]

        # All idswh values must be unique
        self.assertEqual(len(idswh_values), len(set(idswh_values)))

    # ── Edge case tests ────────────────────────────────────────────────────

    def test_sync_with_no_guests_does_not_crash(self):
        """Sync should handle booking with no guests gracefully."""
        self.booking.guests.all().delete()
        sync_guest_stays_for_booking(self.booking)  # Should not raise

        self.assertEqual(GuestStay.objects.filter(booking=self.booking).count(), 0)

    def test_sync_preserves_existing_stays_when_no_changes(self):
        """If nothing changed, sync should not modify existing stays."""
        sync_guest_stays_for_booking(self.booking)

        original_updated_at = {
            stay.id: stay.updated_at
            for stay in GuestStay.objects.filter(booking=self.booking)
        }

        # Sync again with no changes
        sync_guest_stays_for_booking(self.booking)

        # updated_at should not change (no unnecessary updates)
        for stay in GuestStay.objects.filter(booking=self.booking):
            self.assertEqual(stay.updated_at, original_updated_at[stay.id])


class GuestStaySyncSplitBookingTests(TestCase):
    """Test GuestStay sync in split booking scenarios."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(username="splittest", password="pass")
        self.structure = Structure.objects.create(
            user=self.user,
            name="Split Hotel",
            istat_code="SPL001",
        )
        self.property_type = PropertyType.objects.create(
            structure=self.structure,
            name="Standard",
            max_guests=2,
            num_beds=1,
        )
        self.property1 = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 1",
        )
        self.property2 = Property.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            name="Room 2",
        )

        # Create original booking
        self.original_booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property1,
            check_in_date=date(2026, 6, 1),
            check_out_date=date(2026, 6, 5),
            adults_count=2,
            children_count=0,
        )
        self.guest1 = Guest.objects.create(
            booking=self.original_booking,
            full_name="Mario Rossi",
            is_main_guest=True,
        )
        self.guest2 = Guest.objects.create(
            booking=self.original_booking,
            full_name="Luca Bianchi",
            is_main_guest=False,
        )

    def test_split_booking_creates_new_stays(self):
        """When booking is split, new booking should get new GuestStay rows."""
        # Sync original booking
        sync_guest_stays_for_booking(self.original_booking)
        original_stay_count = GuestStay.objects.filter(
            booking=self.original_booking
        ).count()

        # Simulate split: create new booking with same guests
        new_booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property2,
            check_in_date=date(2026, 6, 3),
            check_out_date=date(2026, 6, 5),
            adults_count=2,
            children_count=0,
        )

        # Copy guests to new booking
        for guest in self.original_booking.guests.all():
            Guest.objects.create(
                booking=new_booking,
                full_name=guest.full_name,
                is_main_guest=guest.is_main_guest,
            )

        # Sync new booking
        sync_guest_stays_for_booking(new_booking)

        # New booking should have its own GuestStay rows
        new_stay_count = GuestStay.objects.filter(booking=new_booking).count()
        self.assertEqual(new_stay_count, original_stay_count)

        # Original booking stays should be preserved
        self.assertEqual(
            GuestStay.objects.filter(booking=self.original_booking).count(),
            original_stay_count,
        )

    def test_split_booking_generates_unique_idswh(self):
        """New booking after split should have different idswh values."""
        sync_guest_stays_for_booking(self.original_booking)

        # Create new booking (split)
        new_booking = Booking.objects.create(
            structure=self.structure,
            property_type=self.property_type,
            property=self.property2,
            check_in_date=date(2026, 6, 3),
            check_out_date=date(2026, 6, 5),
            adults_count=2,
        )

        # Copy guests
        for guest in self.original_booking.guests.all():
            Guest.objects.create(
                booking=new_booking,
                full_name=guest.full_name,
                is_main_guest=guest.is_main_guest,
            )

        # Sync both bookings
        sync_guest_stays_for_booking(new_booking)
        sync_guest_stays_for_booking(self.original_booking)

        # Collect idswh from both bookings
        original_idswh = set(
            GuestStay.objects.filter(booking=self.original_booking)
            .values_list("idswh", flat=True)
        )
        new_idswh = set(
            GuestStay.objects.filter(booking=new_booking)
            .values_list("idswh", flat=True)
        )

        # idswh should be completely different
        self.assertEqual(len(original_idswh & new_idswh), 0)
