"""Tests for the alignment mockup decision-label generator."""

import json
import os
import sys

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


def _fixture_config_keep_and_add():
    """MLO1.1 has explicit EXTRACTED coverage; MLO1.2 has no coverage."""
    return {
        "course": {"id": "999", "name": "Decision Test Course"},
        "clos": [{"id": "CLO1", "label": "CLO1", "text": "Test CLO"}],
        "modules": [
            {
                "number": 1,
                "title": "Module 1",
                "mlos_draft": [
                    {"id": "MLO1.1", "text": "Estimate the slope and intercept.", "ladders_to": "CLO1"},
                    {"id": "MLO1.2", "text": "Interpret the fitted model.", "ladders_to": "CLO1"},
                ],
                "assessments": [
                    {
                        "type": "quiz",
                        "title": "M1 Quiz",
                        "mlo_ids": ["MLO1.1"],
                        "questions": [
                            {
                                "id": "Q1",
                                "text": "Which interpretation of the slope is best?",
                                "mlo_ids": ["MLO1.1"],
                            },
                        ],
                    },
                ],
            },
        ],
    }


def _fixture_config_optional_only():
    """MLO has no explicit MLO refs on assessments — INFERRED only."""
    return {
        "course": {"id": "888", "name": "Optional Test Course"},
        "clos": [{"id": "CLO1", "label": "CLO1", "text": "Test CLO"}],
        "modules": [
            {
                "number": 1,
                "title": "Module 1",
                "mlos_draft": [
                    {"id": "MLO1.1", "text": "Apply least squares.", "ladders_to": "CLO1"},
                ],
                "assessments": [
                    {
                        "type": "assignment",
                        "title": "M1 Homework — no explicit MLO",
                    },
                ],
            },
        ],
    }


def _build_graph(config):
    import alignment_graph_preview as agp

    return agp.build_graph_from_course_config(config)


def test_keep_label_for_extracted_mlo_coverage():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions = amd.build_decisions(graph)

    assert decisions["schema_version"] == amd.SCHEMA_VERSION
    assert decisions["course_id"] == "999"

    mlo11 = next(m for m in decisions["decisions_by_mlo"] if m["mlo_label"] == "MLO1.1")
    assert mlo11["overall_label"] == "KEEP"
    assert "explicit assessment evidence" in mlo11["overall_rationale"]
    assert len(mlo11["current_evidence"]) >= 1
    assert all(ev["confidence"] in ("EXTRACTED", "INFERRED", "AMBIGUOUS") for ev in mlo11["current_evidence"])


def test_add_label_for_mlo_without_assessment():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions = amd.build_decisions(graph)

    mlo12 = next(m for m in decisions["decisions_by_mlo"] if m["mlo_label"] == "MLO1.2")
    assert mlo12["overall_label"] == "ADD"
    assert "no assessment evidence" in mlo12["overall_rationale"].lower()
    assert "missing_evidence_note" in mlo12
    assert mlo12["current_evidence"] == []


def test_optional_label_for_inferred_only_coverage():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_optional_only())
    decisions = amd.build_decisions(graph)

    mlo11 = next(m for m in decisions["decisions_by_mlo"] if m["mlo_label"] == "MLO1.1")
    assert mlo11["overall_label"] == "OPTIONAL"
    assert "inferred" in mlo11["overall_rationale"].lower()
    assert all(ev["confidence"] == "INFERRED" for ev in mlo11["current_evidence"])


def test_replace_label_when_only_ambiguous_coverage():
    import alignment_mockup_decisions as amd

    # Construct synthetic graph with AMBIGUOUS-only evidence for a single MLO.
    graph = {
        "schema_version": "alignment-graph-preview/v1",
        "course_id": "777",
        "course_title": "Ambiguous Test",
        "source_mode": "local-course-config",
        "nodes": [
            {"id": "mlo:M1.1:aaa", "type": "mlo", "label": "MLO1.1", "text": "Test MLO", "metadata": {"module_number": 1}},
            {"id": "quiz_question:q1:bbb", "type": "quiz_question", "label": "Q1", "text": "Test question", "metadata": {"module_number": 1}},
        ],
        "edges": [
            {
                "id": "edge:aligns_to:1",
                "type": "aligns_to",
                "source": "quiz_question:q1:bbb",
                "target": "mlo:M1.1:aaa",
                "confidence": "AMBIGUOUS",
                "evidence": "weak link",
            },
        ],
        "gaps": {},
    }
    decisions = amd.build_decisions(graph)

    mlo = decisions["decisions_by_mlo"][0]
    assert mlo["overall_label"] == "REPLACE"
    item = next(i for i in decisions["decisions_by_item"] if i["item_id"] == "quiz_question:q1:bbb")
    assert item["decision_label"] == "REPLACE"
    assert item["confidence"] == "AMBIGUOUS"


def test_per_item_decisions_map_confidence_to_label():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions = amd.build_decisions(graph)

    items = decisions["decisions_by_item"]
    assert len(items) >= 1
    # Q1 has explicit mlo_ids → EXTRACTED → KEEP
    q1 = next(i for i in items if i["item_type"] == "quiz_question" and i["item_label"] == "Q1")
    assert q1["decision_label"] == "KEEP"
    assert q1["confidence"] == "EXTRACTED"
    assert q1["mapped_mlos"][0]["mlo_label"] == "MLO1.1"


def test_per_item_inferred_maps_to_optional():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_optional_only())
    decisions = amd.build_decisions(graph)

    items = decisions["decisions_by_item"]
    assert items, "Expected at least one assessment item for the INFERRED fixture"
    assignment = next(i for i in items if i["item_type"] == "assignment")
    assert assignment["decision_label"] == "OPTIONAL"
    assert assignment["confidence"] == "INFERRED"
    assert assignment["mapped_mlos"][0]["confidence"] == "INFERRED"


def test_per_item_ambiguous_maps_to_replace():
    import alignment_mockup_decisions as amd

    graph = {
        "schema_version": "alignment-graph-preview/v1",
        "course_id": "555",
        "nodes": [
            {"id": "mlo:M1.1:aaa", "type": "mlo", "label": "MLO1.1", "text": "MLO", "metadata": {"module_number": 1}},
            {"id": "quiz_question:q1:bbb", "type": "quiz_question", "label": "Q1", "text": "Q text", "metadata": {"module_number": 1}},
        ],
        "edges": [
            {
                "id": "edge:1",
                "type": "aligns_to",
                "source": "quiz_question:q1:bbb",
                "target": "mlo:M1.1:aaa",
                "confidence": "AMBIGUOUS",
                "evidence": "weak",
            },
        ],
        "gaps": {},
    }
    decisions = amd.build_decisions(graph)
    item = decisions["decisions_by_item"][0]
    assert item["decision_label"] == "REPLACE"
    assert item["confidence"] == "AMBIGUOUS"


def test_faculty_safe_rationale_no_forbidden_phrasing():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions = amd.build_decisions(graph)
    blob = json.dumps(decisions).lower()
    assert "bad alignment" not in blob
    assert "failed assessment" not in blob
    assert "defect" not in blob


def test_include_types_filters_assessment_parts():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions_quiz_only = amd.build_decisions(graph, include_types=("quiz_question",))

    item_types = {i["item_type"] for i in decisions_quiz_only["decisions_by_item"]}
    assert item_types <= {"quiz_question"}
    assert "assignment" not in item_types
    assert "rubric_criterion" not in item_types


def test_orphan_item_with_no_mlo_mapping_gets_replace():
    import alignment_mockup_decisions as amd

    graph = {
        "schema_version": "alignment-graph-preview/v1",
        "course_id": "666",
        "nodes": [
            {"id": "quiz_question:orphan:zzz", "type": "quiz_question", "label": "Orphan Q", "text": "No MLO", "metadata": {}},
        ],
        "edges": [],
        "gaps": {},
    }
    decisions = amd.build_decisions(graph)

    item = decisions["decisions_by_item"][0]
    assert item["decision_label"] == "REPLACE"
    assert item["mapped_mlos"] == []
    assert "no explicit MLO mapping" in item["rationale"]


def test_summary_counts_match_emitted_decisions():
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    decisions = amd.build_decisions(graph)
    counts = decisions["summary"]
    total_emitted = (
        sum(1 for _ in decisions["decisions_by_mlo"])
        + sum(1 for _ in decisions["decisions_by_item"])
    )
    assert sum(counts.values()) == total_emitted
    assert set(counts.keys()) == {"keep", "add", "replace", "optional"}


def test_cli_writes_default_decisions_path(tmp_path, monkeypatch):
    import alignment_mockup_decisions as amd

    graph = _build_graph(_fixture_config_keep_and_add())
    graph_dir = tmp_path / "courses" / "999" / "alignment"
    graph_dir.mkdir(parents=True)
    (graph_dir / "graph.json").write_text(json.dumps(graph), encoding="utf-8")
    monkeypatch.setattr(amd, "PLUGIN_ROOT", tmp_path)

    assert amd.main(["--course-id", "999"]) == 0
    output_path = graph_dir / "mockup_decisions.json"
    assert output_path.exists()
    decisions = json.loads(output_path.read_text(encoding="utf-8"))
    assert decisions["schema_version"] == amd.SCHEMA_VERSION
    assert decisions["course_id"] == "999"
    assert decisions["summary"]["keep"] >= 1


def test_decisions_artifact_never_calls_canvas_write_helpers(monkeypatch):
    """Decisions builder is graph-only and must not touch Canvas API."""
    import alignment_mockup_decisions as amd

    # Importing canvas_api before the build should be safe (lazy in graph_preview),
    # but the decision-builder should never invoke any canvas helpers at all.
    import canvas_api

    write_helpers = (
        "create_page", "update_page",
        "create_quiz", "add_quiz_question", "update_quiz_question",
        "create_assignment", "update_assignment",
        "create_discussion", "update_discussion",
    )
    for name in write_helpers:
        if hasattr(canvas_api, name):
            monkeypatch.setattr(
                canvas_api,
                name,
                lambda *a, **kw: (_ for _ in ()).throw(AssertionError(f"canvas write helper called: {name}")),
            )

    graph = _build_graph(_fixture_config_keep_and_add())
    amd.build_decisions(graph)
    # No assertion needed — if any write helper fires, the lambda raises.
