"""Tests for Phase 3 SP2 incubation contract gap fixes.

Coverage:
* FRSME-INC008 — eligibility now also rejects when AIH affiliation does
  not include any of the programme's ``aih_locations``.
* FRSME-INC014 — submitting an application after the deadline is blocked.
* FRSME-INC020 — confirm_cohort refuses to accept an application that
  has not been fully scored (audit fix using the shared strict-mode
  helper from Phase 1).
* FRSME-INC026 — sector-aligned mentors rank above sector-agnostic
  mentors in the assignment selector.
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.utils import timezone

from apps.core.authentication.models import Institution
from apps.core.permissions.roles import UserRole
from apps.smehub.incubation import services, views
from apps.smehub.incubation.models import Application, CohortMember, Programme
from apps.smehub.onboarding.models import (
    AIHAffiliation,
    Business,
    BusinessBaseline,
    BusinessStage,
    EntrepreneurProfile,
    MentorProfile,
    RevenueRange,
)

User = get_user_model()
pytestmark = pytest.mark.django_db


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def admin():
    return User.objects.create_user(
        email="adm-p3@example.com", password="x",
        is_staff=True, is_superuser=True, role=UserRole.SYSTEM_ADMIN,
    )


@pytest.fixture
def officer():
    return User.objects.create_user(
        email="po-p3@example.com", password="x", role=UserRole.PROGRAMME_OFFICER,
    )


@pytest.fixture
def entrepreneur(admin):
    user = User.objects.create_user(
        email="ent-p3@example.com", password="x", role=UserRole.ENTREPRENEUR,
    )
    profile = EntrepreneurProfile.objects.create(
        user=user, country="UG", organisation="Test U", phone="+25670000",
    )
    profile.verify(by_user=admin)
    profile.save()
    return profile


@pytest.fixture
def verified_business(entrepreneur, admin):
    biz = Business.objects.create(
        entrepreneur=entrepreneur,
        name="P3 Biz",
        sector="Agriculture",
        business_stage=BusinessStage.MVP,
        country="UG",
    )
    baseline = BusinessBaseline.objects.create(
        business=biz, business_stage_at_entry=BusinessStage.MVP,
        fte_count=1, pte_count=0,
        revenue_range=RevenueRange.UNDER_1K,
        primary_target_market="Local", geographic_reach="UG",
    )
    biz.verify(by_user=admin)
    biz.save()
    from apps.smehub.onboarding import signals as onb_signals

    onb_signals.smehub_baseline_initialised.send(
        sender=BusinessBaseline, business=biz, baseline=baseline, entrepreneur=entrepreneur,
    )
    return biz


@pytest.fixture
def programme_open(officer):
    p = services.create_programme(
        officer=officer,
        title="P3 Programme",
        slug="p3-programme",
        target_sectors=["Agriculture"],
        target_stages=["mvp"],
        cohort_size=5,
        duration_weeks=10,
        application_deadline=timezone.now() + timedelta(days=30),
    )
    services.add_rubric_section(p, title="A", weight=Decimal("0.5"), max_marks=10)
    services.add_rubric_section(p, title="B", weight=Decimal("0.5"), max_marks=10)
    p.judges.set([User.objects.create_user(email="p3fix-roster-judge@example.com", password="x", role=UserRole.JUDGE)])
    services.publish_programme(p, by_user=officer)
    return Programme.objects.get(pk=p.pk)


# ---------------------------------------------------------------------------
# FRSME-INC008 — AIH affiliation eligibility check
# ---------------------------------------------------------------------------

def test_eligibility_blocks_when_programme_aih_does_not_match(
    programme_open, verified_business, officer,
):
    """When the programme is scoped to specific AIH locations, an
    entrepreneur whose business has no matching AIH affiliation is
    ineligible (FRSME-INC008 audit fix)."""
    Programme.objects.filter(pk=programme_open.pk).update(
        aih_locations=["Makerere AIH"],
    )
    fresh = Programme.objects.get(pk=programme_open.pk)
    ok, reason = services.check_eligibility(verified_business, fresh)
    assert ok is False
    assert "AIH" in reason


def test_eligibility_passes_when_aih_matches(
    programme_open, verified_business,
):
    inst = Institution.objects.create(name="Makerere AIH", country="UG")
    AIHAffiliation.objects.create(
        business=verified_business,
        institution=inst,
        nature=AIHAffiliation.AffiliationNature.STUDENT,
        is_primary=True,
    )
    Programme.objects.filter(pk=programme_open.pk).update(
        aih_locations=["Makerere AIH"],
    )
    fresh = Programme.objects.get(pk=programme_open.pk)
    ok, _reason = services.check_eligibility(verified_business, fresh)
    assert ok is True


def test_eligibility_aih_match_is_tolerant_of_venue_naming(
    programme_open, verified_business,
):
    """Browser QA F3 — an admin-typed venue name ("Makerere AIH") must match
    the directory institution ("Makerere University") even though the strings
    differ, otherwise the programme silently locks everyone out."""
    inst = Institution.objects.create(name="Makerere University", country="UG")
    AIHAffiliation.objects.create(
        business=verified_business,
        institution=inst,
        nature=AIHAffiliation.AffiliationNature.STUDENT,
        is_primary=True,
    )
    Programme.objects.filter(pk=programme_open.pk).update(
        aih_locations=["Makerere AIH"],
    )
    fresh = Programme.objects.get(pk=programme_open.pk)
    ok, _reason = services.check_eligibility(verified_business, fresh)
    assert ok is True


def test_aih_location_matches_helper():
    # Tolerant: suffix-stripped prefix match in either direction.
    assert services.aih_location_matches("Makerere AIH", "Makerere University")
    assert services.aih_location_matches("Makerere University", "Makerere AIH")
    assert services.aih_location_matches("makerere hub", "Makerere University")
    assert services.aih_location_matches("Makerere AIH", "Makerere AIH")
    # Conservative: unrelated institutions never match.
    assert not services.aih_location_matches("Sokoine University AIH", "Makerere University")
    assert not services.aih_location_matches("", "Makerere University")
    assert not services.aih_location_matches("Nairobi Innovation Hub", "Makerere University")


# ---------------------------------------------------------------------------
# FRSME-INC014 — deadline check at submit
# ---------------------------------------------------------------------------

def test_submit_blocks_after_deadline(programme_open, verified_business, entrepreneur):
    """A draft started before the deadline must not be submittable after it."""
    app = services.start_application(
        entrepreneur=entrepreneur, business=verified_business, programme=programme_open,
    )
    # Roll the deadline back into the past on the in-memory programme so the
    # cached FK reference on the application reflects the change.
    app.programme.application_deadline = timezone.now() - timedelta(hours=1)
    app.programme.save(update_fields=["application_deadline"])
    with pytest.raises(services.ApplicationStateError, match="deadline"):
        services.submit_application(app)


# ---------------------------------------------------------------------------
# FRSME-INC020 — confirm_cohort refuses partial scoring
# ---------------------------------------------------------------------------

def test_confirm_cohort_blocks_when_scoring_incomplete(
    programme_open, verified_business, entrepreneur, officer,
):
    """Audit gap — confirming with un-scored applications must raise."""
    app = services.start_application(
        entrepreneur=entrepreneur, business=verified_business, programme=programme_open,
    )
    app.business_description = "Test Biz supplies inputs to smallholder farmers."
    app.problem_statement = "Smallholders lack reliable access to quality inputs."
    app.save()
    services.submit_application(app)
    # Note: NO scoring done.
    with pytest.raises(services.ApplicationStateError, match="fully scored"):
        services.confirm_cohort(
            programme_open,
            accepted_application_ids=[app.pk],
            name="C1",
            start_date=timezone.now().date(),
            end_date=timezone.now().date() + timedelta(days=84),
            by_user=officer,
        )


def test_confirm_cohort_blocks_when_only_some_sections_scored(
    programme_open, verified_business, entrepreneur, officer,
):
    """One judge scoring just one of two sections must not pass the gate."""
    app = services.start_application(
        entrepreneur=entrepreneur, business=verified_business, programme=programme_open,
    )
    app.business_description = "Test Biz supplies inputs to smallholder farmers."
    app.problem_statement = "Smallholders lack reliable access to quality inputs."
    app.save()
    services.submit_application(app)
    judge = User.objects.create_user(
        email="judge-p3@example.com", password="x", role=UserRole.JUDGE,
    )
    services.assign_judges(app, judges=[judge], by_user=officer)
    sections = list(programme_open.rubric.sections.all())
    services.submit_score(app, judge=judge, section_scores={sections[0].pk: 8})
    with pytest.raises(services.ApplicationStateError, match="fully scored"):
        services.confirm_cohort(
            programme_open,
            accepted_application_ids=[app.pk],
            name="C1",
            start_date=timezone.now().date(),
            end_date=timezone.now().date() + timedelta(days=84),
            by_user=officer,
        )


def test_confirm_cohort_passes_when_officer_decision_method(
    programme_open, verified_business, entrepreneur, officer,
):
    """An OFFICER_DECISION programme bypasses the strict-scoring gate —
    admin selection is by judgement, not numeric ranking."""
    Programme.objects.filter(pk=programme_open.pk).update(
        selection_method=Programme.SelectionMethod.OFFICER_DECISION,
    )
    app = services.start_application(
        entrepreneur=entrepreneur, business=verified_business, programme=programme_open,
    )
    app.business_description = "Test Biz supplies inputs to smallholder farmers."
    app.problem_statement = "Smallholders lack reliable access to quality inputs."
    app.save()
    services.submit_application(app)
    cohort = services.confirm_cohort(
        Programme.objects.get(pk=programme_open.pk),
        accepted_application_ids=[app.pk],
        name="OD-Cohort",
        start_date=timezone.now().date(),
        end_date=timezone.now().date() + timedelta(days=84),
        by_user=officer,
    )
    assert cohort.members.count() == 1


# ---------------------------------------------------------------------------
# FRSME-INC026 — mentor sector ranking
# ---------------------------------------------------------------------------

def test_mentor_qs_ranks_sector_match_first():
    """Mentor whose ``expertise_sectors`` contains the business sector
    appears before sector-agnostic mentors."""
    matched = User.objects.create_user(
        email="m-match@example.com", password="x", role=UserRole.MENTOR,
        first_name="Zoe", last_name="Match",
    )
    MentorProfile.objects.create(user=matched, expertise_sectors=["Agriculture"])
    other = User.objects.create_user(
        email="m-other@example.com", password="x", role=UserRole.MENTOR,
        first_name="Alex", last_name="Other",
    )
    MentorProfile.objects.create(user=other, expertise_sectors=[])

    class _FakeMember:
        class _FakeBusiness:
            sector = "Agriculture"
        business = _FakeBusiness()

    ranked = views._mentor_qs(member=_FakeMember())
    # Must be a QuerySet — the assign-mentor form assigns it to a
    # ModelChoiceField.queryset, which calls .all() (a list 500s the page).
    assert hasattr(ranked, "filter")
    ranked = list(ranked)
    # Matched mentor must come first regardless of alphabetical ordering.
    assert ranked[0] == matched
    assert other in ranked


def test_assign_mentor_page_renders_with_sector_matched_mentor(
    client, programme_open, verified_business, entrepreneur, officer,
):
    """Regression (browser QA F7) — GET assign-mentor for a member whose
    business sector matches a MentorProfile.expertise_sectors entry must
    render 200, not crash with `'list' object has no attribute 'all'`."""
    from django.urls import reverse

    matched = User.objects.create_user(
        email="m-inc026@example.com", password="x", role=UserRole.MENTOR,
    )
    MentorProfile.objects.create(user=matched, expertise_sectors=["Agriculture"])

    Programme.objects.filter(pk=programme_open.pk).update(
        selection_method=Programme.SelectionMethod.OFFICER_DECISION,
    )
    app = services.start_application(
        entrepreneur=entrepreneur, business=verified_business, programme=programme_open,
    )
    app.business_description = "Test Biz supplies inputs to smallholder farmers."
    app.problem_statement = "Smallholders lack reliable access to quality inputs."
    app.save()
    services.submit_application(app)
    cohort = services.confirm_cohort(
        Programme.objects.get(pk=programme_open.pk),
        accepted_application_ids=[app.pk],
        name="F7-Cohort",
        start_date=timezone.now().date(),
        end_date=timezone.now().date() + timedelta(days=84),
        by_user=officer,
    )
    member = cohort.members.first()

    client.force_login(officer)
    resp = client.get(reverse("smehub_incubation:assign_mentor", kwargs={"pk": member.pk}))
    assert resp.status_code == 200


def test_mentor_qs_returns_all_when_no_member_supplied():
    """Legacy contract: no member → plain alphabetical queryset, not a list."""
    User.objects.create_user(
        email="m-leg@example.com", password="x", role=UserRole.MENTOR,
    )
    qs = views._mentor_qs()
    assert hasattr(qs, "filter")  # queryset, not a sorted list
