"""EligibilityRuleForm — typed fields replace the JSON textarea.

These tests pin the contract between the manager-facing UI controls and the
JSON shape the evaluator (`apps.rims.grants.services._rule_passes`) expects:

- nationality_countries → params={"countries": [...]}  (stored as full names)
- degree_levels        → params={"levels":    [...]}
- institution_ids      → params={"institution_ids": [...]}

Drift on either side breaks eligibility evaluation silently, so we lock the
serialization, hydration, and validation here.
"""
from datetime import timedelta

import pytest
from django.utils import timezone

from apps.core.authentication.models import Institution
from apps.rims.grants.forms import EligibilityRuleForm
from apps.rims.grants.models import EligibilityRule, GrantCall


@pytest.fixture
def call(db):
    return GrantCall.objects.create(
        title="Eligibility form test call",
        slug="eligibility-form-test-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=30),
        status=GrantCall.Status.DRAFT,
    )


@pytest.fixture
def institutions(db):
    return [
        Institution.objects.create(name="Test Uni A", country="Uganda", is_member=True),
        Institution.objects.create(name="Test Uni B", country="Kenya", is_member=True),
    ]


def _post(rule_type, **extra):
    """Minimal POST payload for a single (unsaved) formset row."""
    return {
        "rule_type": rule_type,
        "is_blocking": "on",
        **extra,
    }


# ── Serialization: typed controls → params JSON ──────────────────────────────

def test_nationality_field_serializes_to_countries_key(call):
    form = EligibilityRuleForm(
        data=_post("nationality", nationality_countries=["Uganda", "Kenya"]),
        instance=EligibilityRule(call=call),
    )
    assert form.is_valid(), form.errors
    rule = form.save(commit=False)
    rule.call = call
    rule.save()
    assert rule.params == {"countries": ["Uganda", "Kenya"]}
    assert rule.rule_type == "nationality"


def test_nationality_field_accepts_legacy_iso_codes_and_stores_names(call):
    """Posting legacy ISO codes is tolerated and canonicalised to full names."""
    form = EligibilityRuleForm(
        data=_post("nationality", nationality_countries=["UG", "KE"]),
        instance=EligibilityRule(call=call),
    )
    assert form.is_valid(), form.errors
    rule = form.save(commit=False)
    rule.call = call
    rule.save()
    assert rule.params == {"countries": ["Uganda", "Kenya"]}


def test_degree_field_serializes_to_levels_key(call):
    form = EligibilityRuleForm(
        data=_post("degree", degree_levels=["masters", "phd"]),
        instance=EligibilityRule(call=call),
    )
    assert form.is_valid(), form.errors
    rule = form.save(commit=False)
    rule.call = call
    rule.save()
    assert rule.params == {"levels": ["masters", "phd"]}


def test_institution_field_serializes_to_institution_ids_key(call, institutions):
    inst_a, inst_b = institutions
    form = EligibilityRuleForm(
        data=_post("institution", institution_ids=[inst_a.pk, inst_b.pk]),
        instance=EligibilityRule(call=call),
    )
    assert form.is_valid(), form.errors
    rule = form.save(commit=False)
    rule.call = call
    rule.save()
    assert rule.params == {"institution_ids": [inst_a.pk, inst_b.pk]}


def test_empty_list_for_chosen_type_saves_empty_array(call):
    """A rule with type chosen but no selections = "everyone passes" allow-list."""
    form = EligibilityRuleForm(
        data=_post("nationality"),
        instance=EligibilityRule(call=call),
    )
    assert form.is_valid(), form.errors
    rule = form.save(commit=False)
    rule.call = call
    rule.save()
    assert rule.params == {"countries": []}


# ── Hydration: existing params → typed-field initial ─────────────────────────

def test_existing_nationality_rule_hydrates_countries_initial(call):
    rule = EligibilityRule.objects.create(
        call=call, rule_type="nationality", params={"countries": ["Uganda", "Kenya"]}, is_blocking=True
    )
    form = EligibilityRuleForm(instance=rule)
    assert form.fields["nationality_countries"].initial == ["Uganda", "Kenya"]
    assert form.fields["degree_levels"].initial is None or form.fields["degree_levels"].initial == []


def test_existing_degree_rule_hydrates_levels_initial(call):
    rule = EligibilityRule.objects.create(
        call=call, rule_type="degree", params={"levels": ["masters"]}, is_blocking=True
    )
    form = EligibilityRuleForm(instance=rule)
    assert form.fields["degree_levels"].initial == ["masters"]


def test_existing_institution_rule_hydrates_institution_ids_initial(call, institutions):
    inst_a, _ = institutions
    rule = EligibilityRule.objects.create(
        call=call,
        rule_type="institution",
        params={"institution_ids": [inst_a.pk]},
        is_blocking=True,
    )
    form = EligibilityRuleForm(instance=rule)
    assert form.fields["institution_ids"].initial == [inst_a.pk]


def test_legacy_country_codes_are_resolved_to_full_names(call):
    """Defensive: old rows may have stored ISO codes ('ug') instead of names."""
    rule = EligibilityRule.objects.create(
        call=call, rule_type="nationality", params={"countries": ["ug", "ke"]}, is_blocking=True
    )
    form = EligibilityRuleForm(instance=rule)
    assert form.fields["nationality_countries"].initial == ["Uganda", "Kenya"]


# ── Validation: typed fields reject bad values ──────────────────────────────

def test_invalid_country_rejected_at_field_level(call):
    form = EligibilityRuleForm(
        data=_post("nationality", nationality_countries=["Atlantis"]),
        instance=EligibilityRule(call=call),
    )
    assert not form.is_valid()
    assert "nationality_countries" in form.errors


def test_invalid_degree_level_rejected_at_field_level(call):
    form = EligibilityRuleForm(
        data=_post("degree", degree_levels=["doctor-of-mystery"]),
        instance=EligibilityRule(call=call),
    )
    assert not form.is_valid()
    assert "degree_levels" in form.errors


def test_nonexistent_institution_id_rejected(call):
    form = EligibilityRuleForm(
        data=_post("institution", institution_ids=[99999]),
        instance=EligibilityRule(call=call),
    )
    assert not form.is_valid()
    assert "institution_ids" in form.errors


# ── Rule-type switching: only the chosen type's data is serialized ───────────

def test_switching_type_replaces_params_with_new_type_shape(call):
    """Editing a nationality rule to type=degree drops countries, writes levels."""
    rule = EligibilityRule.objects.create(
        call=call, rule_type="nationality", params={"countries": ["Uganda"]}, is_blocking=True
    )
    form = EligibilityRuleForm(
        data=_post("degree", degree_levels=["phd"], nationality_countries=["Uganda"]),
        instance=rule,
    )
    assert form.is_valid(), form.errors
    saved = form.save()
    assert saved.rule_type == "degree"
    assert saved.params == {"levels": ["phd"]}
    assert "countries" not in saved.params
