"""Tests for scripts/session_calendar.py

All tests are unit tests — no network calls, no filesystem writes.
The lookup_term tests read standards/asu-academic-calendar.json; a tmp-path
fixture is used to test the miss/file-absent cases without touching real files.
"""

from __future__ import annotations

import json
import os
import sys
from datetime import date

import pytest

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

import session_calendar as sc


# ===========================================================================
# session_length_weeks
# ===========================================================================

class TestSessionLengthWeeks:
    def test_a_session(self):
        assert sc.session_length_weeks("A") == 7.5

    def test_b_session(self):
        assert sc.session_length_weeks("B") == 7.5

    def test_c_session(self):
        assert sc.session_length_weeks("C") == 15.0

    def test_full_session(self):
        assert sc.session_length_weeks("full") == 15.0

    def test_case_insensitive_a(self):
        assert sc.session_length_weeks("a") == 7.5

    def test_case_insensitive_c(self):
        assert sc.session_length_weeks("c") == 15.0

    def test_case_insensitive_full(self):
        assert sc.session_length_weeks("Full") == 15.0

    def test_unknown_type_raises(self):
        with pytest.raises(ValueError, match="Unknown session type"):
            sc.session_length_weeks("X")

    def test_summer_raises_with_message(self):
        # Summer sessions are out of scope for v1; error message should say so
        with pytest.raises(ValueError, match="Unknown session type"):
            sc.session_length_weeks("Summer")

    def test_empty_string_raises(self):
        with pytest.raises(ValueError):
            sc.session_length_weeks("")

    def test_error_lists_valid_types(self):
        with pytest.raises(ValueError) as exc_info:
            sc.session_length_weeks("Z")
        # Error message should list at least some valid types
        msg = str(exc_info.value)
        assert "A" in msg or "valid" in msg.lower()


# ===========================================================================
# build_deadlines
# ===========================================================================

class TestBuildDeadlines:
    """Exact date math for build_deadlines.

    Anchor: Fall 2026 A starts 2026-08-20
        target = 2026-08-20 - 28d = 2026-07-23
        floor  = 2026-08-20 - 14d = 2026-08-06
    These match the STP 533 conversion numbers cited in the spec.
    """

    def test_target_date_fall_2026_a(self):
        result = sc.build_deadlines(date(2026, 8, 20))
        assert result["target_date"] == date(2026, 7, 23)

    def test_floor_date_fall_2026_a(self):
        result = sc.build_deadlines(date(2026, 8, 20))
        assert result["floor_date"] == date(2026, 8, 6)

    def test_iso_string_input(self):
        result = sc.build_deadlines("2026-08-20")
        assert result["target_date"] == date(2026, 7, 23)
        assert result["floor_date"] == date(2026, 8, 6)

    def test_date_object_input(self):
        result = sc.build_deadlines(date(2026, 10, 14))
        # Fall 2026 B: start 2026-10-14
        # target = 2026-10-14 - 28d = 2026-09-16
        # floor  = 2026-10-14 - 14d = 2026-09-30
        assert result["target_date"] == date(2026, 9, 16)
        assert result["floor_date"] == date(2026, 9, 30)

    def test_returns_dict_with_both_keys(self):
        result = sc.build_deadlines("2026-08-20")
        assert set(result.keys()) == {"target_date", "floor_date"}

    def test_target_is_28_days_before(self):
        start = date(2026, 1, 1)
        result = sc.build_deadlines(start)
        delta = (start - result["target_date"]).days
        assert delta == 28

    def test_floor_is_14_days_before(self):
        start = date(2026, 1, 1)
        result = sc.build_deadlines(start)
        delta = (start - result["floor_date"]).days
        assert delta == 14


# ===========================================================================
# runway_status
# ===========================================================================

class TestRunwayStatus:
    """All three status branches with injected today."""

    SESSION_START = date(2026, 8, 20)
    # target = 2026-07-23, floor = 2026-08-06

    def test_comfortable_on_target_date(self):
        # today == target_date → comfortable
        result = sc.runway_status(self.SESSION_START, today=date(2026, 7, 23))
        assert result["status"] == "comfortable"

    def test_comfortable_before_target(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 7, 1))
        assert result["status"] == "comfortable"

    def test_tight_day_after_target(self):
        # today = target_date + 1 → tight
        result = sc.runway_status(self.SESSION_START, today=date(2026, 7, 24))
        assert result["status"] == "tight"

    def test_tight_on_floor_date(self):
        # today == floor_date → tight (still within floor)
        result = sc.runway_status(self.SESSION_START, today=date(2026, 8, 6))
        assert result["status"] == "tight"

    def test_past_floor_day_after_floor(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 8, 7))
        assert result["status"] == "past_floor"

    def test_past_floor_at_session_start(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 8, 20))
        assert result["status"] == "past_floor"

    def test_days_to_target_positive_when_comfortable(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 7, 1))
        assert result["days_to_target"] > 0

    def test_days_to_target_negative_when_past_target(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 8, 1))
        assert result["days_to_target"] < 0

    def test_days_to_floor_returned(self):
        result = sc.runway_status(self.SESSION_START, today=date(2026, 7, 23))
        assert "days_to_floor" in result

    def test_iso_string_today(self):
        result = sc.runway_status("2026-08-20", today="2026-07-01")
        assert result["status"] == "comfortable"

    def test_iso_string_session_start(self):
        result = sc.runway_status("2026-08-20", today=date(2026, 7, 23))
        assert result["status"] == "comfortable"


# ===========================================================================
# lookup_term
# ===========================================================================

class TestLookupTerm:
    """Hit + miss cases. Miss must return None, never raise."""

    def test_fall_2026_a_start(self):
        result = sc.lookup_term("Fall 2026", "A")
        assert result is not None
        assert result["start"] == "2026-08-20"

    def test_fall_2026_a_end(self):
        result = sc.lookup_term("Fall 2026", "A")
        assert result is not None
        assert result["end"] == "2026-10-09"

    def test_fall_2026_b(self):
        result = sc.lookup_term("Fall 2026", "B")
        assert result is not None
        assert result["start"] == "2026-10-14"
        assert result["end"] == "2026-12-04"

    def test_fall_2026_c(self):
        result = sc.lookup_term("Fall 2026", "C")
        assert result is not None
        assert result["start"] == "2026-08-20"
        assert result["end"] == "2026-12-04"

    def test_case_insensitive_session(self):
        result = sc.lookup_term("Fall 2026", "a")
        assert result is not None
        assert result["start"] == "2026-08-20"

    def test_unknown_term_returns_none(self):
        result = sc.lookup_term("Spring 2099", "A")
        assert result is None

    def test_unknown_session_returns_none(self):
        result = sc.lookup_term("Fall 2026", "X")
        assert result is None

    def test_miss_does_not_raise(self):
        # Must return None, never raise
        result = sc.lookup_term("NONEXISTENT 0000", "A")
        assert result is None

    def test_no_session_type_returns_whole_term(self):
        result = sc.lookup_term("Fall 2026")
        assert result is not None
        assert "A" in result
        assert "B" in result
        assert "C" in result

    def test_file_missing_returns_none(self, monkeypatch, tmp_path):
        """When the calendar JSON is absent, lookup_term returns None."""
        monkeypatch.setattr(sc, "_CALENDAR_JSON", tmp_path / "nonexistent.json")
        result = sc.lookup_term("Fall 2026", "A")
        assert result is None

    def test_malformed_json_returns_none(self, monkeypatch, tmp_path):
        bad_json = tmp_path / "bad.json"
        bad_json.write_text("{not valid json", encoding="utf-8")
        monkeypatch.setattr(sc, "_CALENDAR_JSON", bad_json)
        result = sc.lookup_term("Fall 2026", "A")
        assert result is None


# ===========================================================================
# Input acceptance — both date objects and ISO strings
# ===========================================================================

class TestDateInputFormats:
    def test_build_deadlines_accepts_date_object(self):
        result = sc.build_deadlines(date(2026, 8, 20))
        assert result["target_date"] == date(2026, 7, 23)

    def test_build_deadlines_accepts_iso_string(self):
        result = sc.build_deadlines("2026-08-20")
        assert result["target_date"] == date(2026, 7, 23)

    def test_runway_status_accepts_date_object(self):
        result = sc.runway_status(date(2026, 8, 20), today=date(2026, 7, 1))
        assert result["status"] == "comfortable"

    def test_runway_status_accepts_iso_string_for_start(self):
        result = sc.runway_status("2026-08-20", today=date(2026, 7, 1))
        assert result["status"] == "comfortable"

    def test_runway_status_accepts_iso_string_for_today(self):
        result = sc.runway_status(date(2026, 8, 20), today="2026-07-01")
        assert result["status"] == "comfortable"
