"""Tests for the deep-pass cognitive-demand feature (plan-assurance-deep-bloom.md).

A. Inventory attaches rubric_excerpt + discussion prompt + sets bloom_content_available.
B. Audit emits a bloom_content needs_ai_review item for EVERY weighted assessment,
   carries content_excerpt + rubric_excerpt + bloom_content_available, tags bank-locked
   items basis="format", and dedups structurally-identical items.
C. Renderer prefers the narrative's cognitive_demand object and labels the
   format-inferred weight.

Canvas is mocked throughout; the suite runs offline with no .env.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
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 (mirror tests/test_assurance_content_read.py harness)
# ---------------------------------------------------------------------------

def _mock_config(course_id="151110"):
    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": "introductory"},
    }


def _assignment(
    id,
    name="Assignment",
    points=100.0,
    group_id=1,
    submission_types=None,
    published=True,
    is_quiz=False,
    quiz_id=None,
    description="",
    rubric=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 rubric is not None:
        a["rubric"] = rubric
    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):
    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"}
    qmap = quiz_questions_map or {}

    def mock_paginated_get(url, headers, **kwargs):
        if "/questions" in url:
            import re
            m = re.search(r"/quizzes/(\d+)/questions", url)
            if m:
                return qmap.get(int(m.group(1)), [])
        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 _make_record(**kw):
    base = {
        "id": 1,
        "title": "Assignment",
        "canvas_type": "assignment",
        "engine": "assignment",
        "points": 100.0,
        "grade_weight_pct": 10.0,
        "submission_types": ["online_upload"],
        "has_rubric": False,
        "peer_reviews": False,
        "omit_from_final_grade": False,
        "published": True,
        "module": None,
        "group": "Assignments",
        "is_external_tool": False,
        "proctoring_signal": False,
        "bloom_level": "Unknown",
        "bloom_index": 0,
        "bloom_source": "unknown",
        "content_excerpt": "",
        "rubric_excerpt": "",
        "bloom_content_available": False,
        "_quiz_id": None,
    }
    base.update(kw)
    return base


def _call_score(records):
    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=""):
        return AA.score_course(cfg, inventory=inventory)


# ===========================================================================
# A. INVENTORY: rubric_excerpt + discussion prompt + bloom_content_available
# ===========================================================================

class TestInventoryDeepBloom:
    def test_rubric_excerpt_attached_from_criteria(self):
        rubric = [
            {"description": "Analysis", "long_description": "Evaluate the argument and weigh the evidence."},
            {"description": "Clarity", "long_description": "Writing is clear and organized."},
        ]
        groups = [_group(1, "Papers", 100.0, [
            _assignment(101, "Short Paper", description="Write a short paper.", rubric=rubric),
        ])]
        rec = _run_inventory(groups)["assessments"][0]
        assert "rubric_excerpt" in rec
        assert "Evaluate the argument" in rec["rubric_excerpt"]
        assert "Analysis:" in rec["rubric_excerpt"]
        # HTML-stripped, capped.
        assert "<" not in rec["rubric_excerpt"]

    def test_rubric_excerpt_strips_html(self):
        rubric = [{"description": "Crit", "long_description": "<b>Compare</b> two <em>theories</em>."}]
        groups = [_group(1, "G", 100.0, [_assignment(101, "Paper", rubric=rubric)])]
        rec = _run_inventory(groups)["assessments"][0]
        assert "<b>" not in rec["rubric_excerpt"]
        assert "Compare" in rec["rubric_excerpt"]

    def test_rubric_excerpt_empty_when_no_rubric(self):
        groups = [_group(1, "G", 100.0, [_assignment(101, "Paper", description="Do work.")])]
        rec = _run_inventory(groups)["assessments"][0]
        assert rec["rubric_excerpt"] == ""

    def test_discussion_prompt_attached_as_content_excerpt(self):
        # Graded discussion: topic message carries the prompt.
        disc_assignment = _assignment(
            201, "Module 1 Discussion", description="", points=10.0,
            submission_types=["discussion_topic"],
        )
        groups = [_group(1, "Discussions", 100.0, [disc_assignment])]
        topics = [{"id": 9001, "assignment_id": 201,
                   "message": "<p>Do you agree? Why? Are these practices ethical?</p>"}]
        rec = _run_inventory(groups, discussions=topics)["assessments"][0]
        assert rec["canvas_type"] == "discussion"
        assert "Do you agree? Why?" in rec["content_excerpt"]
        assert rec["bloom_content_available"] is True

    def test_bloom_content_available_true_with_prompt_or_rubric(self):
        groups = [_group(1, "G", 100.0, [
            _assignment(101, "Paper", description="Write an essay on the topic."),
        ])]
        rec = _run_inventory(groups)["assessments"][0]
        assert rec["bloom_content_available"] is True

    def test_bloom_content_available_false_for_banklocked_quiz(self):
        # Classic quiz with no description, no rubric, and no question stems exposed.
        quiz_a = _assignment(
            301, "Exam 1", points=50.0, is_quiz=True, quiz_id=4001,
            submission_types=["online_quiz"], description="",
        )
        groups = [_group(1, "Exams", 100.0, [quiz_a])]
        quizzes = [{"id": 4001, "title": "Exam 1"}]
        # quiz_questions_map empty -> no stems returned -> bank-locked.
        rec = _run_inventory(groups, quizzes=quizzes, quiz_questions_map={})["assessments"][0]
        assert rec["engine"] == "classic_quiz"
        assert rec["content_excerpt"] == ""
        assert rec["rubric_excerpt"] == ""
        assert rec["bloom_content_available"] is False

    def test_bloom_content_available_true_when_quiz_stems_exposed(self):
        quiz_a = _assignment(
            302, "Chapter 2 Quiz", points=20.0, is_quiz=True, quiz_id=4002,
            submission_types=["online_quiz"], description="",
        )
        groups = [_group(1, "Quizzes", 100.0, [quiz_a])]
        quizzes = [{"id": 4002, "title": "Chapter 2 Quiz"}]
        qmap = {4002: [{"question_text": "<p>What is the capital of France?</p>"}]}
        rec = _run_inventory(groups, quizzes=quizzes, quiz_questions_map=qmap)["assessments"][0]
        assert "capital of France" in rec["content_excerpt"]
        assert rec["bloom_content_available"] is True


# ===========================================================================
# B. AUDIT: bloom_content emitted for EVERY weighted assessment
# ===========================================================================

class TestAuditDeepBloom:
    def test_bloom_content_emitted_for_all_weighted(self):
        records = [
            _make_record(id=1, title="Short Paper", grade_weight_pct=30.0,
                         content_excerpt="Summarize the chapter.",
                         rubric_excerpt="Summarize: define and explain key terms.",
                         bloom_content_available=True, bloom_source="mlo",
                         bloom_level="Understand", bloom_index=2),
            _make_record(id=2, title="Module 1 Discussion", canvas_type="discussion",
                         engine="graded_discussion", grade_weight_pct=10.0,
                         content_excerpt="Do you agree? Why?",
                         bloom_content_available=True, bloom_source="mlo",
                         bloom_level="Understand", bloom_index=2),
        ]
        sc = _call_score(records)
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"]
        ids = {i["id"] for i in bc}
        assert {1, 2} <= ids, "Every weighted assessment should emit a bloom_content item"
        for item in bc:
            assert "content_excerpt" in item
            assert "rubric_excerpt" in item
            assert "bloom_content_available" in item

    def test_banklocked_item_tagged_basis_format(self):
        records = [
            _make_record(id=10, title="Exam 1", canvas_type="quiz", engine="classic_quiz",
                         grade_weight_pct=25.0, content_excerpt="", rubric_excerpt="",
                         bloom_content_available=False, bloom_source="objective_floor",
                         bloom_level="Understand", bloom_index=2),
        ]
        sc = _call_score(records)
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"]
        assert len(bc) == 1
        assert bc[0]["basis"] == "format"
        assert bc[0]["bloom_content_available"] is False

    def test_content_item_tagged_basis_content(self):
        records = [
            _make_record(id=11, title="Short Paper", grade_weight_pct=30.0,
                         content_excerpt="Write a 500-word reflection.",
                         rubric_excerpt="Reflect: connect course concepts to experience.",
                         bloom_content_available=True),
        ]
        sc = _call_score(records)
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"][0]
        assert bc["basis"] == "content"

    def test_structurally_identical_items_deduped(self):
        # 10 short papers sharing a template -> one representative.
        records = [
            _make_record(id=100 + i, title=f"Short Paper {i}", grade_weight_pct=5.0,
                         content_excerpt="Write a short paper.",
                         rubric_excerpt="Summarize the reading.",
                         bloom_content_available=True)
            for i in range(1, 11)
        ]
        sc = _call_score(records)
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"]
        assert len(bc) == 1, "Ten identical short papers should dedup to one representative"
        assert bc[0].get("cluster_size") == 10

    def test_zero_weight_not_emitted(self):
        records = [
            _make_record(id=1, title="Practice Paper", grade_weight_pct=0.0,
                         content_excerpt="practice", bloom_content_available=True),
        ]
        sc = _call_score(records)
        bc = [i for i in sc["needs_ai_review"] if i["judgment_type"] == "bloom_content"]
        assert bc == []


# ===========================================================================
# C. RENDERER: prefer narrative cognitive_demand, label + state format-inferred weight
#
# The renderer (scripts/assurance_report_html.py::_render_the_evidence) is owned by the
# base branch; these assertions pin the end-to-end deep-pass contract: when a narrative
# cognitive_demand is present, the rendered report shows the deep-read distribution,
# labels it as read from prompts/rubrics (deep pass), surfaces the per-cluster basis,
# and states the format-inferred share; otherwise it falls back to the deterministic
# distribution with no deep-pass label.
# ===========================================================================

# The deep-pass cognitive demand is authored into narrative["report"]["snapshot"]
# ["measures"] by Claude and rendered as the Design-axis Bloom bar; the report no
# longer auto-renders a narrative["cognitive_demand"] table. Renderer coverage for
# the Bloom bar lives in test_assurance_report_html.py. The deterministic deep-bloom
# emission logic (bloom_content items, dedup, format tagging) is covered above.
