"""Tests for Ross1000 payload validators."""

from __future__ import annotations

from datetime import date
from unittest.mock import MagicMock

import pytest

from istat.ross1000.exceptions import (
    Ross1000GuestError,
    Ross1000StructureError,
    Ross1000ValidationError,
)
from istat.ross1000.validators.payload_validator import (
    require,
    validate_date_range,
    validate_guest_for_ross1000,
    validate_idswh,
    validate_no_duplicate_idswh,
    validate_structure,
)


# ---------------------------------------------------------------------------
# require()
# ---------------------------------------------------------------------------

class TestRequire:
    def test_passes_for_non_blank_string(self):
        require("hello", "field")  # no exception

    def test_passes_for_integer(self):
        require(42, "field")

    def test_raises_for_none(self):
        with pytest.raises(Ross1000ValidationError, match="field"):
            require(None, "field")

    def test_raises_for_empty_string(self):
        with pytest.raises(Ross1000ValidationError, match="field"):
            require("", "field")

    def test_raises_for_whitespace_string(self):
        with pytest.raises(Ross1000ValidationError, match="field"):
            require("   ", "field")

    def test_context_included_in_message(self):
        with pytest.raises(Ross1000ValidationError, match=r"\[ctx\]"):
            require(None, "field", context="ctx")


# ---------------------------------------------------------------------------
# validate_date_range()
# ---------------------------------------------------------------------------

class TestValidateDateRange:
    def test_valid_same_day_range(self):
        d = date(2026, 4, 1)
        start, end = validate_date_range(d, d)
        assert start == d
        assert end == d

    def test_valid_multi_day_range(self):
        start, end = validate_date_range(date(2026, 4, 1), date(2026, 4, 30))
        assert start == date(2026, 4, 1)
        assert end == date(2026, 4, 30)

    def test_raises_when_end_before_start(self):
        with pytest.raises(Ross1000ValidationError, match="end_date"):
            validate_date_range(date(2026, 4, 10), date(2026, 4, 5))

    def test_raises_when_start_is_none(self):
        with pytest.raises(Ross1000ValidationError, match="start_date"):
            validate_date_range(None, date(2026, 4, 30))

    def test_raises_when_end_is_none(self):
        with pytest.raises(Ross1000ValidationError, match="end_date"):
            validate_date_range(date(2026, 4, 1), None)

    def test_raises_when_start_is_not_date(self):
        with pytest.raises(Ross1000ValidationError):
            validate_date_range("2026-04-01", date(2026, 4, 30))  # type: ignore


# ---------------------------------------------------------------------------
# validate_structure()
# ---------------------------------------------------------------------------

class TestValidateStructure:
    def test_passes_when_istat_code_present(self):
        structure = MagicMock()
        structure.istat_code = "058091-CAV-00001"
        validate_structure(structure)  # no exception

    def test_raises_when_istat_code_missing(self):
        structure = MagicMock()
        structure.istat_code = ""
        structure.id = 99
        with pytest.raises(Ross1000StructureError, match="istat_code"):
            validate_structure(structure)

    def test_raises_when_istat_code_is_none(self):
        structure = MagicMock()
        structure.istat_code = None
        structure.id = 99
        with pytest.raises(Ross1000StructureError):
            validate_structure(structure)

    def test_raises_when_istat_code_is_whitespace(self):
        structure = MagicMock()
        structure.istat_code = "   "
        structure.id = 99
        with pytest.raises(Ross1000StructureError):
            validate_structure(structure)


# ---------------------------------------------------------------------------
# validate_idswh()
# ---------------------------------------------------------------------------

class TestValidateIdswh:
    def test_returns_idswh_when_valid(self):
        result = validate_idswh("abc123def456", guest_id=1, booking_id=2)
        assert result == "abc123def456"

    def test_strips_whitespace(self):
        result = validate_idswh("  abc123  ", guest_id=1, booking_id=2)
        assert result == "abc123"

    def test_raises_when_none(self):
        with pytest.raises(Ross1000ValidationError, match="idswh"):
            validate_idswh(None, guest_id=1, booking_id=2)

    def test_raises_when_empty(self):
        with pytest.raises(Ross1000ValidationError, match="idswh"):
            validate_idswh("", guest_id=1, booking_id=2)

    def test_raises_when_whitespace(self):
        with pytest.raises(Ross1000ValidationError, match="idswh"):
            validate_idswh("   ", guest_id=1, booking_id=2)

    def test_error_message_includes_guest_and_booking_ids(self):
        with pytest.raises(Ross1000ValidationError, match="guest 7") as exc_info:
            validate_idswh(None, guest_id=7, booking_id=42)
        assert "booking 42" in str(exc_info.value)


# ---------------------------------------------------------------------------
# validate_guest_for_ross1000()
# ---------------------------------------------------------------------------

def _make_valid_guest(**overrides):
    guest = MagicMock()
    guest.id = 1
    guest.full_name = "ROSSI MARIO"
    guest.date_of_birth = date(1983, 2, 9)
    guest.gender = "male"
    guest.country_of_birth = "IT"
    guest.country = "IT"
    guest.nationality = "IT"
    for key, value in overrides.items():
        setattr(guest, key, value)
    return guest


class TestValidateGuestForRoss1000:
    def test_passes_for_valid_guest(self):
        guest = _make_valid_guest()
        validate_guest_for_ross1000(guest, booking_id=1)  # no exception

    def test_raises_when_full_name_missing(self):
        guest = _make_valid_guest(full_name="")
        with pytest.raises(Ross1000GuestError, match="full_name"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_dob_missing(self):
        guest = _make_valid_guest(date_of_birth=None)
        with pytest.raises(Ross1000GuestError, match="date_of_birth"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_gender_missing(self):
        guest = _make_valid_guest(gender="")
        with pytest.raises(Ross1000GuestError, match="gender"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_gender_invalid(self):
        guest = _make_valid_guest(gender="unknown")
        with pytest.raises(Ross1000GuestError, match="gender"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_passes_for_female_guest(self):
        guest = _make_valid_guest(gender="female")
        validate_guest_for_ross1000(guest, booking_id=1)  # no exception

    def test_raises_when_country_of_birth_missing(self):
        guest = _make_valid_guest(country_of_birth="")
        with pytest.raises(Ross1000GuestError, match="country_of_birth"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_country_of_birth_unknown(self):
        guest = _make_valid_guest(country_of_birth="ZZ")
        with pytest.raises(Ross1000GuestError, match="country_of_birth"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_residence_country_missing(self):
        guest = _make_valid_guest(country="")
        with pytest.raises(Ross1000GuestError, match="country"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_raises_when_nationality_missing(self):
        guest = _make_valid_guest(nationality="")
        with pytest.raises(Ross1000GuestError, match="nationality"):
            validate_guest_for_ross1000(guest, booking_id=1)

    def test_error_includes_guest_and_booking_context(self):
        guest = _make_valid_guest(full_name="", id=5)
        with pytest.raises(Ross1000GuestError, match="guest 5") as exc_info:
            validate_guest_for_ross1000(guest, booking_id=99)
        assert "booking 99" in str(exc_info.value)

    def test_passes_for_foreign_guest(self):
        guest = _make_valid_guest(
            country_of_birth="DE",
            country="DE",
            nationality="DE",
        )
        validate_guest_for_ross1000(guest, booking_id=1)  # no exception


# ---------------------------------------------------------------------------
# validate_no_duplicate_idswh()
# ---------------------------------------------------------------------------

class TestValidateNoDuplicateIdswh:
    def _make_payload(self, idswh: str):
        p = MagicMock()
        p.idswh = idswh
        return p

    def test_passes_for_empty_list(self):
        validate_no_duplicate_idswh([])  # no exception

    def test_passes_for_unique_tokens(self):
        payloads = [self._make_payload(f"token{i}") for i in range(5)]
        validate_no_duplicate_idswh(payloads)  # no exception

    def test_raises_on_duplicate(self):
        payloads = [
            self._make_payload("abc"),
            self._make_payload("xyz"),
            self._make_payload("abc"),  # duplicate
        ]
        with pytest.raises(Ross1000ValidationError, match="abc"):
            validate_no_duplicate_idswh(payloads)

    def test_error_message_includes_context(self):
        payloads = [self._make_payload("dup"), self._make_payload("dup")]
        with pytest.raises(Ross1000ValidationError, match="my_export"):
            validate_no_duplicate_idswh(payloads, context="my_export")

    def test_passes_for_single_payload(self):
        validate_no_duplicate_idswh([self._make_payload("only_one")])
