"""Tests for check_slop() and its wiring into check_page / check_quiz /
check_assessment / check_discussion.

Plan reference: docs/plan-anti-slop.md
Standard reference: standards/anti-slop.md
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import pytest
from preflight_checks import (
    check_slop,
    check_page,
    check_quiz,
    check_assessment,
    check_discussion,
)


# ── Helpers ────────────────────────────────────────────────────────────────

def _slop_ids(issues):
    """Return only style.slop issue messages from an issue list."""
    return [i for i in issues if i.criterion_id == "style.slop"]


def _has_slop(issues):
    return bool(_slop_ids(issues))


# ═══════════════════════════════════════════════════════════════════════════
# 1. Per-category: slop fires / clean sentence does not
# ═══════════════════════════════════════════════════════════════════════════

class TestThroatClearingOpeners:
    def test_heres_the_thing_fires(self):
        issues = check_slop("Here's the thing: contract law is foundational.")
        assert _has_slop(issues)

    def test_the_truth_is_fires(self):
        issues = check_slop("The truth is, most students overlook this rule.")
        assert _has_slop(issues)

    def test_let_me_be_clear_fires(self):
        issues = check_slop("Let me be clear about the policy.")
        assert _has_slop(issues)

    def test_it_turns_out_fires(self):
        issues = check_slop("It turns out that jurisdiction matters.")
        assert _has_slop(issues)

    def test_make_no_mistake_fires(self):
        issues = check_slop("Make no mistake, this exam is comprehensive.")
        assert _has_slop(issues)

    def test_clean_opener_does_not_fire(self):
        issues = check_slop("Contract law governs enforceable agreements.")
        assert not _has_slop(issues)


class TestEmphasisCrutches:
    def test_let_that_sink_in_fires(self):
        issues = check_slop("Let that sink in.")
        assert _has_slop(issues)

    def test_full_stop_not_linted(self):
        # "full stop" is British for the period mark (grammar/ESL/writing) and
        # appears literally in physics/driver-ed; too FP-prone to flag.
        assert not _has_slop(check_slop("A declarative sentence ends with a full stop."))
        assert not _has_slop(check_slop("The car comes to a full stop at the sign."))

    def test_this_matters_because_fires(self):
        issues = check_slop("This matters because students will be tested.")
        assert _has_slop(issues)

    def test_heres_why_that_matters_fires(self):
        issues = check_slop("Here's why that matters for the exam.")
        assert _has_slop(issues)

    def test_clean_emphasis_does_not_fire(self):
        issues = check_slop("The rule matters for two reasons: standing and remedy.")
        assert not _has_slop(issues)


class TestBusinessJargon:
    def test_navigate_challenges_fires(self):
        issues = check_slop("Students must navigate the challenges of civil procedure.")
        assert _has_slop(issues)

    def test_deep_dive_fires(self):
        issues = check_slop("We will do a deep dive into constitutional law.")
        assert _has_slop(issues)

    def test_moving_forward_fires(self):
        issues = check_slop("Moving forward, focus on the remedies doctrine.")
        assert _has_slop(issues)

    def test_circle_back_fires(self):
        issues = check_slop("We will circle back to jurisdiction next week.")
        assert _has_slop(issues)

    def test_game_changer_fires(self):
        issues = check_slop("This ruling was a game-changer for tort law.")
        assert _has_slop(issues)

    def test_lean_into_fires(self):
        issues = check_slop("You need to lean into the ambiguity.")
        assert _has_slop(issues)

    def test_on_the_same_page_fires(self):
        issues = check_slop("Let's make sure we are on the same page.")
        assert _has_slop(issues)

    # False-positive guard: generic navigation must NOT fire
    def test_navigate_to_page_does_not_fire(self):
        issues = check_slop("Navigate to the next module in Canvas.")
        assert not _has_slop(issues)

    # False-positive guard: "not available until Monday" must NOT fire
    def test_not_available_until_monday_does_not_fire(self):
        issues = check_slop("The quiz is not available until Monday.")
        assert not _has_slop(issues)


class TestFillerPhrases:
    def test_at_its_core_fires(self):
        issues = check_slop("At its core, this course is about justice.")
        assert _has_slop(issues)

    def test_in_todays_fires(self):
        issues = check_slop("In today's legal landscape, this is essential.")
        assert _has_slop(issues)

    def test_in_todays_lesson_not_linted(self):
        # "In today's lesson/class/reading" is normal course copy, not slop.
        assert not _has_slop(check_slop("In today's lesson you will analyze cell division."))
        assert not _has_slop(check_slop("In today's reading, focus on the dissent."))

    def test_its_worth_noting_fires(self):
        issues = check_slop("It's worth noting that jurisdiction varies.")
        assert _has_slop(issues)

    def test_at_the_end_of_the_day_fires(self):
        issues = check_slop("At the end of the day, the facts control.")
        assert _has_slop(issues)

    def test_when_it_comes_to_fires(self):
        issues = check_slop("When it comes to contracts, offer matters.")
        assert _has_slop(issues)

    def test_in_a_world_where_fires(self):
        issues = check_slop("In a world where laws change, adaptation is key.")
        assert _has_slop(issues)

    def test_the_reality_is_fires(self):
        issues = check_slop("The reality is that most cases settle.")
        assert _has_slop(issues)

    def test_clean_filler_free_does_not_fire(self):
        issues = check_slop("Most cases settle before trial.")
        assert not _has_slop(issues)


class TestMetaCommentary:
    def test_let_me_walk_you_through_fires(self):
        issues = check_slop("Let me walk you through the elements of a contract.")
        assert _has_slop(issues)

    def test_in_this_section_well_fires(self):
        issues = check_slop("In this section, we'll cover offer and acceptance.")
        assert _has_slop(issues)

    def test_as_well_see_fires(self):
        issues = check_slop("As we'll see, the common law rule applies here.")
        assert _has_slop(issues)

    def test_rest_of_this_explains_fires(self):
        issues = check_slop("The rest of this module explains the doctrine.")
        assert _has_slop(issues)

    def test_plain_structure_does_not_fire(self):
        issues = check_slop("This module covers offer, acceptance, and consideration.")
        assert not _has_slop(issues)


class TestVagueDeclaratives:
    def test_implications_significant_fires(self):
        issues = check_slop("The implications are significant.")
        assert _has_slop(issues)

    def test_stakes_are_high_fires(self):
        issues = check_slop("The stakes are high in this area of law.")
        assert _has_slop(issues)

    def test_reasons_structural_fires(self):
        issues = check_slop("The reasons are structural, not incidental.")
        assert _has_slop(issues)

    def test_consequences_real_fires(self):
        issues = check_slop("The consequences are real for litigants.")
        assert _has_slop(issues)

    def test_specific_declarative_does_not_fire(self):
        issues = check_slop("The statute triggers a 30-day limitation period.")
        assert not _has_slop(issues)


# ═══════════════════════════════════════════════════════════════════════════
# 2. Binary-contrast structures
# ═══════════════════════════════════════════════════════════════════════════

class TestBinaryContrast:
    def test_not_just_a_tool_but_a_tutor_fires(self):
        issues = check_slop("This is not just a tool, but a tutor.")
        assert _has_slop(issues)

    def test_isnt_x_its_y_fires(self):
        issues = check_slop("This isn't a quiz, it's a simulation.")
        assert _has_slop(issues)

    def test_the_answer_isnt_fires(self):
        issues = check_slop("The answer isn't memorization. It's application.")
        assert _has_slop(issues)

    def test_the_question_isnt_fires(self):
        issues = check_slop("The question isn't whether to file. It's when.")
        assert _has_slop(issues)

    # False-positive guard: common negation phrase must NOT fire
    def test_not_available_until_no_fire(self):
        issues = check_slop("This content is not available until the module opens.")
        assert not _has_slop(issues)


# ═══════════════════════════════════════════════════════════════════════════
# 3. Instructional carve-outs: must NOT fire
# ═══════════════════════════════════════════════════════════════════════════

class TestInstructionalCarveOut:
    def test_wh_question_prompt_no_fire(self):
        """Wh- question starters are valid discussion prompts."""
        issues = check_slop("Why does inflation rise during periods of low unemployment?")
        assert not _has_slop(issues)

    def test_adverb_in_objective_no_fire(self):
        """Adverbs in learning objectives and instructions must not fire."""
        issues = check_slop("Students will specifically identify the elements of negligence.")
        assert not _has_slop(issues)

    def test_adverb_sentence_no_fire(self):
        """A sentence containing an adverb must not fire."""
        issues = check_slop("Review each case carefully before the discussion.")
        assert not _has_slop(issues)

    def test_what_distinguishes_no_fire(self):
        issues = check_slop("What distinguishes a contract from a memorandum of understanding?")
        assert not _has_slop(issues)

    def test_second_person_no_fire(self):
        issues = check_slop("You will analyze the case and identify the elements.")
        assert not _has_slop(issues)

    def test_html_stripped_before_scan(self):
        """HTML tags must not produce false matches."""
        html = "<p>Review the <strong>module overview</strong> before the quiz.</p>"
        issues = check_slop(html)
        assert not _has_slop(issues)


# ═══════════════════════════════════════════════════════════════════════════
# 4. Severity is always info
# ═══════════════════════════════════════════════════════════════════════════

class TestSeverity:
    def test_severity_always_info(self):
        issues = check_slop("Here's the thing: Let that sink in. At its core, this is a deep dive.")
        slop = _slop_ids(issues)
        assert slop, "Expected slop findings"
        for issue in slop:
            assert issue.severity == "info", f"Expected info, got {issue.severity}"
            assert not issue.fixable

    def test_criterion_id_is_style_slop(self):
        issues = check_slop("It turns out the stakes are high.")
        slop = _slop_ids(issues)
        assert all(i.criterion_id == "style.slop" for i in slop)


# ═══════════════════════════════════════════════════════════════════════════
# 5. Dedupe + cap behavior
# ═══════════════════════════════════════════════════════════════════════════

class TestDedupeAndCap:
    def test_dedupe_same_phrase_fires_once(self):
        """The same phrase repeated in content should only produce one finding."""
        text = "It turns out. It turns out. It turns out."
        issues = check_slop(text)
        slop = _slop_ids(issues)
        # "it turns out" is one unique phrase key; should appear only once in cap issues
        phrase_issues = [i for i in slop if "It turns out" in i.message
                         or "it turns out" in i.message.lower()]
        assert len(phrase_issues) == 1

    def test_cap_at_10_plus_summary(self):
        """When more than 10 distinct slop phrases are present, a summary issue appears."""
        # Build content with 11+ distinct slop phrases
        phrases = [
            "Here's the thing:",
            "The truth is,",
            "Let me be clear",
            "It turns out",
            "Make no mistake,",
            "Let that sink in.",
            "It's worth noting",
            "This matters because",
            "At its core",
            "In today's world",
            "The reality is",
            "At the end of the day",
        ]
        text = " ".join(phrases) + " the rest follows."
        issues = check_slop(text)
        slop = _slop_ids(issues)
        # Some of these may deduplicate into fewer unique matches, but at minimum
        # we verify the cap logic: if total > 10, a summary issue ("...and N more") appears.
        # With 12 distinct entries, we expect a summary row.
        total_phrases = sum(1 for i in slop if "more slop" not in i.message)
        if total_phrases >= 10:
            summary = [i for i in slop if "more slop" in i.message.lower()]
            assert summary, "Expected a summary 'and N more' issue when cap exceeded"


# ═══════════════════════════════════════════════════════════════════════════
# 6. Wiring: check_page surfaces style.slop
# ═══════════════════════════════════════════════════════════════════════════

class TestWiringCheckPage:
    def test_check_page_surfaces_slop(self):
        html = "<p>Here's the thing: At its core, this module is a deep dive into law.</p>"
        issues = check_page(html)
        assert _has_slop(issues)

    def test_check_page_non_blocking(self):
        """Slop findings must never be error-severity in page context."""
        html = "<p>It turns out the stakes are high.</p>"
        issues = check_page(html)
        slop = _slop_ids(issues)
        for issue in slop:
            assert issue.severity != "error"


# ═══════════════════════════════════════════════════════════════════════════
# 7. Wiring: check_assessment surfaces style.slop via check_page delegation
# ═══════════════════════════════════════════════════════════════════════════

class TestWiringCheckAssessment:
    def test_check_assessment_surfaces_slop_via_description(self):
        data = {
            "name": "Module 1 Essay",
            "description": "<p>Here's the thing: this assignment is a deep dive.</p>",
            "points_possible": 100,
            "due_at": "2026-08-01T23:59:00Z",
            "rubric": [{"description": "Analysis"}],
        }
        issues = check_assessment(data)
        assert _has_slop(issues)

    def test_check_assessment_slop_is_info(self):
        data = {
            "name": "Essay",
            "description": "<p>It turns out the stakes are high for this essay.</p>",
            "points_possible": 50,
            "due_at": "2026-08-01T23:59:00Z",
            "rubric": [{"description": "Argument"}],
        }
        issues = check_assessment(data)
        slop = _slop_ids(issues)
        for issue in slop:
            assert issue.severity == "info"


# ═══════════════════════════════════════════════════════════════════════════
# 8. Wiring: check_discussion surfaces style.slop via check_page delegation
# ═══════════════════════════════════════════════════════════════════════════

class TestWiringCheckDiscussion:
    def test_check_discussion_surfaces_slop_via_message(self):
        # Make the message long enough to avoid the "too short" check
        long_slop_prompt = (
            "Here's the thing: At its core, this discussion is a deep dive into "
            "the consequences of constitutional interpretation. The reality is that "
            "the stakes are high. Moving forward, reflect on what you have learned "
            "about standing doctrine in federal courts and its implications for litigants."
        )
        data = {
            "title": "Module 2 Discussion",
            "message": f"<p>{long_slop_prompt}</p>",
            "require_initial_post": True,
        }
        issues = check_discussion(data)
        assert _has_slop(issues)


# ═══════════════════════════════════════════════════════════════════════════
# 9. Wiring: check_quiz surfaces style.slop on description + question_text
# ═══════════════════════════════════════════════════════════════════════════

class TestWiringCheckQuiz:
    def test_check_quiz_surfaces_slop_in_description(self):
        data = {
            "title": "Module 3 Quiz",
            "description": "<p>Here's the thing: this quiz is a deep dive.</p>",
            "allowed_attempts": 2,
            "due_at": "2026-08-01T23:59:00Z",
            "questions": [],
        }
        issues = check_quiz(data)
        assert _has_slop(issues)

    def test_check_quiz_surfaces_slop_in_question_text(self):
        data = {
            "title": "Module 4 Quiz",
            "description": "",
            "allowed_attempts": 2,
            "due_at": "2026-08-01T23:59:00Z",
            "questions": [
                {
                    "question_text": "<p>It turns out the stakes are high. Select the best answer.</p>",
                    "question_type": "multiple_choice_question",
                    "answers": [
                        {"comments": "Correct!", "weight": 100},
                        {"comments": "Incorrect.", "weight": 0},
                    ],
                }
            ],
        }
        issues = check_quiz(data)
        assert _has_slop(issues)

    def test_check_quiz_slop_is_info_never_blocks(self):
        data = {
            "title": "Quiz",
            "description": "<p>At its core, moving forward, the reality is deep.</p>",
            "allowed_attempts": 2,
            "due_at": "2026-08-01T23:59:00Z",
            "questions": [],
        }
        issues = check_quiz(data)
        slop = _slop_ids(issues)
        for issue in slop:
            assert issue.severity == "info"

    def test_clean_quiz_no_slop(self):
        data = {
            "title": "Module 1 Knowledge Check",
            "description": "<p>Answer each question based on the assigned reading.</p>",
            "allowed_attempts": 2,
            "due_at": "2026-08-01T23:59:00Z",
            "questions": [
                {
                    "question_text": "<p>Which element is required for contract formation?</p>",
                    "question_type": "multiple_choice_question",
                    "answers": [
                        {"comments": "Correct: mutual assent is required.", "weight": 100},
                        {"comments": "Incorrect: this is not an element.", "weight": 0},
                    ],
                }
            ],
        }
        issues = check_quiz(data)
        assert not _has_slop(issues)


# ═══════════════════════════════════════════════════════════════════════════
# 10. Empty / None input guard
# ═══════════════════════════════════════════════════════════════════════════

class TestEdgeCases:
    def test_empty_string_returns_empty(self):
        assert check_slop("") == []

    def test_none_returns_empty(self):
        assert check_slop(None) == []

    def test_html_only_no_text_returns_empty(self):
        assert check_slop("<p></p>") == []
