"""Tests for the assurance content-read fixes (plan-assurance-content-read.md).

Criteria B-E each have at least one test here:
  B. content_excerpt + bloom_content needs_ai_review in assurance_inventory/audit
  C. AIAS prefilter changes (prompt/chatgpt removed; policy_excerpts carried)
  D. Tri-state proctoring + description scan in lane classification
  E. Dedup of needs_ai_review fan-out (cluster_size, cluster_members)

Criterion A (MLO heading-list fallback) is owned by PR #171 on main and is
covered by tests/test_objective_extractor.py::HeadingListMLOFallbackTest.
"""

import os
import sys
from unittest.mock import MagicMock, patch

import pytest

HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPTS = os.path.join(HERE, "..", "scripts")
sys.path.insert(0, SCRIPTS)

import assurance_inventory as AI
import assurance_audit as AA


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------

def _mock_config(course_id="12345"):
    return {
        "token": "fake",
        "domain": "canvas.test.edu",
        "course_id": str(course_id),
        "base_url": "https://canvas.test.edu/api/v1",
        "course_url": f"https://canvas.test.edu/api/v1/courses/{course_id}",
        "headers": {"Authorization": "Bearer fake"},
        "course_name": "Test Course",
        "course": {"tier": "advanced"},
    }


def _assignment(
    id,
    name="Assignment",
    points=100.0,
    group_id=1,
    submission_types=None,
    published=True,
    is_quiz=False,
    quiz_id=None,
    description="",
    external_tool_tag=None,
):
    a = {
        "id": id,
        "name": name,
        "points_possible": points,
        "assignment_group_id": group_id,
        "submission_types": submission_types or ["online_upload"],
        "omit_from_final_grade": False,
        "workflow_state": "published" if published else "unpublished",
        "published": published,
        "peer_reviews": False,
        "is_quiz_assignment": is_quiz,
        "description": description,
    }
    if quiz_id is not None:
        a["quiz_id"] = quiz_id
    if external_tool_tag is not None:
        a["external_tool_tag"] = external_tool_tag
    return a


def _group(id, name, weight, assignments):
    return {"id": id, "name": name, "group_weight": weight, "assignments": assignments}


def _run_inventory(groups, quizzes=None, discussions=None, weighted=False, quiz_questions_map=None):
    """Run build_inventory with Canvas calls mocked."""
    cfg = _mock_config()
    course_resp = MagicMock()
    course_resp.status_code = 200
    course_resp.json.return_value = {"apply_assignment_group_weights": weighted, "name": "Test"}

    empty_clos = {"clos": [], "bloom_index": 0, "bloom_level": "Unknown", "source": "none"}

    # quiz_questions_map: quiz_id -> list[question dicts]
    qmap = quiz_questions_map or {}

    def mock_paginated_get(url, headers, **kwargs):
        if "/questions" in url:
            # Extract quiz id from URL.
            import re
            m = re.search(r"/quizzes/(\d+)/questions", url)
            if m:
                qid = int(m.group(1))
                return qmap.get(qid, [])
        return groups

    with (
        patch("assurance_inventory.canvas_api._request_with_retry", return_value=course_resp),
        patch("assurance_inventory.canvas_api.paginated_get", side_effect=mock_paginated_get),
        patch("assurance_inventory.canvas_api.list_quizzes", return_value=quizzes or []),
        patch("assurance_inventory.canvas_api.list_discussions", return_value=discussions or []),
        patch("assurance_inventory._build_module_map", return_value={}),
        patch("assurance_inventory.objective_extractor.extract_course_clos", return_value=empty_clos),
        patch("assurance_inventory.objective_extractor.extract_module_mlos", return_value={}),
    ):
        return AI.build_inventory(cfg)


def _call_score(records, syllabus_text=""):
    """Call score_course with a pre-built inventory (no Canvas calls)."""
    cfg = _mock_config()
    inventory = {
        "assessments": records,
        "course_weighting_mode": "points_only",
        "totals": {"counted_points": 100.0, "grade_weight_pct_sum": 100.0},
        "course_clos": {"clos": [], "bloom_index": 0, "bloom_level": "Unknown", "source": "none"},
        "module_mlos": {},
        "assumptions": [],
    }
    with patch("assurance_audit._fetch_syllabus_text", return_value=syllabus_text):
        return AA.score_course(cfg, inventory=inventory)


def _make_record(
    id=1,
    title="Assignment",
    canvas_type="assignment",
    engine="assignment",
    points=100.0,
    grade_weight_pct=10.0,
    submission_types=None,
    has_rubric=False,
    peer_reviews=False,
    bloom_level="Unknown",
    bloom_index=0,
    bloom_source="unknown",
    proctoring_signal=False,
    content_excerpt="",
):
    return {
        "id": id,
        "title": title,
        "canvas_type": canvas_type,
        "engine": engine,
        "points": points,
        "grade_weight_pct": grade_weight_pct,
        "submission_types": submission_types or ["online_upload"],
        "has_rubric": has_rubric,
        "peer_reviews": peer_reviews,
        "omit_from_final_grade": False,
        "published": True,
        "module": None,
        "group": "Assignments",
        "is_external_tool": False,
        "proctoring_signal": proctoring_signal,
        "bloom_level": bloom_level,
        "bloom_index": bloom_index,
        "bloom_source": bloom_source,
        "content_excerpt": content_excerpt,
        "_quiz_id": None,
    }



# ===========================================================================
# CRITERION B: content_excerpt + bloom_content needs_ai_review
# ===========================================================================

class TestCriterionB_ContentExcerptAndBloomContent:
    """Criterion B: Bloom-Unknown assessments carry content_excerpt; score_course emits
    bloom_content needs_ai_review items with content_excerpt."""

    def test_assignment_description_attached_as_content_excerpt(self):
        groups = [
            _group(1, "Assignments", 100.0, [
                _assignment(101, "Generic Homework", description="<p>Analyze the case study data.</p>"),
            ]),
        ]
        result = _run_inventory(groups)
        rec = result["assessments"][0]
        assert "content_excerpt" in rec
        assert "Analyze the case study data" in rec["content_excerpt"]

    def test_content_excerpt_stripped_of_html(self):
        groups = [
            _group(1, "G", 100.0, [
                _assignment(101, "Paper", description="<b>Write</b> an <em>essay</em>."),
            ]),
        ]
        result = _run_inventory(groups)
        rec = result["assessments"][0]
        assert "<b>" not in rec["content_excerpt"]
        assert "Write" in rec["content_excerpt"]

    def test_content_excerpt_capped_at_max_chars(self):
        long_desc = "A" * 5000
        groups = [
            _group(1, "G", 100.0, [_assignment(101, "Long Paper", description=long_desc)]),
        ]
        result = _run_inventory(groups)
        rec = result["assessments"][0]
        assert len(rec["content_excerpt"]) <= AI.CONTENT_EXCERPT_MAX_CHARS

    def test_classic_quiz_question_stems_fetched_for_bloom_unknown(self):
        # A Bloom-Unknown classic quiz: questions should be fetched and attached.
        quiz = _assignment(101, "Module 3 Quiz", is_quiz=True, quiz_id=77,
                           submission_types=["online_quiz"])
        groups = [_group(1, "Quizzes", 100.0, [quiz])]
        quizzes = [{"id": 77, "access_code": None, "ip_filter": None}]
        questions = [
            {"question_text": "<p>Which of the following best describes a contract?</p>"},
            {"question_text": "<p>Identify the elements required for offer and acceptance.</p>"},
        ]
        result = _run_inventory(groups, quizzes=quizzes, quiz_questions_map={77: questions})
        rec = result["assessments"][0]
        assert rec.get("content_excerpt"), "Expected question stems in content_excerpt"
        assert "contract" in rec["content_excerpt"].lower() or "offer" in rec["content_excerpt"].lower()

    def test_bloom_content_items_emitted_for_unknown_weighted_assessments(self):
        records = [
            _make_record(id=1, title="Generic Assignment", bloom_source="unknown",
                         grade_weight_pct=30.0, content_excerpt="Analyze the contract clause."),
            _make_record(id=2, title="Oral Presentation", bloom_source="title",
                         bloom_level="Evaluate", bloom_index=5, grade_weight_pct=20.0),
        ]
        sc = _call_score(records)
        types = [i["judgment_type"] for i in sc["needs_ai_review"]]
        assert "bloom_content" in types, "Expected bloom_content item for Bloom-Unknown assessment"
        bc_items = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"]
        assert bc_items[0]["id"] == 1
        assert bc_items[0]["content_excerpt"] == "Analyze the contract clause."
        assert bc_items[0]["bloom_source"] == "content_ai"

    def test_bloom_content_not_emitted_for_zero_weight(self):
        records = [
            _make_record(id=1, title="Practice Quiz", bloom_source="unknown",
                         grade_weight_pct=0.0, content_excerpt="What is consideration?"),
        ]
        sc = _call_score(records)
        types = [i["judgment_type"] for i in sc["needs_ai_review"]]
        assert "bloom_content" not in types, "Zero-weight items should not emit bloom_content"

    def test_bloom_content_emitted_for_grounded_bloom_deep_pass(self):
        # Deep pass: bloom_content is now emitted for EVERY weighted assessment, even
        # title-grounded ones, so Claude can confirm the level from the real task text
        # rather than trusting the title verb. (Was: not emitted for grounded bloom.)
        records = [
            _make_record(id=1, title="Analyze Case Study", bloom_source="title",
                         bloom_level="Analyze", bloom_index=4, grade_weight_pct=40.0),
        ]
        sc = _call_score(records)
        types = [i["judgment_type"] for i in sc["needs_ai_review"]]
        assert "bloom_content" in types, "Deep pass emits bloom_content for every weighted assessment"
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"][0]
        assert bc["deterministic_bloom"] == "Analyze"
        assert bc["deterministic_bloom_source"] == "title"


# ===========================================================================
# CRITERION C: AIAS prefilter changes (prompt/chatgpt removed; policy_excerpts)
# ===========================================================================

class TestCriterionC_AIASPrefilter:
    """Criterion C: 'prompt' and 'chatgpt' are not standalone AIAS triggers;
    policy_excerpts carries matched text spans."""

    def test_prompt_alone_does_not_trigger_aias(self):
        # Many assignment descriptions use "prompt" (writing prompt, submission prompt).
        result = AA._detect_aias_presence(
            "",
            [("assignment:Essay", "Respond to the following writing prompt in 500 words.")],
        )
        assert not result["declared_presence"], "'prompt' alone should NOT trigger AIAS detection"

    def test_chatgpt_alone_does_not_trigger_aias(self):
        result = AA._detect_aias_presence(
            "",
            [("assignment:Memo", "Do not use ChatGPT to write this memo.")],
        )
        # "chatgpt" alone no longer in _AIAS_POLICY_KEYWORDS.
        assert not result["declared_presence"], "'chatgpt' alone should NOT trigger AIAS detection"

    def test_generative_ai_triggers_aias(self):
        result = AA._detect_aias_presence(
            "Use of generative AI must be disclosed per the course AI policy.",
            [],
        )
        assert result["declared_presence"]

    def test_ai_policy_phrase_triggers_aias(self):
        result = AA._detect_aias_presence(
            "Please review the AI use policy before submitting.",
            [],
        )
        assert result["declared_presence"]

    def test_policy_excerpts_carried_for_claude_inline_read(self):
        """policy_excerpts must carry the matched text span (criterion C)."""
        syllabus = (
            "Course Policies: Students are expected to follow the AI use policy "
            "described at the beginning of the syllabus."
        )
        result = AA._detect_aias_presence(syllabus, [])
        assert result["declared_presence"]
        assert len(result["policy_excerpts"]) >= 1
        # The excerpt should contain the matched keyword context.
        assert any("ai use policy" in ex.lower() for ex in result["policy_excerpts"])

    def test_aias_item_carries_policy_excerpt_via_dimension_d(self):
        """Dimension D aias dict carries policy_excerpts for Claude's read."""
        records = [_make_record(id=1, title="Essay", grade_weight_pct=100.0)]
        sc = _call_score(records, syllabus_text="The AI use policy requires disclosure.")
        policy_excerpts = sc["dimensions"]["D"]["aias"].get("policy_excerpts", [])
        assert len(policy_excerpts) >= 1


# ===========================================================================
# CRITERION D: Tri-state proctoring + description scan in lane classification
# ===========================================================================

class TestCriterionD_TristateProctoring:
    """Criterion D: _detect_proctoring returns True/False/'unknown';
    'unknown' not counted as Lane 1; description scan for oral/process signals."""

    def test_classic_quiz_with_access_code_returns_true(self):
        assignment = _assignment(1, is_quiz=True, submission_types=["online_quiz"])
        quiz_detail = {"access_code": "secret", "ip_filter": None}
        result = AI._detect_proctoring(assignment, quiz_detail)
        assert result is True

    def test_classic_quiz_confirmed_no_access_code_returns_false(self):
        assignment = _assignment(1, is_quiz=True, submission_types=["online_quiz"])
        quiz_detail = {"access_code": None, "ip_filter": None}
        result = AI._detect_proctoring(assignment, quiz_detail)
        assert result is False

    def test_classic_quiz_no_detail_returns_unknown(self):
        """quiz_detail=None for a classic quiz -> 'unknown' (cannot confirm proctoring)."""
        assignment = _assignment(1, is_quiz=True, submission_types=["online_quiz"])
        result = AI._detect_proctoring(assignment, None)
        assert result == "unknown"

    def test_unknown_proctoring_not_counted_as_lane1(self):
        """A classic quiz with unknown proctoring is NOT a Lane 1 anchor."""
        rec = _make_record(id=1, title="Module Exam", canvas_type="quiz",
                           engine="classic_quiz", submission_types=["online_quiz"],
                           grade_weight_pct=50.0, proctoring_signal="unknown")
        lane = AA._classify_lane(rec)
        # Should be lane2 or mismatch, never lane1 from proctoring alone.
        assert lane != "lane1" or rec.get("peer_reviews") or any(
            kw in rec.get("title", "").lower() for kw in AA._ORAL_KEYWORDS | AA._PROCESS_TRAIL_KEYWORDS
        ), f"Unknown proctoring should not produce lane1, got: {lane}"

    def test_unknown_proctoring_lane2_for_quiz(self):
        rec = _make_record(id=1, title="Unit Quiz", canvas_type="quiz",
                           engine="classic_quiz", submission_types=["online_quiz"],
                           grade_weight_pct=10.0, proctoring_signal="unknown")
        lane = AA._classify_lane(rec)
        assert lane == "lane2", f"Expected lane2 for unknown-proctored quiz, got: {lane}"

    def test_unknown_proctoring_mismatch_for_high_weight(self):
        rec = _make_record(id=1, title="Final Exam", canvas_type="quiz",
                           engine="classic_quiz", submission_types=["online_quiz"],
                           grade_weight_pct=40.0, proctoring_signal="unknown")
        lane = AA._classify_lane(rec)
        assert lane in ("mismatch", "lane2"), (
            f"Unknown-proctored high-weight item should be mismatch or lane2, got: {lane}"
        )

    def test_unknown_not_counted_as_secured_in_coverage(self):
        """_compute_assurance_coverage excludes 'unknown' proctoring from anchors."""
        records = [
            _make_record(id=1, title="Final Exam", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=100.0, proctoring_signal="unknown"),
        ]
        # Classify lane first (unknown proctoring -> mismatch or lane2 for high-weight).
        lanes = {r["id"]: AA._classify_lane(r) for r in records}
        coverage = AA._compute_assurance_coverage(records, lanes)
        assert coverage["anchor_count"] == 0, (
            "Unknown proctoring must not count as a secured anchor"
        )

    def test_description_scan_finds_process_trail_keyword(self):
        """Criterion A: process-trail keyword in content_excerpt → lane2 + lane_review (not lane1).

        Before fix: 'portfolio' in description promoted to lane1.
        After fix: keyword hits are advisory; only structured metadata (proctoring_signal/peer_reviews)
        promotes to lane1.
        """
        rec = _make_record(id=1, title="Paper Assignment",
                           submission_types=["online_upload"],
                           grade_weight_pct=30.0,
                           content_excerpt="Submit a final portfolio with all drafts and revisions.")
        lane = AA._classify_lane(rec)
        assert lane == "lane2", (
            f"Process-trail keyword in description must yield lane2 (advisory), got: {lane}"
        )

    def test_lane_review_items_emitted_for_unknown_proctoring_high_weight(self):
        """score_course emits lane_review items for unknown-proctored high-weight items."""
        records = [
            _make_record(id=1, title="Final Exam", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=50.0, proctoring_signal="unknown"),
        ]
        sc = _call_score(records)
        lane_review = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "lane_review"]
        assert len(lane_review) >= 1, "Expected lane_review item for unknown-proctored high-weight exam"
        item = lane_review[0]
        assert item["proctoring_signal"] == "unknown"

    def test_report_states_unknown_not_unproctored(self):
        """proctoring_signal='unknown' is preserved in the lane_review item (not coerced to False)."""
        records = [
            _make_record(id=1, title="Midterm", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=40.0, proctoring_signal="unknown"),
        ]
        sc = _call_score(records)
        lane_review = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "lane_review"]
        if lane_review:
            # The item must carry the tri-state, not coerce to False.
            assert lane_review[0]["proctoring_signal"] == "unknown"


# ===========================================================================
# CRITERION E: Dedup of needs_ai_review fan-out
# ===========================================================================

class TestCriterionE_Dedup:
    """Criterion E: structurally-identical assessments are clustered;
    one representative item emitted per (cluster x judgment_type)."""

    def test_title_cluster_key_strips_trailing_chapter(self):
        assert AA._title_cluster_key("Quiz: Chapter 3") == AA._title_cluster_key("Quiz: Chapter 7")

    def test_title_cluster_key_strips_trailing_number(self):
        assert AA._title_cluster_key("Week 5 Discussion") == AA._title_cluster_key("Week 12 Discussion")

    def test_structurally_identical_items_deduplicated(self):
        """10 chapter quizzes of same type + weight -> 1 representative item per judgment_type."""
        records = [
            _make_record(
                id=i,
                title=f"Quiz: Chapter {i}",
                canvas_type="quiz",
                engine="classic_quiz",
                submission_types=["online_quiz"],
                grade_weight_pct=5.0,
                has_rubric=False,
                bloom_source="unknown",
            )
            for i in range(1, 11)
        ]
        ai_items = [
            {
                "id": r["id"],
                "name": r["title"],
                "points": r["points"],
                "grade_weight_pct": r["grade_weight_pct"],
                "question": r["title"],
                "judgment_type": "authenticity_5df",
                "prompt_version": AA.PROMPT_VERSION,
                "content_hash": AA._content_hash(r["title"]),
                "label": "within-course diagnostic, not cross-course comparable",
            }
            for r in records
        ]
        deduped = AA._dedup_ai_items(ai_items, records)
        # All 10 share the same structural key -> 1 representative.
        auth_items = [i for i in deduped if i["judgment_type"] == "authenticity_5df"]
        assert len(auth_items) == 1, (
            f"Expected 1 deduplicated authenticity item, got {len(auth_items)}"
        )

    def test_cluster_size_and_members_recorded(self):
        records = [
            _make_record(id=i, title=f"SUBMIT: Ch{i} Short Paper",
                         submission_types=["online_upload"], grade_weight_pct=5.0,
                         has_rubric=False, bloom_source="unknown")
            for i in range(1, 6)
        ]
        ai_items = [
            {
                "id": r["id"], "name": r["title"], "points": r["points"],
                "grade_weight_pct": r["grade_weight_pct"], "question": r["title"],
                "judgment_type": "fire_quality",
                "prompt_version": AA.PROMPT_VERSION,
                "content_hash": AA._content_hash(r["title"]),
                "label": "within-course diagnostic, not cross-course comparable",
            }
            for r in records
        ]
        deduped = AA._dedup_ai_items(ai_items, records)
        rep = next((i for i in deduped if i["judgment_type"] == "fire_quality"), None)
        assert rep is not None
        assert rep.get("cluster_size") == 5, f"Expected cluster_size=5, got {rep.get('cluster_size')}"
        assert len(rep.get("cluster_members", [])) == 4

    def test_dedup_reduces_item_count_materially(self):
        """Simulates a COM 100-style course: 37 generic assessments -> materially fewer items."""
        # 7 chapter quizzes (authenticity + aias_level = 2 items each)
        # 7 generic discussion posts (authenticity + fire_quality + aias_level = 3 each)
        # 7 generic assignments (authenticity + aias_level = 2 each)
        records = []
        ai_items = []
        for i in range(1, 8):
            r = _make_record(id=i, title=f"Quiz Ch{i}", canvas_type="quiz",
                             engine="classic_quiz", submission_types=["online_quiz"],
                             grade_weight_pct=3.0, has_rubric=False)
            records.append(r)
            for jtype in ("authenticity_5df", "aias_level"):
                ai_items.append({
                    "id": r["id"], "name": r["title"], "points": r["points"],
                    "grade_weight_pct": r["grade_weight_pct"], "question": r["title"],
                    "judgment_type": jtype, "prompt_version": AA.PROMPT_VERSION,
                    "content_hash": AA._content_hash(r["title"]),
                    "label": "within-course diagnostic, not cross-course comparable",
                })
        for i in range(8, 15):
            r = _make_record(id=i, title=f"Discussion W{i}", canvas_type="discussion",
                             engine="graded_discussion", submission_types=["discussion_topic"],
                             grade_weight_pct=3.0, has_rubric=False)
            records.append(r)
            for jtype in ("authenticity_5df", "fire_quality", "aias_level"):
                ai_items.append({
                    "id": r["id"], "name": r["title"], "points": r["points"],
                    "grade_weight_pct": r["grade_weight_pct"], "question": r["title"],
                    "judgment_type": jtype, "prompt_version": AA.PROMPT_VERSION,
                    "content_hash": AA._content_hash(r["title"]),
                    "label": "within-course diagnostic, not cross-course comparable",
                })
        original_count = len(ai_items)
        deduped = AA._dedup_ai_items(ai_items, records)
        deduped_count = len(deduped)
        assert deduped_count < original_count * 0.7, (
            f"Expected material reduction via dedup; original={original_count}, deduped={deduped_count}"
        )

    def test_different_judgment_types_not_merged(self):
        """Items with the same structural key but different judgment_types stay separate."""
        records = [
            _make_record(id=1, title="Quiz Ch1", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=5.0),
            _make_record(id=2, title="Quiz Ch2", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=5.0),
        ]
        ai_items = [
            {"id": 1, "name": "Quiz Ch1", "points": 5.0, "grade_weight_pct": 5.0,
             "question": "Quiz Ch1", "judgment_type": "authenticity_5df",
             "prompt_version": AA.PROMPT_VERSION,
             "content_hash": AA._content_hash("Quiz Ch1"),
             "label": "within-course diagnostic, not cross-course comparable"},
            {"id": 2, "name": "Quiz Ch2", "points": 5.0, "grade_weight_pct": 5.0,
             "question": "Quiz Ch2", "judgment_type": "aias_level",
             "prompt_version": AA.PROMPT_VERSION,
             "content_hash": AA._content_hash("Quiz Ch2"),
             "label": "within-course diagnostic, not cross-course comparable"},
        ]
        deduped = AA._dedup_ai_items(ai_items, records)
        jtypes = {i["judgment_type"] for i in deduped}
        assert "authenticity_5df" in jtypes
        assert "aias_level" in jtypes

    def test_score_course_needs_ai_review_count_reduced_for_identical_items(self):
        """Integration: score_course's dedup reduces fan-out for repetitive courses."""
        records = [
            _make_record(id=i, title=f"Quiz Ch{i}", canvas_type="quiz",
                         engine="classic_quiz", submission_types=["online_quiz"],
                         grade_weight_pct=100.0 / 10, has_rubric=False,
                         bloom_source="unknown")
            for i in range(1, 11)
        ]
        sc = _call_score(records)
        # Without dedup: 10 quizzes * ~4 items each = ~40 items minimum.
        # With dedup: should be far fewer (all 10 quizzes cluster to ~1 per judgment_type).
        count = len(sc["needs_ai_review"])
        # At most ~8 unique judgment_types exist; should be well under 40.
        assert count < 30, f"Expected < 30 deduped items, got {count}"
