"""Tests for post-P3 preflight lints: Bloom jargon, REPLACE tone, badge style.

Mirrors test_p3.py style: isolated_root fixture, monkeypatching, parametrize.
All three preflight checks emit WARNINGS, not blocking errors.
"""
from __future__ import annotations

import os
import sys
from pathlib import Path

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import build_mockup_quiz
import canvas_api
import preflight_checks
import staging_manager
import staging_schema


COURSE_ID = "218764"
FIXTURE_DIR = Path(__file__).parent / "fixtures"


def _config(course_id: str = COURSE_ID) -> dict:
    return {
        "token": "fake",
        "domain": "asu-dev.instructure.com",
        "course_id": course_id,
        "base_url": "https://asu-dev.instructure.com/api/v1",
        "course_url": f"https://asu-dev.instructure.com/api/v1/courses/{course_id}",
        "headers": {"Authorization": "Bearer fake"},
    }


@pytest.fixture()
def isolated_root(tmp_path, monkeypatch):
    monkeypatch.setattr(staging_manager, "get_plugin_root", lambda: str(tmp_path))
    monkeypatch.setattr(staging_schema, "get_plugin_root", lambda: str(tmp_path))
    monkeypatch.setattr(canvas_api, "get_plugin_root", lambda: str(tmp_path))
    monkeypatch.setattr(canvas_api, "get_config", lambda course_id=None: _config(str(course_id or COURSE_ID)))
    monkeypatch.setattr(canvas_api, "require_course_id", lambda config: None)
    return tmp_path


def _live_questions():
    return [
        {
            "id": 1001,
            "question_name": "Current Q1",
            "question_text": "<p>Students use the sampling distribution to solve a scenario.</p>",
            "question_type": "multiple_choice_question",
            "points_possible": 1,
            "answers": [{"answer_text": "Current answer A", "answer_weight": 100}],
        },
        {
            "id": 1002,
            "question_name": "Current Q2",
            "question_text": "<p>Students define a p-value.</p>",
            "question_type": "multiple_choice_question",
            "points_possible": 1,
            "answers": [{"answer_text": "Current answer B", "answer_weight": 100}],
        },
        {
            "id": 1003,
            "question_name": "Current Q3",
            "question_text": "<p>Placeholder current text.</p>",
            "question_type": "multiple_choice_question",
            "points_possible": 1,
            "answers": [{"answer_text": "Current answer C", "answer_weight": 100}],
        },
        {
            "id": 1004,
            "question_name": "Current Q4",
            "question_text": "<p>Students compose an interpretation.</p>",
            "question_type": "multiple_choice_question",
            "points_possible": 1,
            "answers": [{"answer_text": "Current answer D", "answer_weight": 100}],
        },
    ]


@pytest.fixture()
def staged_mockup_quiz(isolated_root, monkeypatch):
    monkeypatch.setattr(canvas_api, "get_quiz_questions", lambda config, quiz_id: _live_questions())
    result = build_mockup_quiz.build_staged_quiz(
        COURSE_ID,
        FIXTURE_DIR / "sample_quiz_decisions.yaml",
        "mockup-quiz",
    )
    assert result["ok"], result
    return staging_manager.get_staged(COURSE_ID, "quizzes", "mockup-quiz")


# ═══════════════════════════════════════════════════════════════════════════
# FEATURE A — Bloom jargon lint
# ═══════════════════════════════════════════════════════════════════════════

@pytest.mark.parametrize("banned_term", [
    "Remember level",
    "Apply level",
    "Understand level",
    "Analyze level",
    "Evaluate level",
    "Create level",
    "Bloom",
    "cognitive level",
    "cognitive demand",
    "cognitive load level",
])
def test_bloom_jargon_catches_each_banned_term(banned_term):
    """Each banned term triggers a bloom_jargon warning."""
    content = f"<p>This item targets the {banned_term} per MLO 2.</p>"
    issues = preflight_checks.check_bloom_jargon(content)
    assert len(issues) >= 1
    assert all(i.criterion_id == "bloom_jargon" for i in issues)
    assert all(i.severity == "warning" for i in issues)


@pytest.mark.parametrize("banned_term", [
    "remember level",
    "BLOOM",
    "Cognitive Demand",
])
def test_bloom_jargon_catches_case_insensitive(banned_term):
    """Banned-term detection is case-insensitive."""
    content = f"<p>{banned_term} consideration here.</p>"
    issues = preflight_checks.check_bloom_jargon(content)
    assert len(issues) >= 1


def test_bloom_jargon_passes_plain_language():
    """Plain-language rationales do not trigger bloom_jargon."""
    plain_rationales = [
        "<p>MLO 2 calls for building and interpreting a confidence interval.</p>",
        "<p>This item asks students to apply the formula to a new dataset.</p>",
        "<p>Closes a gap on MLO 3 — no current item assesses interval interpretation.</p>",
        "<p>Aligns with MLO 1 because students must construct an argument from evidence.</p>",
    ]
    for content in plain_rationales:
        issues = preflight_checks.check_bloom_jargon(content)
        assert issues == [], f"Unexpected jargon issues for: {content!r} — got {issues}"


def test_bloom_jargon_check_via_tier1_page(tmp_path):
    """check_page_tier1 surfaces bloom_jargon issues from page HTML."""
    html = "<p>This item targets the Bloom taxonomy at the Apply level.</p>"
    issues = preflight_checks.check_page_tier1(html, slug="m1-overview")
    bloom_issues = [i for i in issues if i.criterion_id == "bloom_jargon"]
    assert len(bloom_issues) >= 1


def test_bloom_jargon_check_via_tier1_quiz():
    """check_quiz_tier1 surfaces bloom_jargon from question_text fields."""
    quiz = {
        "title": "Test Quiz",
        "questions": [
            {
                "question_name": "Q1",
                "question_text": "<p>This targets the cognitive level of Analyze.</p>",
                "answers": [],
            }
        ],
    }
    issues = preflight_checks.check_quiz_tier1(quiz)
    bloom_issues = [i for i in issues if i.criterion_id == "bloom_jargon"]
    assert len(bloom_issues) >= 1


def test_build_mockup_quiz_no_bloom_jargon(staged_mockup_quiz):
    """build_mockup_quiz.py output across all decisions contains zero Bloom terms."""
    import re
    bloom_re = re.compile(
        r"\bRemember level\b|\bApply level\b|\bUnderstand level\b|\bAnalyze level\b"
        r"|\bEvaluate level\b|\bCreate level\b|\bBloom\b|\bcognitive level\b"
        r"|\bcognitive demand\b|\bcognitive load level\b",
        re.I,
    )
    for question in staged_mockup_quiz["questions"]:
        q_text = question.get("question_text", "")
        m = bloom_re.search(q_text)
        assert m is None, (
            f"Bloom jargon '{m.group()}' found in question {question.get('question_name')!r}: "
            f"{q_text[:200]}"
        )


def test_build_mockup_quiz_static_template_assertion_passes():
    """The module-level Bloom jargon regression guard in build_mockup_quiz.py passes at import."""
    # If the assertion fires on import, the fixture collection would already have failed.
    # This test makes the guard's intent explicit in the test suite.
    for s in build_mockup_quiz._STATIC_TEMPLATE_STRINGS:
        assert not build_mockup_quiz._BLOOM_JARGON_RE.search(s), (
            f"Static template contains Bloom jargon: {s!r}"
        )


# ═══════════════════════════════════════════════════════════════════════════
# FEATURE B — Collaborative tone constraints for REPLACE rationales
# ═══════════════════════════════════════════════════════════════════════════

@pytest.mark.parametrize("directive_phrase", [
    "MLO 2 requires students to apply",
    "the current item is wrong",
    "you should revise this",
    "you must replace this item",
    "the item fails to assess MLO 3",
    "we need to replace this question",
    "students fail to demonstrate",
    "this is incorrect because the MLO calls for Apply",
])
def test_replace_tone_catches_directive_phrase(directive_phrase):
    """Each denylist phrase triggers replace_tone_directive in a REPLACE-tagged context."""
    # Embed the directive phrase in content that looks like a REPLACE-tagged rationale
    content = (
        f'<span style="display:inline-block; background:#8C1D40; color:#FFFFFF; '
        f'padding:3px 9px; border-radius:4px; font-size:12px;">REPLACE</span>'
        f"<p>{directive_phrase}.</p>"
    )
    issues = preflight_checks.check_replace_tone(content)
    assert len(issues) >= 1, f"No issue raised for directive phrase: {directive_phrase!r}"
    assert all(i.criterion_id == "replace_tone_directive" for i in issues)
    assert all(i.severity == "warning" for i in issues)


@pytest.mark.parametrize("directive_phrase", [
    "MLO 3 requires proof",
    "you should consider this",
])
def test_replace_tone_catches_directive_via_decision_yaml(directive_phrase):
    """check_replace_tone detects directives when decision_yaml tag is REPLACE."""
    content = f"<p>{directive_phrase}.</p>"
    decision_yaml = {"tag": "REPLACE"}
    issues = preflight_checks.check_replace_tone(content, decision_yaml=decision_yaml)
    assert len(issues) >= 1
    assert issues[0].criterion_id == "replace_tone_directive"


@pytest.mark.parametrize("collab_phrase", [
    "MLO X calls for a different evidence target",
    "if you'd like an item that assesses interpretation",
    "open to your preferred direction on wording",
    "this could be strengthened by asking students to apply",
    "happy to revise either direction",
    "one possibility is to reframe the scenario",
])
def test_replace_tone_passes_collaborative_phrases(collab_phrase):
    """Allow-list collaborative phrases do not trigger replace_tone_directive."""
    content = (
        f'<span style="display:inline-block; background:#8C1D40; color:#FFFFFF; '
        f'padding:3px 9px; border-radius:4px; font-size:12px;">REPLACE</span>'
        f"<p>{collab_phrase}.</p>"
    )
    issues = preflight_checks.check_replace_tone(content)
    tone_issues = [i for i in issues if i.criterion_id == "replace_tone_directive"]
    assert tone_issues == [], f"False positive for collaborative phrase: {collab_phrase!r}"


def test_replace_tone_not_replace_tagged_is_silent():
    """Non-REPLACE content is not flagged even if directive phrases appear."""
    content = "<p>The item fails to address the MLO, you must revise this.</p>"
    # No REPLACE badge, no decision_yaml
    issues = preflight_checks.check_replace_tone(content)
    assert issues == []


def test_replace_tone_keep_tagged_is_silent():
    """KEEP-tagged content is not flagged for replace_tone_directive."""
    content = (
        '<span style="background:#78BE20">KEEP</span>'
        "<p>The item fails to assess MLO 2 in the current wording.</p>"
    )
    decision_yaml = {"tag": "KEEP"}
    issues = preflight_checks.check_replace_tone(content, decision_yaml=decision_yaml)
    assert issues == []


def test_build_mockup_quiz_replace_template_no_directive_phrases(staged_mockup_quiz):
    """build_mockup_quiz.py REPLACE output contains zero denylist directive phrases."""
    import re
    # Load denylist patterns
    directive_pats = [
        re.compile(r"MLO \d+ requires", re.I),
        re.compile(r"the current item is wrong", re.I),
        re.compile(r"you should", re.I),
        re.compile(r"you must", re.I),
        re.compile(r"the item fails to", re.I),
        re.compile(r"we need to replace", re.I),
        re.compile(r"students fail to", re.I),
        re.compile(r"this is incorrect because", re.I),
    ]

    replace_questions = [
        q for q in staged_mockup_quiz["questions"]
        if "REPLACE" in q.get("question_name", "")
    ]
    assert len(replace_questions) >= 1, "No REPLACE questions found in staged quiz"

    for question in replace_questions:
        q_text = question.get("question_text", "")
        plain = preflight_checks._strip_html(q_text)
        for pat in directive_pats:
            m = pat.search(plain)
            assert m is None, (
                f"Directive phrase '{m.group()}' found in REPLACE question "
                f"{question.get('question_name')!r}"
            )


# ═══════════════════════════════════════════════════════════════════════════
# FEATURE C — ASU-brand pill badge defaults
# ═══════════════════════════════════════════════════════════════════════════

def test_build_mockup_quiz_keep_badge_canonical(staged_mockup_quiz):
    """KEEP badge uses canonical ASU green #78BE20."""
    keep_questions = [
        q for q in staged_mockup_quiz["questions"]
        if "KEEP" in q.get("question_name", "") and "REPLACE" not in q.get("question_name", "")
    ]
    assert len(keep_questions) >= 1, "No KEEP questions found"
    for question in keep_questions:
        assert "#78BE20" in question["question_text"], (
            f"KEEP badge missing #78BE20 in {question.get('question_name')!r}"
        )


def test_build_mockup_quiz_replace_badge_canonical(staged_mockup_quiz):
    """REPLACE badge uses canonical ASU maroon #8C1D40."""
    replace_questions = [
        q for q in staged_mockup_quiz["questions"]
        if "REPLACE" in q.get("question_name", "")
    ]
    assert len(replace_questions) >= 1, "No REPLACE questions found"
    for question in replace_questions:
        assert "#8C1D40" in question["question_text"], (
            f"REPLACE badge missing #8C1D40 in {question.get('question_name')!r}"
        )


def test_build_mockup_quiz_add_badge_canonical(staged_mockup_quiz):
    """ADD badge uses canonical ASU gold #FFC627."""
    add_questions = [
        q for q in staged_mockup_quiz["questions"]
        if "ADD" in q.get("question_name", "") and "REPLACE" not in q.get("question_name", "")
    ]
    assert len(add_questions) >= 1, "No ADD questions found"
    for question in add_questions:
        assert "#FFC627" in question["question_text"], (
            f"ADD badge missing #FFC627 in {question.get('question_name')!r}"
        )


def test_badge_constants_match_canonical_values():
    """build_mockup_quiz badge constants match canonical values from preflight_checks."""
    assert "#78BE20" in build_mockup_quiz.KEEP_BADGE
    assert "#8C1D40" in build_mockup_quiz.REPLACE_BADGE
    assert "#FFC627" in build_mockup_quiz.ADD_BADGE
    assert "#8C1D40" in build_mockup_quiz.OPTIONAL_CHIP


def test_preflight_badge_style_catches_flat_green_keep():
    """check_badge_style warns on flat-color KEEP badge (e.g. P3 #2d8659)."""
    old_badge = (
        '<span style="display:inline-block;border-radius:4px;padding:2px 8px;'
        'font-size:12px;font-weight:700;background:#2d8659;color:#ffffff;">KEEP</span>'
    )
    issues = preflight_checks.check_badge_style(old_badge)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert len(badge_issues) >= 1
    assert badge_issues[0].severity == "warning"


def test_preflight_badge_style_catches_blue_add():
    """check_badge_style warns on blue ADD badge (e.g. P3 #0374b5)."""
    old_badge = (
        '<span style="display:inline-block;background:#0374b5;color:#ffffff;">ADD</span>'
    )
    issues = preflight_checks.check_badge_style(old_badge)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert len(badge_issues) >= 1


def test_preflight_badge_style_catches_gold_replace():
    """check_badge_style warns on gold REPLACE badge (P3 used #ffc627 for REPLACE)."""
    old_badge = (
        '<span style="display:inline-block;background:#ffc627;color:#191919;">REPLACE</span>'
    )
    issues = preflight_checks.check_badge_style(old_badge)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert len(badge_issues) >= 1


def test_preflight_badge_style_passes_canonical_keep():
    """Canonical KEEP badge passes without issues."""
    issues = preflight_checks.check_badge_style(preflight_checks.CANONICAL_KEEP_BADGE)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert badge_issues == []


def test_preflight_badge_style_passes_canonical_replace():
    """Canonical REPLACE badge passes without issues."""
    issues = preflight_checks.check_badge_style(preflight_checks.CANONICAL_REPLACE_BADGE)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert badge_issues == []


def test_preflight_badge_style_passes_canonical_add():
    """Canonical ADD badge passes without issues."""
    issues = preflight_checks.check_badge_style(preflight_checks.CANONICAL_ADD_BADGE)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert badge_issues == []


def test_preflight_badge_style_passes_canonical_optional():
    """Canonical OPTIONAL chip passes without issues."""
    issues = preflight_checks.check_badge_style(preflight_checks.CANONICAL_OPTIONAL_CHIP)
    badge_issues = [i for i in issues if i.criterion_id == "badge_style_mismatch"]
    assert badge_issues == []


def test_build_mockup_quiz_full_output_no_non_canonical_badges(staged_mockup_quiz):
    """All badge HTML in staged quiz questions uses canonical colors only."""
    non_canonical_colors = [
        "#2d8659",  # P3 green
        "#03893d",  # flat green
        "#0374b5",  # P3 blue
        "#6b7280",  # gray OPTIONAL
        "#f7b500",  # alternate gold
    ]
    for question in staged_mockup_quiz["questions"]:
        q_text = question.get("question_text", "").lower()
        for color in non_canonical_colors:
            assert color.lower() not in q_text, (
                f"Non-canonical color {color} found in question "
                f"{question.get('question_name')!r}"
            )
