"""Tests for the needs_analysis course-config schema and skill guardrails."""

import json
from pathlib import Path

import pytest


PLUGIN_ROOT = Path(__file__).resolve().parent.parent
TEMPLATE_PATH = PLUGIN_ROOT / "templates" / "course-config.json"
SKILL_PATH = PLUGIN_ROOT / "skills" / "needs-analysis" / "SKILL.md"

REQUIRED_TOP_LEVEL_KEYS = {
    "context",
    "task_analysis",
    "learner_analysis",
    "planning_flags",
}

ARRAY_FIELD_PATHS = [
    ("context", "delivery_constraints"),
    ("task_analysis", "core_tasks"),
    ("task_analysis", "prerequisites"),
    ("task_analysis", "tools_and_systems"),
    ("learner_analysis", "motivation"),
    ("learner_analysis", "access_constraints"),
    ("planning_flags", "risk_notes"),
]

ALLOWED_TRAINING_FIT = {"clear", "mixed", "unclear"}

ALLOWED_BEM_CELLS = {
    "Information",
    "Instruments",
    "Incentives",
    "Knowledge",
    "Capacity",
    "Motives",
}

ALLOWED_BLOOM_LEVELS = {
    "Remember",
    "Understand",
    "Apply",
    "Analyze",
    "Evaluate",
    "Create",
}

ALLOWED_MAGER_PIPE_BRANCH = {
    "skill_knowledge",
    "will_environment",
    "mixed",
    "unclear",
    "not_applicable",
}


def load_template():
    return json.loads(TEMPLATE_PATH.read_text(encoding="utf-8"))


def load_skill_text():
    return SKILL_PATH.read_text(encoding="utf-8")


def test_needs_analysis_block_round_trips_through_json():
    config = load_template()
    sample_block = {
        "context": {
            "request_origin": "Instructor-led redesign",
            "delivery_constraints": ["fully online asynchronous", "7.5-week session"],
            "professional_context": "Pre-health gateway course",
            "timeline_notes": "Must launch in Fall A",
        },
        "task_analysis": {
            "core_tasks": ["Interpret case data", "Communicate evidence-based recommendations"],
            "core_task_bloom": {
                "Interpret case data": "Analyze",
                "Communicate evidence-based recommendations": "Evaluate",
            },
            "prerequisites": ["Foundational biology", "Basic academic writing"],
            "tools_and_systems": ["Canvas", "Respondus LockDown Browser"],
        },
        "learner_analysis": {
            "prior_knowledge": "Mixed preparation across first-year students",
            "motivation": ["degree progress", "career relevance"],
            "access_constraints": ["mobile-first access", "variable work schedules"],
            "audience_notes": "Many learners are balancing work and family responsibilities",
        },
        "planning_flags": {
            "training_fit": "mixed",
            "risk_notes": [
                "Learner success also depends on advising and onboarding",
                "Instruments cell: tool access uncertain for remote learners",
            ],
            "risk_notes_bem": [
                {"bem_cell": "Instruments", "note": "Tool access uncertain for remote learners"},
            ],
            "mager_pipe_branch": "skill_knowledge",
        },
    }

    config["needs_analysis"] = sample_block
    serialized = json.dumps(config)
    loaded = json.loads(serialized)

    assert loaded["needs_analysis"] == sample_block


def test_needs_analysis_schema_has_all_required_keys():
    config = load_template()

    assert "needs_analysis" in config
    assert set(config["needs_analysis"]) == REQUIRED_TOP_LEVEL_KEYS


@pytest.mark.parametrize("value", sorted(ALLOWED_TRAINING_FIT))
def test_training_fit_accepts_only_allowed_values(value):
    config = load_template()

    config["needs_analysis"]["planning_flags"]["training_fit"] = value

    assert config["needs_analysis"]["planning_flags"]["training_fit"] in ALLOWED_TRAINING_FIT


def test_training_fit_rejects_disallowed_values():
    config = load_template()

    disallowed = ["yes", "no", "advisory", "", None, "Clear"]
    for value in disallowed:
        config["needs_analysis"]["planning_flags"]["training_fit"] = value
        assert config["needs_analysis"]["planning_flags"]["training_fit"] not in ALLOWED_TRAINING_FIT


def test_needs_analysis_array_fields_are_lists():
    config = load_template()
    needs_analysis = config["needs_analysis"]

    for section, field in ARRAY_FIELD_PATHS:
        value = needs_analysis[section][field]
        assert isinstance(value, list), f"{section}.{field} should be a list"


def test_skill_rejects_learning_styles_language():
    skill_text = load_skill_text().lower()

    assert "vark" not in skill_text or "not evidence-backed" in skill_text
    assert "learning style" not in skill_text or "not evidence-backed" in skill_text
    assert "auditory learner" not in skill_text or "not evidence-backed" in skill_text
    assert "visual learner" not in skill_text or "not evidence-backed" in skill_text


# ---------------------------------------------------------------------------
# New HPI-field schema tests
# ---------------------------------------------------------------------------


def test_core_task_bloom_is_dict_with_valid_bloom_values():
    config = load_template()
    bloom = config["needs_analysis"]["task_analysis"]["core_task_bloom"]

    assert isinstance(bloom, dict), "core_task_bloom should be a dict"
    for key, value in bloom.items():
        assert isinstance(key, str), f"core_task_bloom key {key!r} should be a string"
        assert value in ALLOWED_BLOOM_LEVELS, (
            f"core_task_bloom value {value!r} not in allowed Bloom levels"
        )


@pytest.mark.parametrize("level", sorted(ALLOWED_BLOOM_LEVELS))
def test_core_task_bloom_accepts_all_bloom_levels(level):
    config = load_template()
    config["needs_analysis"]["task_analysis"]["core_task_bloom"] = {
        "Sample task": level
    }
    serialized = json.dumps(config)
    loaded = json.loads(serialized)

    assert loaded["needs_analysis"]["task_analysis"]["core_task_bloom"]["Sample task"] == level
    assert level in ALLOWED_BLOOM_LEVELS


def test_risk_notes_bem_is_list_of_valid_objects():
    config = load_template()
    bem = config["needs_analysis"]["planning_flags"]["risk_notes_bem"]

    assert isinstance(bem, list), "risk_notes_bem should be a list"
    for item in bem:
        assert isinstance(item, dict), f"risk_notes_bem item {item!r} should be a dict"
        assert set(item.keys()) == {"bem_cell", "note"}, (
            f"risk_notes_bem item should have exactly keys 'bem_cell' and 'note', got {set(item.keys())}"
        )
        assert item["bem_cell"] in ALLOWED_BEM_CELLS, (
            f"bem_cell {item['bem_cell']!r} not in allowed BEM cells"
        )
        assert isinstance(item["note"], str), (
            f"risk_notes_bem note should be a string, got {type(item['note'])}"
        )


@pytest.mark.parametrize("cell", sorted(ALLOWED_BEM_CELLS))
def test_risk_notes_bem_accepts_all_bem_cells(cell):
    config = load_template()
    config["needs_analysis"]["planning_flags"]["risk_notes_bem"] = [
        {"bem_cell": cell, "note": f"Test concern for {cell} cell"}
    ]
    serialized = json.dumps(config)
    loaded = json.loads(serialized)
    item = loaded["needs_analysis"]["planning_flags"]["risk_notes_bem"][0]

    assert item["bem_cell"] == cell
    assert item["bem_cell"] in ALLOWED_BEM_CELLS


def test_mager_pipe_branch_is_valid():
    config = load_template()
    branch = config["needs_analysis"]["planning_flags"]["mager_pipe_branch"]

    assert branch in ALLOWED_MAGER_PIPE_BRANCH, (
        f"mager_pipe_branch {branch!r} not in allowed values"
    )


@pytest.mark.parametrize("branch", sorted(ALLOWED_MAGER_PIPE_BRANCH))
def test_mager_pipe_branch_accepts_all_allowed_values(branch):
    config = load_template()
    config["needs_analysis"]["planning_flags"]["mager_pipe_branch"] = branch
    serialized = json.dumps(config)
    loaded = json.loads(serialized)

    assert loaded["needs_analysis"]["planning_flags"]["mager_pipe_branch"] == branch
    assert branch in ALLOWED_MAGER_PIPE_BRANCH


def test_mager_pipe_branch_rejects_disallowed_values():
    disallowed = ["yes", "no", "training", "environment", "gap", "", None, "Skill_Knowledge"]
    for value in disallowed:
        assert value not in ALLOWED_MAGER_PIPE_BRANCH, (
            f"Value {value!r} should not be in ALLOWED_MAGER_PIPE_BRANCH"
        )


def test_skill_references_gilbert_bem():
    skill_text = load_skill_text()

    assert "Gilbert" in skill_text or "BEM" in skill_text, (
        "SKILL.md should reference Gilbert BEM"
    )
    for cell in ALLOWED_BEM_CELLS:
        assert cell in skill_text, f"SKILL.md should reference BEM cell '{cell}'"


def test_skill_references_mager_pipe():
    skill_text = load_skill_text()

    has_mager_pipe = "Mager-Pipe" in skill_text or "Mager" in skill_text
    has_stakes_probe = "stakes were high" in skill_text or "life depended" in skill_text or "stakes-based" in skill_text
    assert has_mager_pipe or has_stakes_probe, (
        "SKILL.md should reference Mager-Pipe or use the stakes-based performance probe"
    )


def test_skill_references_bloom_levels():
    skill_text = load_skill_text()

    for level in ALLOWED_BLOOM_LEVELS:
        assert level in skill_text, f"SKILL.md should reference Bloom level '{level}'"


def test_legacy_block_without_new_fields_loads_without_error():
    """Backward-compat: a config missing the 3 new HPI fields should still load cleanly.

    Legacy consumer code reading the block as a plain dict should not raise.
    Missing fields are treated as defaults (empty dict/list, 'unclear'/'not_applicable').
    """
    legacy_block = {
        "context": {
            "request_origin": "Legacy course",
            "delivery_constraints": ["online asynchronous"],
            "professional_context": "",
            "timeline_notes": "",
        },
        "task_analysis": {
            "core_tasks": ["Read primary sources", "Write lab reports"],
            "prerequisites": ["Intro biology"],
            "tools_and_systems": ["Canvas"],
        },
        "learner_analysis": {
            "prior_knowledge": "First-year students",
            "motivation": ["degree progress"],
            "access_constraints": [],
            "audience_notes": "",
        },
        "planning_flags": {
            "training_fit": "clear",
            "risk_notes": [],
        },
    }

    # Simulate legacy load: new fields are absent — accessing them returns defaults
    task_analysis = legacy_block["task_analysis"]
    core_task_bloom = task_analysis.get("core_task_bloom", {})
    assert isinstance(core_task_bloom, dict)

    planning_flags = legacy_block["planning_flags"]
    risk_notes_bem = planning_flags.get("risk_notes_bem", [])
    assert isinstance(risk_notes_bem, list)

    mager_pipe_branch = planning_flags.get("mager_pipe_branch", "unclear")
    assert mager_pipe_branch in ALLOWED_MAGER_PIPE_BRANCH

    # Verify top-level keys still satisfy the canonical schema contract
    assert set(legacy_block.keys()) == REQUIRED_TOP_LEVEL_KEYS

    # Verify original fields are unchanged
    assert legacy_block["planning_flags"]["risk_notes"] == []
    assert legacy_block["task_analysis"]["core_tasks"] == [
        "Read primary sources", "Write lab reports"
    ]

    # Round-trip the legacy block through JSON without error
    serialized = json.dumps(legacy_block)
    loaded = json.loads(serialized)
    assert loaded == legacy_block
