"""Tests for Ross1000 movement day builder.

Uses Django TestCase with a real (test) database to verify:
- One movement day per calendar day in the range
- Empty days are included
- Arrivals and departures are correctly assigned
- idswh is read from GuestStay, never generated
- Overnight presence is NOT a separate block (only arrivals/departures)
- N+1 query avoidance (basic check)
"""

from __future__ import annotations

from datetime import date
from unittest.mock import MagicMock, patch

import pytest

from istat.ross1000.exceptions import Ross1000ValidationError
from istat.ross1000.models.movement_payload import Ross1000MovementDayPayload
from istat.ross1000.services.movement_builder import (
    _build_day_guests,
    _fetch_idswh_index,
    build_movement_days,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_structure(istat_code="058091-CAV-00001", structure_id=1):
    structure = MagicMock()
    structure.id = structure_id
    structure.istat_code = istat_code
    structure.total_units = 6
    return structure


def _make_booking(
    booking_id,
    check_in: date,
    check_out: date,
    guests=None,
    property_id=1,
):
    booking = MagicMock()
    booking.id = booking_id
    booking.check_in_date = check_in
    booking.check_out_date = check_out
    booking.property_id = property_id
    booking.is_checked_in = True
    booking.guests.all.return_value = guests or []
    return booking


def _make_guest_mock(guest_id: int, **kwargs):
    guest = MagicMock()
    guest.id = guest_id
    guest.full_name = kwargs.get("full_name", "ROSSI MARIO")
    guest.date_of_birth = kwargs.get("date_of_birth", date(1983, 2, 9))
    guest.gender = kwargs.get("gender", "male")
    guest.country_of_birth = kwargs.get("country_of_birth", "IT")
    guest.country = kwargs.get("country", "IT")
    guest.nationality = kwargs.get("nationality", "IT")
    guest.extra_data = {}
    guest.tourism_type = None
    guest.transport_type = None
    guest.city = None
    guest.region = None
    guest.city_of_birth = None
    return guest


_BUILDER = "istat.ross1000.services.movement_builder"


# ---------------------------------------------------------------------------
# _fetch_idswh_index() — unit tests (no DB)
# ---------------------------------------------------------------------------

class TestFetchIdswh:
    def test_returns_empty_dict_for_empty_booking_ids(self):
        result = _fetch_idswh_index([])
        assert result == {}


# ---------------------------------------------------------------------------
# _build_day_guests() — unit tests with mocked guest resolver
# ---------------------------------------------------------------------------

class TestBuildDayGuests:

    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_arrivals_only_on_checkin_date(self, mock_build):
        """Guests appear in arrivi ONLY on their check_in_date."""
        guest = _make_guest_mock(1)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[guest])
        idswh_index = {(1, 1): "token-abc"}
        mock_build.return_value = MagicMock(idswh="token-abc")

        # On check-in day → arrival
        result = _build_day_guests([booking], date(2026, 5, 7), idswh_index, is_arrival=True)
        assert len(result) == 1

        # On intermediate day → no arrival
        result = _build_day_guests([booking], date(2026, 5, 8), idswh_index, is_arrival=True)
        assert len(result) == 0

        # On check-out day → no arrival
        result = _build_day_guests([booking], date(2026, 5, 9), idswh_index, is_arrival=True)
        assert len(result) == 0

    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_departures_only_on_checkout_date(self, mock_build):
        """Guests appear in partenze ONLY on their check_out_date."""
        guest = _make_guest_mock(1)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[guest])
        idswh_index = {(1, 1): "token-abc"}
        mock_build.return_value = MagicMock(idswh="token-abc")

        # On check-in day → no departure
        result = _build_day_guests([booking], date(2026, 5, 7), idswh_index, is_arrival=False)
        assert len(result) == 0

        # On intermediate day → no departure
        result = _build_day_guests([booking], date(2026, 5, 8), idswh_index, is_arrival=False)
        assert len(result) == 0

        # On check-out day → departure
        result = _build_day_guests([booking], date(2026, 5, 9), idswh_index, is_arrival=False)
        assert len(result) == 1

    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_guest_skipped_when_idswh_missing(self, mock_build):
        guest = _make_guest_mock(1)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[guest])
        idswh_index = {}  # no idswh for this guest

        result = _build_day_guests([booking], date(2026, 5, 7), idswh_index, is_arrival=True)
        assert len(result) == 0
        mock_build.assert_not_called()

    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_guest_skipped_on_validation_error(self, mock_build):
        guest = _make_guest_mock(1)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[guest])
        idswh_index = {(1, 1): "token-abc"}
        mock_build.side_effect = Ross1000ValidationError("bad guest")

        result = _build_day_guests([booking], date(2026, 5, 7), idswh_index, is_arrival=True)
        assert len(result) == 0

    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_multiple_guests_in_same_booking(self, mock_build):
        g1 = _make_guest_mock(1)
        g2 = _make_guest_mock(2)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[g1, g2])
        idswh_index = {(1, 1): "tok1", (1, 2): "tok2"}
        mock_build.side_effect = [MagicMock(idswh="tok1"), MagicMock(idswh="tok2")]

        result = _build_day_guests([booking], date(2026, 5, 7), idswh_index, is_arrival=True)
        assert len(result) == 2


# ---------------------------------------------------------------------------
# build_movement_days() — unit tests with mocked DB
# ---------------------------------------------------------------------------

class TestBuildMovementDays:

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_one_day_per_calendar_day(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        mock_bookings.return_value = []
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 30),
        )
        assert len(days) == 30

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_empty_days_included(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        mock_bookings.return_value = []
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 5),
        )
        assert len(days) == 5
        for day in days:
            assert isinstance(day, Ross1000MovementDayPayload)
            assert len(day.arrivi) == 0
            assert len(day.partenze) == 0

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_struttura_block_present_on_every_day(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        mock_bookings.return_value = []
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="058091-CAV-00001", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 3),
        )
        for day in days:
            assert day.struttura is not None
            assert day.struttura.codice == "058091-CAV-00001"

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_days_are_sorted_by_date(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        mock_bookings.return_value = []
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 4, 1),
            end_date=date(2026, 4, 5),
        )
        dates = [d.date for d in days]
        assert dates == sorted(dates)

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_single_day_range(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        mock_bookings.return_value = []
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 4, 15),
            end_date=date(2026, 4, 15),
        )
        assert len(days) == 1
        assert days[0].date == date(2026, 4, 15)

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    @patch(f"{_BUILDER}.build_ross1000_guest_payload")
    def test_arrivals_on_checkin_departures_on_checkout(
        self, mock_guest, mock_struttura, mock_idswh, mock_bookings
    ):
        """Checkin 2026-05-07, checkout 2026-05-09:
        - 2026-05-07 → guest in arrivi
        - 2026-05-08 → empty
        - 2026-05-09 → guest in partenze
        """
        guest = _make_guest_mock(1)
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9), guests=[guest])
        mock_bookings.return_value = [booking]
        mock_idswh.return_value = {(1, 1): "token-abc"}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        mock_guest.return_value = MagicMock(idswh="token-abc")

        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 9),
        )
        assert len(days) == 3

        day_map = {d.date: d for d in days}
        assert len(day_map[date(2026, 5, 7)].arrivi) == 1
        assert len(day_map[date(2026, 5, 7)].partenze) == 0

        assert len(day_map[date(2026, 5, 8)].arrivi) == 0
        assert len(day_map[date(2026, 5, 8)].partenze) == 0

        assert len(day_map[date(2026, 5, 9)].arrivi) == 0
        assert len(day_map[date(2026, 5, 9)].partenze) == 1

    @patch(f"{_BUILDER}._fetch_bookings_for_range")
    @patch(f"{_BUILDER}._fetch_idswh_index")
    @patch(f"{_BUILDER}.build_struttura_payload")
    def test_occupied_rooms_calculated_per_day(
        self, mock_struttura, mock_idswh, mock_bookings
    ):
        """Occupied rooms = bookings where check_in <= day < check_out."""
        booking = _make_booking(1, date(2026, 5, 7), date(2026, 5, 9))
        mock_bookings.return_value = [booking]
        mock_idswh.return_value = {}
        mock_struttura.return_value = MagicMock(
            codice="TEST", apertura=1, camere_occupate=0,
            camere_disponibili=6, letti_disponibili=20,
        )
        days = build_movement_days(
            structure=_make_structure(),
            start_date=date(2026, 5, 7),
            end_date=date(2026, 5, 9),
        )
        day_map = {d.date: d for d in days}
        assert day_map[date(2026, 5, 7)].struttura.camere_occupate == 1
        assert day_map[date(2026, 5, 8)].struttura.camere_occupate == 1
        assert day_map[date(2026, 5, 9)].struttura.camere_occupate == 0

    def test_raises_when_structure_missing_istat_code(self):
        with pytest.raises(Ross1000ValidationError):
            build_movement_days(
                structure=_make_structure(istat_code=""),
                start_date=date(2026, 4, 1),
                end_date=date(2026, 4, 5),
            )

    def test_raises_when_end_before_start(self):
        with pytest.raises(Ross1000ValidationError):
            build_movement_days(
                structure=_make_structure(),
                start_date=date(2026, 4, 10),
                end_date=date(2026, 4, 5),
            )
