"""Tests for SME-Hub showcasing services (SP4).

Coverage targets the key PRD acceptance points:
  • FRSME-ISD001 — admin schedules + publishes a showcase event (FSM)
  • FRSME-ISD003 / ISD011 — event with video link + livestream-friendly fields
  • FRSME-ISD004–006 — entrepreneur applies, admin confirms participants
  • FRSME-ISD009–010 — publish to catalogue + versioned update
  • FRSME-ISD012 — attendance recording
  • FRSME-ISD013 — pitch scoring weighted-average parity with SP2
  • FRSME-ISD014–016 — deal room open / message / document / expiry beat
  • FRSME-ISD019 — agreement formalisation
  • PRD A4 — multiple concurrent deal rooms per entrepreneur (different counterparties)
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.utils import timezone

from apps.core.permissions.roles import UserRole
from apps.smehub.linkage.models import PartnerDirectoryEntry, ServiceProviderEntry
from apps.smehub.onboarding.models import Business, BusinessStage, EntrepreneurProfile
from apps.smehub.showcasing import services
from apps.smehub.showcasing.models import (
    ChallengeRubricSection,
    ChallengeScoringRubric,
    DealAgreement,
    DealRoom,
    DealRoomDocument,
    DealRoomMessage,
    EventAttendance,
    InnovationCatalogueEntry,
    InnovationCatalogueVersion,
    InnovationChallenge,
    PitchScore,
    ShowcaseApplication,
    ShowcaseEvent,
)

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


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

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


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


@pytest.fixture
def verified_business(entrepreneur, admin):
    biz = Business.objects.create(
        entrepreneur=entrepreneur,
        name="AgriCo",
        sector="Agriculture",
        business_stage=BusinessStage.MVP,
        country="UG",
    )
    biz.verify(by_user=admin)
    biz.save()
    return biz


@pytest.fixture
def second_business(entrepreneur, admin):
    biz = Business.objects.create(
        entrepreneur=entrepreneur,
        name="EnergyCo",
        sector="Energy",
        business_stage=BusinessStage.MVP,
        country="UG",
    )
    biz.verify(by_user=admin)
    biz.save()
    return biz


@pytest.fixture
def judges():
    return [
        User.objects.create_user(email=f"j{i}@example.com", password="x", role=UserRole.JUDGE)
        for i in range(3)
    ]


@pytest.fixture
def partner_user():
    return User.objects.create_user(email="partner@example.com", password="x", role=UserRole.PARTNER)


@pytest.fixture
def partner_entry(partner_user, admin):
    entry = PartnerDirectoryEntry.objects.create(
        organisation_name="Counterparty One",
        org_type=PartnerDirectoryEntry.OrgType.NGO,
        sector_focus=["Agriculture"],
        geographic_coverage=["UG"],
        contact_email="cp@example.com",
        managed_by=partner_user,
    )
    entry.verify(by_user=admin)
    entry.save()
    return entry


@pytest.fixture
def provider_entry(admin):
    user = User.objects.create_user(
        email="provider@example.com", password="x", role=UserRole.SERVICE_PROVIDER,
    )
    entry = ServiceProviderEntry.objects.create(
        organisation_name="Counterparty Two",
        org_type=PartnerDirectoryEntry.OrgType.CORPORATE,
        sector_focus=["FinTech"],
        geographic_coverage=["UG"],
        contact_email="cp2@example.com",
        managed_by=user,
        service_offerings=["legal"],
    )
    entry.verify(by_user=admin)
    entry.save()
    return entry


@pytest.fixture
def published_event(admin, judges):
    event = services.create_showcase_event(
        organiser=admin,
        name="AgriTech Demo",
        starts_at=timezone.now() + timedelta(days=14),
        ends_at=timezone.now() + timedelta(days=14, hours=4),
        application_deadline=timezone.now() + timedelta(days=10),
        format=ShowcaseEvent.Format.HYBRID,
        target_sectors=["Agriculture"],
        venue="Kampala",
        video_link="https://example.com/live",
        panel_users=judges[:2],
    )
    event = services.publish_showcase(event, by_user=admin)
    return ShowcaseEvent.objects.get(pk=event.pk)


@pytest.fixture
def open_challenge(admin):
    challenge = services.create_challenge(
        organiser=admin,
        title="Climate Adaptation Challenge",
        application_deadline=timezone.now() + timedelta(days=20),
        target_sectors=["Agriculture"],
        rubric_sections=[
            {"title": "Problem", "weight": "0.25", "max_marks": 10, "order": 0},
            {"title": "Innovation", "weight": "0.25", "max_marks": 10, "order": 1},
            {"title": "Impact", "weight": "0.25", "max_marks": 10, "order": 2},
            {"title": "Team", "weight": "0.25", "max_marks": 10, "order": 3},
        ],
    )
    return services.publish_challenge(challenge, by_user=admin)


# ---------------------------------------------------------------------------
# Showcase event lifecycle (FRSME-ISD001, ISD003, ISD011)
# ---------------------------------------------------------------------------

def test_create_and_publish_showcase_event(admin, judges):
    event = services.create_showcase_event(
        organiser=admin,
        name="Test Showcase",
        starts_at=timezone.now() + timedelta(days=7),
        ends_at=timezone.now() + timedelta(days=7, hours=3),
        format=ShowcaseEvent.Format.VIRTUAL,
        target_sectors=["Tech"],
        video_link="https://example.com/live",
        panel_users=judges[:1],
    )
    assert event.status == ShowcaseEvent.Status.DRAFT
    assert event.video_link == "https://example.com/live"
    services.publish_showcase(event, by_user=admin)
    refreshed = ShowcaseEvent.objects.get(pk=event.pk)
    assert refreshed.status == ShowcaseEvent.Status.PUBLISHED
    assert refreshed.published_at is not None


def test_go_live_generates_conference_and_notifies(
    published_event, entrepreneur, verified_business, admin, judges,
):
    """FRSME-ISD011 — going live mints a conferencing session and distributes
    the join link to confirmed participants + evaluators."""
    from apps.core.notifications.models import Notification

    app = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil", pitch_video_link="https://v.example/x",
    )
    services.confirm_participants(
        target=published_event, application_ids=[app.pk], by_user=admin,
    )
    services.mark_showcase_live(published_event, by_user=admin)
    ev = ShowcaseEvent.objects.get(pk=published_event.pk)
    assert ev.conference_meeting_id  # minted on go-live
    assert ev.conference_attendee_pw and ev.conference_moderator_pw
    # The evaluator (panel judge) is notified of the live session.
    assert Notification.objects.filter(recipient=judges[0]).exists()


def test_join_url_ensures_bbb_meeting_exists(published_event, admin, monkeypatch):
    """Regression (client bug #3) — joining a live session must (re)create the
    BBB meeting *before* building the join URL. BBB garbage-collects empty
    meetings within minutes of the go-live create, so a later join to a
    join-only URL hits BBB's ``invalidMeetingIdentifier`` error page. ``create``
    is idempotent, so we call it at join time; this test locks that ordering in.
    """
    from apps.core.integrations import bbb_client
    from apps.smehub.showcasing.models import ShowcaseEventAccess

    published_event.conference_meeting_id = "smehub-showcase-test-abc123"
    published_event.conference_attendee_pw = "attpw"
    published_event.conference_moderator_pw = "modpw"
    published_event.save(update_fields=[
        "conference_meeting_id", "conference_attendee_pw", "conference_moderator_pw",
    ])

    calls = {"create": 0, "join": 0}

    def fake_create(**kwargs):
        calls["create"] += 1
        assert kwargs["meeting_id"] == "smehub-showcase-test-abc123"
        return {"returncode": "SUCCESS"}

    def fake_join(**kwargs):
        calls["join"] += 1
        assert calls["create"] >= 1, "create_meeting must run before join_url is built"
        return "https://bbb.example/api/join?meetingID=" + kwargs["meeting_id"]

    monkeypatch.setattr(bbb_client, "create_meeting", fake_create)
    monkeypatch.setattr(bbb_client, "join_url", fake_join)

    url = services.event_conference_join_url(
        published_event, user=admin, role=ShowcaseEventAccess.Role.ORGANISER,
    )
    assert calls["create"] == 1
    assert calls["join"] == 1
    assert "smehub-showcase-test-abc123" in url


def test_event_access_role_gate_and_log(
    published_event, entrepreneur, verified_business, admin, judges,
):
    """FRSME-ISD011 — confirmed participants + evaluators may join (logged);
    outsiders are refused."""
    from apps.smehub.showcasing.models import ShowcaseEventAccess

    app = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil", pitch_video_link="https://v.example/x",
    )
    services.confirm_participants(
        target=published_event, application_ids=[app.pk], by_user=admin,
    )
    role = services.event_access_role(published_event, entrepreneur.user)
    assert role == ShowcaseEventAccess.Role.PARTICIPANT
    services.record_event_access(published_event, user=entrepreneur.user, role=role)
    assert ShowcaseEventAccess.objects.filter(
        event=published_event, user=entrepreneur.user,
    ).exists()

    assert services.event_access_role(published_event, judges[0]) == ShowcaseEventAccess.Role.EVALUATOR

    outsider = User.objects.create_user(
        email="outsider-isd11@example.com", password="x", role=UserRole.ENTREPRENEUR,
    )
    assert services.event_access_role(published_event, outsider) is None


def test_event_lifecycle_full_path(published_event, admin):
    services.mark_showcase_live(published_event, by_user=admin)
    live = ShowcaseEvent.objects.get(pk=published_event.pk)
    assert live.status == ShowcaseEvent.Status.LIVE
    services.conclude_showcase(live, by_user=admin)
    concluded = ShowcaseEvent.objects.get(pk=published_event.pk)
    assert concluded.status == ShowcaseEvent.Status.CONCLUDED
    assert concluded.concluded_at is not None


# ---------------------------------------------------------------------------
# Application flow (FRSME-ISD004 → ISD006)
# ---------------------------------------------------------------------------

def test_apply_to_event_creates_application(published_event, entrepreneur, verified_business):
    application = services.apply_to_event(
        event=published_event,
        entrepreneur=entrepreneur,
        business=verified_business,
        innovation_title="Soil Sensor Net",
    )
    assert application.application_id.startswith("SHC-")
    assert application.status == ShowcaseApplication.Status.SUBMITTED
    assert application.is_event_application


def test_save_draft_then_submit(published_event, entrepreneur, verified_business):
    """FRSME-ISD004 — saving a draft persists without submitting; submitting
    reuses the same row and flips it to SUBMITTED."""
    draft = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Draft Idea", as_draft=True,
    )
    assert draft.status == ShowcaseApplication.Status.DRAFT
    # Re-saving the draft updates the same row (no duplicate).
    draft2 = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Draft Idea v2", as_draft=True,
    )
    assert draft2.pk == draft.pk
    assert draft2.innovation_title == "Draft Idea v2"

    # get_showcase_draft surfaces it for prefill.
    assert services.get_showcase_draft(
        entrepreneur=entrepreneur, target=published_event,
    ).pk == draft.pk

    # Submitting reuses the draft row and flips status.
    submitted = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Draft Idea v2", pitch_video_link="https://v.example/x",
    )
    assert submitted.pk == draft.pk
    assert submitted.status == ShowcaseApplication.Status.SUBMITTED
    assert ShowcaseApplication.objects.filter(
        entrepreneur=entrepreneur, target_object_id=str(published_event.pk),
    ).count() == 1


def test_apply_to_event_rejects_unverified_business(published_event, entrepreneur):
    biz = Business.objects.create(
        entrepreneur=entrepreneur, name="UnverifiedCo",
        sector="Agriculture", business_stage=BusinessStage.IDEA, country="UG",
    )
    with pytest.raises(services.ShowcasingStateError):
        services.apply_to_event(
            event=published_event, entrepreneur=entrepreneur, business=biz,
            innovation_title="X",
        )


def test_duplicate_application_returns_existing(published_event, entrepreneur, verified_business):
    a = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil",
    )
    b = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Different title",
    )
    assert a.pk == b.pk


def test_confirm_participants_emits_notification(
    published_event, entrepreneur, verified_business, admin,
):
    application = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil",
    )
    services.confirm_participants(
        target=published_event,
        application_ids=[application.pk],
        by_user=admin,
    )
    refreshed = ShowcaseApplication.objects.get(pk=application.pk)
    assert refreshed.status == ShowcaseApplication.Status.CONFIRMED
    from apps.core.notifications.models import Notification
    assert Notification.objects.filter(
        recipient=entrepreneur.user,
        verb=Notification.Verb.SMEHUB_SHOWCASE_PARTICIPANT_CONFIRMED,
    ).exists()


# ---------------------------------------------------------------------------
# Challenge + rubric scoring (FRSME-ISD002, ISD008, ISD013)
# ---------------------------------------------------------------------------

def test_publish_challenge_requires_complete_rubric(admin):
    challenge = services.create_challenge(
        organiser=admin,
        title="Bad Rubric",
        application_deadline=timezone.now() + timedelta(days=10),
        rubric_sections=[
            {"title": "Only one", "weight": "0.50", "max_marks": 10, "order": 0},
        ],
    )
    with pytest.raises(services.ShowcasingStateError):
        services.publish_challenge(challenge, by_user=admin)


def test_pitch_ranking_weighted_average_parity_with_sp2(
    open_challenge, entrepreneur, verified_business, judges,
):
    application = services.apply_to_challenge(
        challenge=open_challenge,
        entrepreneur=entrepreneur,
        business=verified_business,
        innovation_title="Climate-smart pump",
    )
    sections = list(open_challenge.rubric.sections.all())
    # Two judges score every section at score=8/10. Weighted avg = 8/10 * 100 = 80.00
    for judge in judges[:2]:
        for section in sections:
            services.submit_pitch_score(
                application=application, judge=judge, section=section, score=Decimal("8.0"),
            )
    score = services.compute_pitch_ranking(application)
    assert score == Decimal("80.00")


def test_event_pitch_ranking_handles_overall_score(
    published_event, entrepreneur, verified_business, judges,
):
    application = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil",
    )
    services.submit_pitch_score(
        application=application, judge=judges[0], section=None, score=Decimal("7.5"),
    )
    services.submit_pitch_score(
        application=application, judge=judges[1], section=None, score=Decimal("8.5"),
    )
    score = services.compute_pitch_ranking(application)
    # Average of 7.5 and 8.5 = 8.0; expressed as 0–100 (×10) = 80.00
    assert score == Decimal("80.00")


# ---------------------------------------------------------------------------
# Catalogue (FRSME-ISD009, ISD010)
# ---------------------------------------------------------------------------

def test_publish_to_catalogue_creates_versioned_entry(
    published_event, entrepreneur, verified_business, admin,
):
    application = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil",
    )
    services.confirm_participants(
        target=published_event, application_ids=[application.pk], by_user=admin,
    )
    application = ShowcaseApplication.objects.get(pk=application.pk)
    entry = services.publish_to_catalogue(
        application,
        by_user=admin,
        sectors=["Agriculture"],
        impact_metrics={"reach": "5,000"},
    )
    assert entry.version == 1
    assert InnovationCatalogueVersion.objects.filter(entry=entry).count() == 1


def test_update_catalogue_entry_preserves_history(
    published_event, entrepreneur, verified_business, admin,
):
    application = services.apply_to_event(
        event=published_event, entrepreneur=entrepreneur, business=verified_business,
        innovation_title="Soil",
    )
    services.confirm_participants(
        target=published_event, application_ids=[application.pk], by_user=admin,
    )
    application = ShowcaseApplication.objects.get(pk=application.pk)
    entry = services.publish_to_catalogue(application, by_user=admin)
    services.update_catalogue_entry(
        entry, by_user=admin, title="Soil Sensor Network v2",
        notes="Renamed for clarity.",
    )
    refreshed = InnovationCatalogueEntry.objects.get(pk=entry.pk)
    assert refreshed.version == 2
    versions = InnovationCatalogueVersion.objects.filter(entry=refreshed).order_by("version")
    assert versions.count() == 2
    assert versions.first().version == 1
    assert versions.last().version == 2


# ---------------------------------------------------------------------------
# Attendance (FRSME-ISD012)
# ---------------------------------------------------------------------------

def test_record_attendance_idempotent(published_event, entrepreneur, admin):
    services.record_attendance(
        event=published_event, participant=entrepreneur.user, attended=True, by_user=admin,
    )
    services.record_attendance(
        event=published_event, participant=entrepreneur.user, attended=True, by_user=admin,
    )
    assert EventAttendance.objects.filter(event=published_event, participant=entrepreneur.user).count() == 1


# ---------------------------------------------------------------------------
# Deal rooms (FRSME-ISD014 → ISD016, ISD019, PRD A4)
# ---------------------------------------------------------------------------

def test_open_deal_room_creates_open_room(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur,
        business=verified_business,
        counterparty=partner_entry,
        created_by=admin,
    )
    assert room.status == DealRoom.Status.OPEN
    assert room.expiry_date > timezone.now()


def test_open_deal_room_idempotent_on_pair(
    entrepreneur, verified_business, partner_entry, admin,
):
    a = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    b = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    assert a.pk == b.pk


def test_concurrent_deal_rooms_with_different_counterparties_allowed(
    entrepreneur, verified_business, partner_entry, provider_entry, admin,
):
    """PRD A4 — multiple concurrent rooms per entrepreneur across different counterparties."""
    a = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    b = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=provider_entry, created_by=admin,
    )
    assert a.pk != b.pk
    assert DealRoom.objects.filter(business=verified_business, status=DealRoom.Status.OPEN).count() == 2


def test_post_deal_room_message_and_bumps_activity(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    initial_activity = room.last_activity_at
    msg = services.post_deal_room_message(
        room=room, author=entrepreneur.user, body="Hello",
    )
    refreshed = DealRoom.objects.get(pk=room.pk)
    assert refreshed.last_activity_at >= initial_activity
    assert DealRoomMessage.objects.filter(deal_room=room).count() == 1
    assert msg.body == "Hello"


def test_upload_deal_room_document_versions(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    doc1 = services.upload_deal_room_document(
        room=room, label="Proposal", file=ContentFile(b"v1", name="p.txt"),
        uploaded_by=admin,
    )
    doc2 = services.upload_deal_room_document(
        room=room, label="Proposal", file=ContentFile(b"v2", name="p.txt"),
        uploaded_by=admin,
    )
    assert doc1.version == 1
    assert doc2.version == 2
    assert DealRoomDocument.objects.filter(deal_room=room, label="Proposal").count() == 2


def test_expire_deal_rooms_beat_flips_status(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
        expiry_date=timezone.now() - timedelta(days=1),
    )
    count = services.expire_deal_rooms()
    assert count == 1
    refreshed = DealRoom.objects.get(pk=room.pk)
    assert refreshed.status == DealRoom.Status.EXPIRED


def test_formalise_agreement_flips_to_agreement_reached(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    services.formalise_agreement(
        room=room,
        title="MoU",
        signed_file=ContentFile(b"signed", name="mou.pdf"),
        by_admin=admin,
    )
    refreshed = DealRoom.objects.get(pk=room.pk)
    assert refreshed.status == DealRoom.Status.AGREEMENT_REACHED
    assert DealAgreement.objects.filter(deal_room=room).exists()


def test_close_deal_room_records_reason(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    services.close_deal_room(room, by_user=admin, reason="No fit")
    refreshed = DealRoom.objects.get(pk=room.pk)
    assert refreshed.status == DealRoom.Status.CLOSED
    assert refreshed.closed_reason == "No fit"


def test_post_message_blocked_on_closed_room(
    entrepreneur, verified_business, partner_entry, admin,
):
    room = services.open_deal_room(
        entrepreneur=entrepreneur, business=verified_business,
        counterparty=partner_entry, created_by=admin,
    )
    services.close_deal_room(room, by_user=admin)
    refreshed = DealRoom.objects.get(pk=room.pk)
    with pytest.raises(services.ShowcasingStateError):
        services.post_deal_room_message(
            room=refreshed, author=entrepreneur.user, body="late",
        )


def test_signal_emitted_on_agreement(
    entrepreneur, verified_business, partner_entry, admin,
):
    received = []

    from apps.smehub.showcasing import signals as show_signals

    def handler(sender, deal_room, agreement, **kwargs):
        received.append((deal_room.pk, agreement.pk))

    show_signals.smehub_agreement_formalised.connect(handler)
    try:
        room = services.open_deal_room(
            entrepreneur=entrepreneur, business=verified_business,
            counterparty=partner_entry, created_by=admin,
        )
        services.formalise_agreement(
            room=room, title="MoU",
            signed_file=ContentFile(b"signed", name="mou.pdf"),
            by_admin=admin,
        )
    finally:
        show_signals.smehub_agreement_formalised.disconnect(handler)
    assert received  # at least one delivery
