"""Tests for scripts/course_level.py -- course-level detection + Bloom calibration.

Pure functions, no I/O. See docs/plan-assurance-accuracy-v2.md workstream 3.
"""
from __future__ import annotations

import sys
from pathlib import Path

_scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
if str(_scripts_dir) not in sys.path:
    sys.path.insert(0, str(_scripts_dir))

import course_level  # noqa: E402


class TestDetectLevel:
    """Spec-listed cases (docs/plan-assurance-accuracy-v2.md workstream 3)."""

    def test_com_100(self):
        result = course_level.detect_level("COM 100")
        assert result == {"level": 100, "band": "100", "label": "100-level"}

    def test_phi_101_floors_to_100(self):
        result = course_level.detect_level("PHI 101")
        assert result == {"level": 100, "band": "100", "label": "100-level"}

    def test_soc_334_floors_to_300(self):
        result = course_level.detect_level("SOC 334")
        assert result == {"level": 300, "band": "300", "label": "300-level"}

    def test_law_524_is_graduate(self):
        result = course_level.detect_level("LAW 524")
        assert result == {"level": 500, "band": "500", "label": "graduate"}

    def test_ars101_no_space(self):
        result = course_level.detect_level("ARS101")
        assert result == {"level": 100, "band": "100", "label": "100-level"}

    def test_foundations_no_digit_is_none(self):
        result = course_level.detect_level("Foundations")
        assert result == {"level": None, "band": None, "label": None}

    def test_ogl_720_clamps_to_700(self):
        result = course_level.detect_level("OGL 720")
        assert result == {"level": 700, "band": "700", "label": "graduate"}


class TestDetectLevelEdgeCases:
    def test_empty_string(self):
        assert course_level.detect_level("") == {"level": None, "band": None, "label": None}

    def test_none_input(self):
        assert course_level.detect_level(None) == {"level": None, "band": None, "label": None}

    def test_whitespace_only(self):
        assert course_level.detect_level("   ") == {"level": None, "band": None, "label": None}

    def test_200_level_label(self):
        assert course_level.detect_level("ENG 205")["label"] == "200-level"

    def test_400_level_label(self):
        assert course_level.detect_level("HST 499") == {"level": 400, "band": "400", "label": "400-level"}

    def test_600_level_is_graduate(self):
        result = course_level.detect_level("LAW 619")
        assert result == {"level": 600, "band": "600", "label": "graduate"}

    def test_above_700_clamps_down(self):
        # A stray high number (e.g. a mis-tagged section code) still clamps
        # into the valid band rather than producing an out-of-range level.
        result = course_level.detect_level("XYZ 850")
        assert result == {"level": 700, "band": "700", "label": "graduate"}

    def test_exact_hundred_boundary(self):
        assert course_level.detect_level("BIO 300")["level"] == 300

    def test_first_digit_run_wins(self):
        # "first \d+ in the code" -- the course code itself, not a trailing
        # section or year suffix appended after it.
        result = course_level.detect_level("PHI 101-002")
        assert result["level"] == 100

    def test_course_name_with_colon_suffix(self):
        # detect_level takes a course code, not a full "CODE: Title" name;
        # callers are expected to pass the code segment (see SKILL.md), but a
        # code-shaped prefix should still parse correctly if handed the whole
        # string, since \d+ finds the number wherever it sits.
        result = course_level.detect_level("PHI 101: Introduction to Philosophy")
        assert result["level"] == 100


class TestBloomExpectation:
    def test_100_level_floor_remember_ceiling_apply(self):
        result = course_level.bloom_expectation(100)
        assert result["floor"] == "Remember"
        assert result["expected_ceiling"] == "Apply"
        assert "note" in result and result["note"]

    def test_200_level_same_band_as_100(self):
        result = course_level.bloom_expectation(200)
        assert result["floor"] == "Remember"
        assert result["expected_ceiling"] == "Apply"

    def test_300_level_floor_apply_ceiling_analyze(self):
        result = course_level.bloom_expectation(300)
        assert result["floor"] == "Apply"
        assert result["expected_ceiling"] == "Analyze"

    def test_400_level_same_band_as_300(self):
        result = course_level.bloom_expectation(400)
        assert result["floor"] == "Apply"
        assert result["expected_ceiling"] == "Analyze"

    def test_500_level_floor_analyze_ceiling_create(self):
        result = course_level.bloom_expectation(500)
        assert result["floor"] == "Analyze"
        assert result["expected_ceiling"] == "Create"

    def test_600_and_700_same_band_as_500(self):
        for level in (600, 700):
            result = course_level.bloom_expectation(level)
            assert result["floor"] == "Analyze"
            assert result["expected_ceiling"] == "Create"

    def test_none_level_returns_empty(self):
        assert course_level.bloom_expectation(None) == {}

    def test_all_bands_have_notes(self):
        for level in (100, 300, 500):
            result = course_level.bloom_expectation(level)
            assert isinstance(result["note"], str) and result["note"]


class TestIntegration:
    """detect_level()'s output level feeds directly into bloom_expectation()."""

    def test_com_100_end_to_end(self):
        detected = course_level.detect_level("COM 100")
        expectation = course_level.bloom_expectation(detected["level"])
        assert expectation["floor"] == "Remember"
        assert expectation["expected_ceiling"] == "Apply"

    def test_law_524_end_to_end(self):
        detected = course_level.detect_level("LAW 524")
        expectation = course_level.bloom_expectation(detected["level"])
        assert expectation["floor"] == "Analyze"
        assert expectation["expected_ceiling"] == "Create"

    def test_unparseable_code_yields_no_calibration(self):
        detected = course_level.detect_level("Foundations")
        expectation = course_level.bloom_expectation(detected["level"])
        assert expectation == {}
