"""WS4 (feedback/surveys) MEL fix-sweep regression tests — 2026-07-11.

Covers register defects #11–#15, #29–#32, #34, #46, #47b:
  * D-D1  dispatch take-link delivered (notification carries the token URL)
  * D-D2  editing a survey with responses does not delete its answers
  * D-D3  a CLOSED survey refuses dispatch-token takes
  * D-D4  a 2-page channel take yields ONE response with all answers
  * D-D5  server-side rejection of empty question text / empty MC options
  * D-D6  non-response increments feedback_non_response_count on the right record
  * D-D7  deactivated intake links render a polite page, not a raw 404
  * D-D8  a take-page validation error re-renders the submitted values
  * D-D9  an unset public rating is stored as null, not 1
  * D-D10 results KPI page count uses distinct pages
"""
from __future__ import annotations

from datetime import timedelta

import pytest
from django.contrib.auth import get_user_model
from django.test import Client
from django.urls import reverse
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.core.permissions.roles import UserRole
from apps.mel.feedback.models import (
    FeedbackChannel,
    FeedbackChannelType,
    FeedbackSeverity,
    FeedbackSubmission,
    Survey,
    SurveyAnswer,
    SurveyDispatch,
    SurveyQuestion,
    SurveyQuestionType,
    SurveyResponse,
    SurveyStatus,
)
from apps.mel.feedback.services import (
    activate_survey,
    close_survey,
    collect_response,
    dispatch_survey,
)

pytestmark = pytest.mark.django_db

User = get_user_model()


def _officer():
    user = User.objects.create_user(email="ws4-officer@example.com", password="x")
    user.role = UserRole.MEL_OFFICER.value
    user.save(update_fields=["role"])
    return user


def _active_survey(slug="ws4-survey", *, pages=1):
    survey = Survey.objects.create(title="WS4 survey", slug=slug)
    for p in range(1, pages + 1):
        SurveyQuestion.objects.create(
            survey=survey, ordinal=p - 1, page=p,
            type=SurveyQuestionType.SHORT_ANSWER,
            text=f"Thoughts page {p}?", required=False,
        )
    activate_survey(survey)
    return survey


# --- #11 / D-D1 -------------------------------------------------------------


def test_dispatch_notification_includes_take_token_url():
    survey = _active_survey("ws4-d1")
    recipient = User.objects.create_user(email="d1-recipient@example.com", password="x")

    dispatches = dispatch_survey(survey, recipients=[recipient], trigger_event="unit")
    dispatch = dispatches[0]

    take_path = reverse("mel_feedback:survey_take_dispatch", args=[dispatch.token])
    note = Notification.objects.filter(recipient=recipient).order_by("-id").first()
    assert note is not None
    assert dispatch.token in note.action_url
    assert take_path in note.action_url
    assert dispatch.token in note.data.get("take_url", "")


# --- #12 / D-D2 -------------------------------------------------------------


def test_editing_survey_with_responses_preserves_answers():
    survey = _active_survey("ws4-d2")
    question = survey.questions.first()
    collect_response(survey, answers={question.pk: "keep me"}, complete=True)
    assert SurveyAnswer.objects.filter(response__survey=survey).count() == 1

    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_feedback:survey_edit", args=[survey.slug]),
        {
            "title": "WS4 survey (renamed)",
            "description": "new desc",
            "trigger": "manual",
            "response_window_days": "14",
            "questions_json": "[]",  # attempt to wipe questions
        },
    )
    assert resp.status_code == 302
    # Questions + answers must survive; only metadata changed.
    assert SurveyAnswer.objects.filter(response__survey=survey).count() == 1
    assert survey.questions.count() == 1
    survey_reloaded = Survey.objects.get(pk=survey.pk)
    assert survey_reloaded.title == "WS4 survey (renamed)"


# --- #13 / D-D3 -------------------------------------------------------------


def test_closed_survey_refuses_dispatch_take():
    survey = _active_survey("ws4-d3")
    dispatch = SurveyDispatch.objects.create(
        survey=survey,
        recipient=None,
        recipient_email="d3@example.com",
        token=SurveyDispatch.generate_token(),
        due_at=timezone.now() + timedelta(days=7),
    )
    close_survey(survey)

    url = reverse("mel_feedback:survey_take_dispatch", args=[dispatch.token])
    client = Client(SERVER_NAME="localhost")

    get_resp = client.get(url)
    assert get_resp.status_code == 200
    assert b"closed" in get_resp.content.lower()

    before = SurveyResponse.objects.filter(survey=survey).count()
    post_resp = client.post(url, {"page": "1", f"answer_{survey.questions.first().pk}": "late"})
    assert post_resp.status_code == 200
    assert b"closed" in post_resp.content.lower()
    assert SurveyResponse.objects.filter(survey=survey).count() == before


# --- #14 / D-D4 -------------------------------------------------------------


def test_channel_multipage_take_yields_single_response():
    survey = _active_survey("ws4-d4", pages=2)
    q1 = survey.questions.get(page=1)
    q2 = survey.questions.get(page=2)
    url = reverse("mel_feedback:survey_take_channel", args=[survey.channel.token])
    client = Client(SERVER_NAME="localhost")

    r1 = client.post(url, {"page": "1", f"answer_{q1.pk}": "page one"})
    assert r1.status_code == 302  # advance to page 2
    r2 = client.post(url, {"page": "2", f"answer_{q2.pk}": "page two"})
    assert r2.status_code == 200

    responses = SurveyResponse.objects.filter(survey=survey)
    assert responses.count() == 1
    response = responses.first()
    assert response.status == SurveyResponse.Status.COMPLETED
    values = {a.question_id: (a.value or {}).get("value") for a in response.answers.all()}
    assert values == {q1.pk: "page one", q2.pk: "page two"}


# --- #29 / D-D5 -------------------------------------------------------------


def test_survey_create_rejects_empty_question_text():
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_feedback:survey_create"),
        {
            "title": "Bad survey",
            "trigger": "manual",
            "response_window_days": "14",
            "questions_json": '[{"type":"short_answer","text":"","required":false,"page":1,"options":[]}]',
        },
    )
    assert resp.status_code == 200  # re-rendered with error, not saved
    assert not Survey.objects.filter(title="Bad survey").exists()


def test_survey_create_rejects_multiple_choice_without_options():
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_feedback:survey_create"),
        {
            "title": "Bad MC survey",
            "trigger": "manual",
            "response_window_days": "14",
            "questions_json": '[{"type":"multiple_choice","text":"Pick one","required":true,"page":1,"options":[]}]',
        },
    )
    assert resp.status_code == 200
    assert not Survey.objects.filter(title="Bad MC survey").exists()


# --- #15 / D-D6 -------------------------------------------------------------


def test_non_response_increments_tracking_counter():
    from apps.mel.feedback.tasks import scan_survey_non_responses
    from apps.mel.tracking.models import SMEHubTrackingRecord
    from apps.smehub.onboarding.models import EntrepreneurProfile

    survey = Survey.objects.create(
        title="D6 survey", slug="ws4-d6",
        status=SurveyStatus.ACTIVE, response_window_days=7,
    )
    user = User.objects.create_user(email="d6-entrepreneur@example.com", password="x")
    profile = EntrepreneurProfile.objects.create(user=user)
    record, _ = SMEHubTrackingRecord.objects.get_or_create(
        entrepreneur=profile, defaults={"feedback_non_response_count": 0}
    )
    SMEHubTrackingRecord.objects.filter(pk=record.pk).update(feedback_non_response_count=0)
    record.refresh_from_db()
    assert record.feedback_non_response_count == 0

    SurveyDispatch.objects.create(
        survey=survey,
        recipient=user,
        token=SurveyDispatch.generate_token(),
        status=SurveyDispatch.Status.REMINDED,
        due_at=timezone.now() - timedelta(days=1),
    )

    scan_survey_non_responses()

    record.refresh_from_db()
    assert record.feedback_non_response_count == 1


# --- #30 / D-D7 -------------------------------------------------------------


def test_deactivated_public_channel_renders_polite_page():
    channel = FeedbackChannel.objects.create(
        type=FeedbackChannelType.BENEFICIARY,
        name="Closed intake",
        slug="ws4-d7-closed",
        token=FeedbackChannel.generate_token(),
        is_public=True,
        is_active=False,
    )
    url = reverse("mel_feedback:public_submit", args=[channel.token])
    resp = Client(SERVER_NAME="localhost").get(url)
    assert resp.status_code == 200
    assert b"no longer open" in resp.content.lower() or b"closed" in resp.content.lower()


def test_unknown_public_token_still_404s():
    resp = Client(SERVER_NAME="localhost").get(
        reverse("mel_feedback:public_submit", args=["not-a-real-token"])
    )
    assert resp.status_code == 404


# --- #31 / D-D8 -------------------------------------------------------------


def test_take_validation_error_retains_typed_answers():
    survey = Survey.objects.create(title="D8 survey", slug="ws4-d8")
    free = SurveyQuestion.objects.create(
        survey=survey, ordinal=0, page=1,
        type=SurveyQuestionType.SHORT_ANSWER, text="Anything?", required=False,
    )
    SurveyQuestion.objects.create(
        survey=survey, ordinal=1, page=1,
        type=SurveyQuestionType.MULTIPLE_CHOICE, text="Pick", required=True,
        options=[{"value": "a", "label": "A"}, {"value": "b", "label": "B"}],
    )
    activate_survey(survey)

    url = reverse("mel_feedback:survey_take_channel", args=[survey.channel.token])
    resp = Client(SERVER_NAME="localhost").post(
        url, {"page": "1", f"answer_{free.pk}": "please keep this text"}
    )
    assert resp.status_code == 200
    assert b"please keep this text" in resp.content
    assert SurveyResponse.objects.filter(survey=survey).count() == 0


# --- #32 / D-D9 -------------------------------------------------------------


def test_public_submission_without_rating_stores_null():
    channel = FeedbackChannel.objects.create(
        type=FeedbackChannelType.BENEFICIARY,
        name="Rating intake",
        slug="ws4-d9",
        token=FeedbackChannel.generate_token(),
        is_public=True,
        is_active=True,
    )
    url = reverse("mel_feedback:public_submit", args=[channel.token])
    resp = Client(SERVER_NAME="localhost").post(
        url,
        {"narrative": "No rating given.", "severity": FeedbackSeverity.LOW},
    )
    assert resp.status_code == 200
    submission = FeedbackSubmission.objects.get(channel=channel)
    assert submission.rating is None


# --- #34 / D-D10 ------------------------------------------------------------


def test_results_page_count_uses_distinct_pages():
    survey = _active_survey("ws4-d10", pages=2)  # 2 questions, 2 pages
    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_feedback:survey_results", args=[survey.slug]))
    assert resp.status_code == 200
    assert resp.context["page_count"] == 2
    assert b"across 2 pages" in resp.content
