"""
alloggiati/tests_validation_errors.py
======================================
Django TestCase suite for structured guest validation error reporting.

Tests cover all 8 required scenarios from the JIRA spec:
  A. Single invalid guest — missing DOB
  B. Multiple field failures — all errors returned
  C. Mixed valid + invalid guests — PARTIAL status, valid guests still sent
  D. Invalid normalization — bad nationality / document type
  E. Validation persistence — validation_errors saved in DB
  F. Security — credentials never exposed in errors
  G. API response shape — exact structure validated
  H. Empty validation_errors on success

Run with:
    python manage.py test alloggiati.tests_validation_errors --verbosity=2
"""

from __future__ import annotations

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

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

from alloggiati.models import AlloggiatiCredential, AlloggiatiSyncLog
from alloggiati.transformer import (
    collect_guest_validation_errors,
    reason_to_error,
    transform_booking_guests,
)
from bookings.models import Booking
from guests.models import Guest
from istat.models import IstatCountry, IstatDocumentType
from properties.models import Property, PropertyType
from structures.models import Structure


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------

def _make_user(username="teststaff"):
    return User.objects.create_user(username=username, password="pass")


def _make_structure(user, name="Hotel Roma", istat_code="ISTAT01"):
    return Structure.objects.create(
        user=user,
        name=name,
        structure_type="Hotel",
        zip_code="00100",
        country="Italy",
        istat_code=istat_code,
    )


def _make_booking(structure, check_in=None, check_out=None):
    pt = PropertyType.objects.create(
        structure=structure, name="Standard", num_beds=1, num_sofa_beds=0
    )
    prop = Property.objects.create(
        structure=structure, property_type=pt, name="Room 1"
    )
    return Booking.objects.create(
        structure=structure,
        property_type=pt,
        property=prop,
        check_in_date=check_in or datetime.date(2026, 7, 1),
        check_out_date=check_out or datetime.date(2026, 7, 5),
        adults_count=1,
        is_checked_in=True,
        guest_group_type="single",
        tourism_type="Cultural",
        transport_type="Car",
    )


def _make_valid_guest(booking, name="Mario Rossi", suffix=""):
    """Create a guest that passes all Alloggiati validation."""
    IstatCountry.objects.get_or_create(
        code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
    )
    IstatDocumentType.objects.get_or_create(
        code="PASS", defaults={"description": "Passport"}
    )
    return Guest.objects.create(
        booking=booking,
        full_name=name,
        is_main_guest=True,
        guest_type="16",
        date_of_birth=datetime.date(1985, 6, 15),
        gender="male",
        nationality="Italy",
        country_of_birth="Italy",
        document_type="passport",
        document_issuing_country="Italy",
        tourism_type="Cultural",
        transport_type="Car",
        extra_data={"document_number": f"PASS{suffix}001"},
    )


def _make_credential(structure):
    cred = AlloggiatiCredential(structure=structure, mode=AlloggiatiCredential.MODE_CODES)
    cred.username = "testuser"
    cred.password = "testpass"
    cred.save()
    return cred


# ---------------------------------------------------------------------------
# A. Single invalid guest — missing DOB
# ---------------------------------------------------------------------------

class SingleInvalidGuestTest(TestCase):
    """
    TEST A: A guest missing date_of_birth produces a validation_errors entry
    with the correct field and human-readable message.
    """

    def setUp(self):
        self.user = _make_user("user_a")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        self.guest = Guest.objects.create(
            booking=self.booking,
            full_name="John Test",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=None,          # ← missing
            gender="male",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS123"},
        )

    def test_collect_errors_returns_missing_dob(self):
        errors = collect_guest_validation_errors(self.guest)
        fields = [e["field"] for e in errors]
        self.assertIn("date_of_birth", fields)

    def test_error_message_is_human_readable(self):
        errors = collect_guest_validation_errors(self.guest)
        dob_error = next(e for e in errors if e["field"] == "date_of_birth")
        self.assertIn("required", dob_error["message"].lower())
        self.assertNotIn("None", dob_error["message"])
        self.assertNotIn("AttributeError", dob_error["message"])

    def test_transform_marks_guest_invalid(self):
        result = transform_booking_guests(self.booking)
        self.assertEqual(len(result["valid"]), 0)
        self.assertEqual(len(result["invalid"]), 1)

    def test_invalid_record_has_structured_errors(self):
        result = transform_booking_guests(self.booking)
        rec = result["invalid"][0]
        self.assertIn("errors", rec)
        self.assertIsInstance(rec["errors"], list)
        self.assertGreater(len(rec["errors"]), 0)

    def test_invalid_record_has_guest_metadata(self):
        result = transform_booking_guests(self.booking)
        rec = result["invalid"][0]
        self.assertEqual(rec["guest_id"], self.guest.id)
        self.assertEqual(rec["booking_id"], self.booking.id)
        self.assertEqual(rec["guest_name"], "John Test")

    def test_guest_name_does_not_contain_document_number(self):
        result = transform_booking_guests(self.booking)
        rec = result["invalid"][0]
        self.assertNotIn("PASS123", rec["guest_name"])


# ---------------------------------------------------------------------------
# B. Multiple field failures — all errors returned
# ---------------------------------------------------------------------------

class MultipleFieldFailuresTest(TestCase):
    """
    TEST B: A guest missing multiple required fields produces ALL errors,
    not just the first one.
    """

    def setUp(self):
        self.user = _make_user("user_b")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        self.guest = Guest.objects.create(
            booking=self.booking,
            full_name="Jane Missing",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=None,           # missing
            gender="male",
            nationality=None,             # missing
            country_of_birth="Italy",
            document_type=None,           # missing
            document_issuing_country=None,# missing
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={},                # no document_number either
        )

    def test_all_errors_collected(self):
        errors = collect_guest_validation_errors(self.guest)
        fields = [e["field"] for e in errors]
        self.assertIn("date_of_birth", fields)
        self.assertIn("nationality", fields)
        self.assertIn("document_type", fields)
        self.assertIn("id_number", fields)

    def test_returns_more_than_one_error(self):
        errors = collect_guest_validation_errors(self.guest)
        self.assertGreater(len(errors), 1)

    def test_each_error_has_field_and_message(self):
        errors = collect_guest_validation_errors(self.guest)
        for err in errors:
            self.assertIn("field", err)
            self.assertIn("message", err)
            self.assertIsInstance(err["field"], str)
            self.assertIsInstance(err["message"], str)
            self.assertTrue(err["field"])
            self.assertTrue(err["message"])

    def test_no_stack_traces_in_messages(self):
        errors = collect_guest_validation_errors(self.guest)
        for err in errors:
            self.assertNotIn("Traceback", err["message"])
            self.assertNotIn("Error", err["message"])
            self.assertNotIn("Exception", err["message"])


# ---------------------------------------------------------------------------
# C. Mixed valid + invalid guests — PARTIAL status
# ---------------------------------------------------------------------------

class MixedGuestsTest(TestCase):
    """
    TEST C: When a booking has both valid and invalid guests, valid guests
    are transformed successfully and invalid guests produce structured errors.
    The overall result is PARTIAL.
    """

    def setUp(self):
        self.user = _make_user("user_c")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        # Valid guest
        self.valid_guest = _make_valid_guest(self.booking, name="Valid Person", suffix="V")
        # Invalid guest — missing DOB and document number
        self.invalid_guest = Guest.objects.create(
            booking=self.booking,
            full_name="Invalid Person",
            is_main_guest=False,
            guest_type="16",
            date_of_birth=None,
            gender="female",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={},
        )

    def test_valid_guest_in_valid_bucket(self):
        result = transform_booking_guests(self.booking)
        self.assertEqual(len(result["valid"]), 1)

    def test_invalid_guest_in_invalid_bucket(self):
        result = transform_booking_guests(self.booking)
        self.assertEqual(len(result["invalid"]), 1)

    def test_invalid_bucket_has_structured_errors(self):
        result = transform_booking_guests(self.booking)
        rec = result["invalid"][0]
        self.assertGreater(len(rec["errors"]), 0)

    def test_valid_guest_payload_has_required_fields(self):
        result = transform_booking_guests(self.booking)
        payload = result["valid"][0]
        for field in ("nationality", "document_type", "document_number",
                      "date_of_birth", "structure_id", "booking_reference"):
            self.assertTrue(payload.get(field), f"Missing field: {field}")

    def test_service_returns_partial_status_via_api(self):
        """
        End-to-end: POST /api/alloggiati/sync with mixed guests returns
        PARTIAL status and populates validation_errors.
        """
        _make_credential(self.structure)
        client = APIClient()
        client.force_authenticate(user=self.user)

        mock_send = MagicMock(return_value={
            "success": True, "accepted": 1, "rejected": 0, "raw_message": ""
        })

        with patch("alloggiati.client.AlloggiatiClient.send_guests", mock_send):
            with patch("alloggiati.client.AlloggiatiClient.from_codes",
                       return_value=MagicMock(send_guests=mock_send)):
                response = client.post(
                    "/api/alloggiati/sync/",
                    {
                        "structureId": self.structure.id,
                        "dateFrom": "2026-07-01",
                        "dateTo": "2026-07-05",
                    },
                    format="json",
                )

        self.assertIn(response.status_code, [200, 502])
        data = response.json()
        self.assertIn("validation_errors", data)
        self.assertIsInstance(data["validation_errors"], list)


# ---------------------------------------------------------------------------
# D. Invalid normalization — bad nationality / document type
# ---------------------------------------------------------------------------

class InvalidNormalizationTest(TestCase):
    """
    TEST D: A guest with an unrecognisable nationality or document type
    produces a specific, human-readable normalization error.
    """

    def setUp(self):
        self.user = _make_user("user_d")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )

    def test_invalid_nationality_produces_normalization_error(self):
        guest = Guest.objects.create(
            booking=self.booking,
            full_name="Bad Nationality",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=datetime.date(1990, 1, 1),
            gender="male",
            nationality="Atlantis",       # ← unresolvable
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS999"},
        )
        errors = collect_guest_validation_errors(guest)
        fields = [e["field"] for e in errors]
        self.assertIn("nationality", fields)
        nat_error = next(e for e in errors if e["field"] == "nationality")
        self.assertIn("country code", nat_error["message"].lower())

    def test_invalid_document_type_produces_normalization_error(self):
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        guest = Guest.objects.create(
            booking=self.booking,
            full_name="Bad DocType",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=datetime.date(1990, 1, 1),
            gender="male",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="magic_scroll",  # ← unrecognised
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS999"},
        )
        errors = collect_guest_validation_errors(guest)
        fields = [e["field"] for e in errors]
        self.assertIn("document_type", fields)
        doc_error = next(e for e in errors if e["field"] == "document_type")
        self.assertNotIn("magic_scroll", doc_error["message"])  # raw value not leaked

    def test_normalization_error_message_is_staff_readable(self):
        guest = Guest.objects.create(
            booking=self.booking,
            full_name="Norm Error Guest",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=datetime.date(1990, 1, 1),
            gender="male",
            nationality="Narnia",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS999"},
        )
        errors = collect_guest_validation_errors(guest)
        for err in errors:
            # Must not contain internal reason codes
            self.assertNotIn("invalid_nationality_code", err["message"])
            self.assertNotIn("invalid_document_type", err["message"])
            # Must not contain raw input values
            self.assertNotIn("Narnia", err["message"])


# ---------------------------------------------------------------------------
# E. Validation persistence — validation_errors saved in DB
# ---------------------------------------------------------------------------

class ValidationPersistenceTest(TestCase):
    """
    TEST E: validation_errors are persisted in AlloggiatiSyncLog and can
    be retrieved from the DB for audit trail and frontend rendering.
    """

    def setUp(self):
        self.user = _make_user("user_e")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        _make_credential(self.structure)
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        Guest.objects.create(
            booking=self.booking,
            full_name="Persist Test",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=None,           # will fail
            gender="male",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS001"},
        )

    def test_validation_errors_persisted_in_sync_log(self):
        client = APIClient()
        client.force_authenticate(user=self.user)

        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 0, "rejected": 0, "raw_message": ""
        }

        with patch("alloggiati.service._build_client", return_value=mock_client):
            client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

        log = AlloggiatiSyncLog.objects.filter(structure=self.structure).first()
        self.assertIsNotNone(log)
        self.assertIsInstance(log.validation_errors, list)
        self.assertGreater(len(log.validation_errors), 0)

    def test_persisted_errors_have_correct_schema(self):
        client = APIClient()
        client.force_authenticate(user=self.user)

        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 0, "rejected": 0, "raw_message": ""
        }

        with patch("alloggiati.service._build_client", return_value=mock_client):
            client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

        log = AlloggiatiSyncLog.objects.filter(structure=self.structure).first()
        for entry in log.validation_errors:
            self.assertIn("guest_id", entry)
            self.assertIn("booking_id", entry)
            self.assertIn("guest_name", entry)
            self.assertIn("errors", entry)
            for err in entry["errors"]:
                self.assertIn("field", err)
                self.assertIn("message", err)

    def test_successful_sync_persists_empty_validation_errors(self):
        # Replace invalid guest with a valid one
        Guest.objects.filter(booking=self.booking).delete()
        _make_valid_guest(self.booking, name="Clean Guest")

        client = APIClient()
        client.force_authenticate(user=self.user)

        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 1, "rejected": 0, "raw_message": ""
        }

        with patch("alloggiati.service._build_client", return_value=mock_client):
            client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

        log = AlloggiatiSyncLog.objects.filter(structure=self.structure).first()
        self.assertIsNotNone(log)
        self.assertEqual(log.validation_errors, [])


# ---------------------------------------------------------------------------
# F. Security — credentials never exposed
# ---------------------------------------------------------------------------

class SecurityTest(TestCase):
    """
    TEST F: Credentials, document numbers, and stack traces are never
    included in validation_errors or API responses.
    """

    def setUp(self):
        self.user = _make_user("user_f")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        cred = _make_credential(self.structure)
        self.username = "secretuser"
        self.password = "supersecretpassword"
        cred.username = self.username
        cred.password = self.password
        cred.save()
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        self.doc_number = "SECRETDOC9999"
        Guest.objects.create(
            booking=self.booking,
            full_name="Security Test Guest",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=None,
            gender="male",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": self.doc_number},
        )

    def _get_sync_response(self):
        api_client = APIClient()
        api_client.force_authenticate(user=self.user)
        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 0, "rejected": 0, "raw_message": ""
        }
        with patch("alloggiati.service._build_client", return_value=mock_client):
            return api_client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

    def test_password_not_in_response(self):
        response = self._get_sync_response()
        self.assertNotIn(self.password, str(response.content))

    def test_username_not_in_response(self):
        response = self._get_sync_response()
        self.assertNotIn(self.username, str(response.content))

    def test_document_number_not_in_error_messages(self):
        response = self._get_sync_response()
        data = response.json()
        for entry in data.get("validation_errors", []):
            for err in entry.get("errors", []):
                self.assertNotIn(self.doc_number, err["message"])

    def test_no_stack_traces_in_response(self):
        response = self._get_sync_response()
        content = str(response.content)
        self.assertNotIn("Traceback", content)
        self.assertNotIn("File \"", content)

    def test_no_credentials_in_persisted_log(self):
        self._get_sync_response()
        log = AlloggiatiSyncLog.objects.filter(structure=self.structure).first()
        log_str = str(log.validation_errors)
        self.assertNotIn(self.password, log_str)
        self.assertNotIn(self.username, log_str)


# ---------------------------------------------------------------------------
# G. API response shape — exact structure validated
# ---------------------------------------------------------------------------

class APIResponseShapeTest(TestCase):
    """
    TEST G: The sync API response always contains the exact expected fields
    with the correct types, regardless of outcome.
    """

    def setUp(self):
        self.user = _make_user("user_g")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        _make_credential(self.structure)
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        Guest.objects.create(
            booking=self.booking,
            full_name="Shape Test Guest",
            is_main_guest=True,
            guest_type="16",
            date_of_birth=None,
            gender="male",
            nationality="Italy",
            country_of_birth="Italy",
            document_type="passport",
            document_issuing_country="Italy",
            tourism_type="Cultural",
            transport_type="Car",
            extra_data={"document_number": "PASS001"},
        )
        self.api_client = APIClient()
        self.api_client.force_authenticate(user=self.user)

    def _call_sync(self):
        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 0, "rejected": 0, "raw_message": ""
        }
        with patch("alloggiati.service._build_client", return_value=mock_client):
            return self.api_client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

    def test_top_level_fields_present(self):
        data = self._call_sync().json()
        for field in ("status", "sent", "rejected", "message",
                      "sync_log_id", "validation_errors"):
            self.assertIn(field, data, f"Missing top-level field: {field}")

    def test_validation_errors_is_list(self):
        data = self._call_sync().json()
        self.assertIsInstance(data["validation_errors"], list)

    def test_validation_error_entry_shape(self):
        data = self._call_sync().json()
        self.assertGreater(len(data["validation_errors"]), 0)
        entry = data["validation_errors"][0]
        self.assertIn("guest_id", entry)
        self.assertIn("booking_id", entry)
        self.assertIn("guest_name", entry)
        self.assertIn("errors", entry)

    def test_field_error_shape(self):
        data = self._call_sync().json()
        entry = data["validation_errors"][0]
        self.assertGreater(len(entry["errors"]), 0)
        err = entry["errors"][0]
        self.assertIn("field", err)
        self.assertIn("message", err)
        self.assertIsInstance(err["field"], str)
        self.assertIsInstance(err["message"], str)

    def test_status_is_valid_choice(self):
        data = self._call_sync().json()
        self.assertIn(data["status"], ("CONNECTED", "PARTIAL", "ERROR"))

    def test_sent_and_rejected_are_integers(self):
        data = self._call_sync().json()
        self.assertIsInstance(data["sent"], int)
        self.assertIsInstance(data["rejected"], int)


# ---------------------------------------------------------------------------
# H. Empty validation_errors on full success
# ---------------------------------------------------------------------------

class EmptyValidationErrorsOnSuccessTest(TestCase):
    """
    TEST H: When all guests pass validation and are accepted by the portal,
    validation_errors is an empty list in both the response and the DB log.
    """

    def setUp(self):
        self.user = _make_user("user_h")
        self.structure = _make_structure(self.user)
        self.booking = _make_booking(self.structure)
        _make_credential(self.structure)
        IstatCountry.objects.get_or_create(
            code="000000100", defaults={"name": "Italy", "iso_code": "IT"}
        )
        IstatDocumentType.objects.get_or_create(
            code="PASS", defaults={"description": "Passport"}
        )
        _make_valid_guest(self.booking, name="Perfect Guest")
        self.api_client = APIClient()
        self.api_client.force_authenticate(user=self.user)

    def _call_sync(self):
        mock_client = MagicMock()
        mock_client.send_guests.return_value = {
            "success": True, "accepted": 1, "rejected": 0, "raw_message": ""
        }
        with patch("alloggiati.service._build_client", return_value=mock_client):
            return self.api_client.post(
                "/api/alloggiati/sync/",
                {
                    "structureId": self.structure.id,
                    "dateFrom": "2026-07-01",
                    "dateTo": "2026-07-05",
                },
                format="json",
            )

    def test_response_validation_errors_is_empty(self):
        data = self._call_sync().json()
        self.assertEqual(data["validation_errors"], [])

    def test_response_status_is_connected(self):
        data = self._call_sync().json()
        self.assertEqual(data["status"], "CONNECTED")

    def test_db_log_validation_errors_is_empty(self):
        self._call_sync()
        log = AlloggiatiSyncLog.objects.filter(structure=self.structure).first()
        self.assertIsNotNone(log)
        self.assertEqual(log.validation_errors, [])

    def test_sent_count_is_correct(self):
        data = self._call_sync().json()
        self.assertEqual(data["sent"], 1)
        self.assertEqual(data["rejected"], 0)


# ---------------------------------------------------------------------------
# I. reason_to_error helper — unit tests
# ---------------------------------------------------------------------------

class ReasonToErrorTest(TestCase):
    """Unit tests for the reason_to_error() helper."""

    def test_known_reason_returns_correct_field(self):
        err = reason_to_error("missing_date_of_birth")
        self.assertEqual(err["field"], "date_of_birth")

    def test_known_reason_returns_human_message(self):
        err = reason_to_error("missing_id_number")
        self.assertIn("required", err["message"].lower())

    def test_unknown_reason_returns_safe_fallback(self):
        err = reason_to_error("some_future_reason_code")
        self.assertIn("field", err)
        self.assertIn("message", err)
        self.assertNotIn("some_future_reason_code", err["message"])

    def test_all_defined_reasons_have_non_empty_messages(self):
        from alloggiati.transformer import _REASON_MESSAGES
        for code, mapping in _REASON_MESSAGES.items():
            self.assertTrue(mapping["field"], f"Empty field for {code}")
            self.assertTrue(mapping["message"], f"Empty message for {code}")
