"""RED contracts for privacy-minimizing nutrition-onboarding value models."""

from __future__ import annotations

import importlib.util
from datetime import date
from decimal import Decimal
from enum import StrEnum

import pytest
from pydantic import ValidationError


MODULE = "checkin_cli.nutrition_onboarding_models"
assert importlib.util.find_spec(MODULE) is not None, (
    "nutrition onboarding contract missing: "
    "checkin_cli.nutrition_onboarding_models"
)

from checkin_cli.nutrition_onboarding_models import (  # noqa: E402
    ActivityCategory,
    EquationSexBasis,
    GoalType,
    NutritionOnboardingBaseline,
    OnboardingSessionStatus,
    PublicationStatus,
    ReviewDecision,
    StructuredItemStatus,
    StructuredItems,
    build_onboarding_baseline,
    canonical_equation_sex_basis,
    derive_adult_age,
)


def _none() -> StructuredItems:
    return StructuredItems(status=StructuredItemStatus.NONE, items=())


def _baseline(**overrides: object) -> NutritionOnboardingBaseline:
    values: dict[str, object] = {
        "schema_version": "1.0",
        "customer_key": "client_001",
        "adult_age": 30,
        "equation_sex_basis": EquationSexBasis.MALE,
        "height_cm": Decimal("180.50"),
        "weight_kg": Decimal("80.00"),
        "activity_category": ActivityCategory.MODERATE,
        "goal_type": GoalType.MAINTAIN,
        "target_weight_kg": None,
        "target_date": None,
        "dietary_preferences": _none(),
        "disliked_foods": _none(),
        "allergies": _none(),
        "intolerances": _none(),
        "religious_ethical_exclusions": _none(),
        "conditions": _none(),
        "medications": _none(),
        "session_status": OnboardingSessionStatus.COMPLETED,
        "review_decision": ReviewDecision.PENDING,
        "publication_status": PublicationStatus.UNPUBLISHED,
    }
    values.update(overrides)
    return NutritionOnboardingBaseline(**values)


def _raw_answers(**overrides: object) -> dict[str, object]:
    values: dict[str, object] = {
        "schema_version": "1.0",
        "customer_key": "client_001",
        "date_of_birth": "1996-08-01",
        "equation_sex_basis": "female",
        "height_cm": "165.50",
        "weight_kg": "65.00",
        "activity_category": "light",
        "goal_type": "maintain",
        "target_weight_kg": None,
        "target_date": None,
        "dietary_preferences": {"status": "none", "items": []},
        "disliked_foods": {"status": "none", "items": []},
        "allergies": {"status": "none", "items": []},
        "intolerances": {"status": "none", "items": []},
        "religious_ethical_exclusions": {"status": "none", "items": []},
        "conditions": {"status": "none", "items": []},
        "medications": {"status": "none", "items": []},
        "session_status": "completed",
        "review_decision": "pending",
        "publication_status": "unpublished",
    }
    values.update(overrides)
    return values


def test_workflow_enums_are_closed_string_contracts() -> None:
    assert issubclass(OnboardingSessionStatus, StrEnum)
    assert [item.value for item in OnboardingSessionStatus] == [
        "in_progress",
        "completed",
        "cancelled",
    ]
    assert [item.value for item in ReviewDecision] == [
        "pending",
        "approved",
        "rejected",
    ]
    assert [item.value for item in PublicationStatus] == [
        "unpublished",
        "published",
        "withdrawn",
    ]
    assert [item.value for item in EquationSexBasis] == [
        "male",
        "female",
        "decline",
    ]


@pytest.mark.parametrize(
    ("value", "expected"),
    (
        ("male", "male"),
        ("Female", "female"),
        ("decline", "decline"),
        ("남성", "male"),
        ("여성입니다", "female"),
        ("선택 안 합니다", "decline"),
        ("넵 남성입니다", "male"),
        ("네, 여성입니다", "female"),
        ("Yes, male", "male"),
        ("Yep female", "female"),
        ("I prefer not to say", "decline"),
    ),
)
def test_equation_sex_basis_normalizes_only_exact_supported_answers(
    value: str,
    expected: str,
) -> None:
    assert canonical_equation_sex_basis(value) == expected


@pytest.mark.parametrize(
    "value",
    (
        "남성이라는 단어가 포함됨",
        "female-ish",
        "maybe male",
        "선택 안 함이라고 쓰인 설명",
        1,
        None,
    ),
)
def test_equation_sex_basis_does_not_guess_from_substrings(value: object) -> None:
    assert canonical_equation_sex_basis(value) is None


def test_every_public_schema_is_frozen_and_forbids_extra_fields() -> None:
    baseline = _baseline()
    structured = _none()

    with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
        NutritionOnboardingBaseline(**baseline.model_dump(), surprise=True)
    with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
        StructuredItems(status="none", items=(), surprise=True)
    with pytest.raises(ValidationError, match="frozen"):
        baseline.adult_age = 31
    with pytest.raises(ValidationError, match="frozen"):
        structured.items = ("changed",)


def test_decimal_fields_serialize_as_canonical_decimal_strings() -> None:
    payload = _baseline().model_dump(mode="json")

    assert payload["height_cm"] == "180.5"
    assert payload["weight_kg"] == "80"
    assert payload["target_weight_kg"] is None
    assert not isinstance(payload["height_cm"], float)
    assert not isinstance(payload["weight_kg"], float)


def test_adult_age_uses_a_supplied_date_and_raw_dob_is_discarded() -> None:
    assert derive_adult_age(
        date(2008, 8, 1),
        as_of=date(2026, 8, 1),
    ) == 18
    assert derive_adult_age(
        date(1996, 8, 2),
        as_of=date(2026, 8, 1),
    ) == 29

    baseline = build_onboarding_baseline(
        _raw_answers(),
        as_of=date(2026, 8, 1),
    )

    assert baseline.adult_age == 30
    assert "date_of_birth" not in type(baseline).model_fields
    assert "date_of_birth" not in baseline.model_dump(mode="json")
    assert "1996-08-01" not in baseline.model_dump_json()


def test_minor_and_implausible_adult_ages_are_rejected() -> None:
    with pytest.raises(ValueError, match="adult"):
        derive_adult_age(
            date(2008, 8, 2),
            as_of=date(2026, 8, 1),
        )
    with pytest.raises(ValidationError):
        _baseline(adult_age=17)
    with pytest.raises(ValidationError):
        _baseline(adult_age=121)


def test_raw_dob_cannot_be_reintroduced_into_the_canonical_schema() -> None:
    with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
        NutritionOnboardingBaseline(
            **_baseline().model_dump(),
            date_of_birth="1996-08-01",
        )


def test_structured_none_is_explicit_and_not_an_empty_or_ambiguous_answer() -> None:
    assert _none().model_dump(mode="json") == {
        "status": "none",
        "items": [],
    }

    with pytest.raises(ValidationError, match="none"):
        StructuredItems(status="none", items=("peanut",))
    with pytest.raises(ValidationError, match="provided"):
        StructuredItems(status="provided", items=())
    with pytest.raises(ValidationError):
        StructuredItems(items=())
    assert StructuredItems(
        status="unknown",
        items=(),
    ).model_dump(mode="json") == {
        "status": "unknown",
        "items": [],
    }
    with pytest.raises(ValidationError, match="unknown"):
        StructuredItems(status="unknown", items=("uncertain",))


def test_structured_items_trim_nfc_normalize_and_deduplicate_in_order() -> None:
    answer = StructuredItems(
        status="provided",
        items=("  Café  ", "Cafe\u0301", "두부", " 두부 "),
    )

    assert answer.items == ("Café", "두부")
    assert all(
        item == __import__("unicodedata").normalize("NFC", item)
        for item in answer.items
    )


@pytest.mark.parametrize("bad_item", ["", "   ", "x" * 201])
def test_structured_item_text_bounds_are_enforced(bad_item: str) -> None:
    with pytest.raises(ValidationError):
        StructuredItems(status="provided", items=(bad_item,))


def test_structured_item_count_bound_is_enforced_after_normalization() -> None:
    with pytest.raises(ValidationError):
        StructuredItems(
            status="provided",
            items=tuple(f"item-{index}" for index in range(41)),
        )


def test_anthropometric_and_goal_bounds_are_closed() -> None:
    for field, value in (
        ("height_cm", "119.9"),
        ("height_cm", "250.1"),
        ("weight_kg", "34.9"),
        ("weight_kg", "300.1"),
    ):
        with pytest.raises(ValidationError):
            _baseline(**{field: Decimal(value)})

    with pytest.raises(ValidationError, match="target"):
        _baseline(goal_type=GoalType.LOSS)
    with pytest.raises(ValidationError, match="maintain"):
        _baseline(
            goal_type=GoalType.MAINTAIN,
            target_weight_kg=Decimal("75"),
            target_date=date(2026, 10, 1),
        )


def test_unknown_enum_values_are_rejected_instead_of_coerced() -> None:
    for field in (
        "equation_sex_basis",
        "activity_category",
        "goal_type",
        "session_status",
        "review_decision",
        "publication_status",
    ):
        with pytest.raises(ValidationError):
            _baseline(**{field: "other"})
