""" Tests für State Engine. Testet: - Phasen-Management - Antizipations-Regeln - API Endpoints """ import pytest from datetime import datetime, timedelta from fastapi.testclient import TestClient import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from main import app from state_engine import ( AnticipationEngine, PhaseService, TeacherContext, SchoolYearPhase, ClassSummary, Event, TeacherStats, Suggestion, SuggestionPriority, RULES ) client = TestClient(app) class TestSchoolYearPhase: """Tests für SchoolYearPhase.""" def test_all_phases_defined(self): """Testet dass alle 9 Phasen definiert sind.""" phases = list(SchoolYearPhase) assert len(phases) == 9 assert SchoolYearPhase.ONBOARDING in phases assert SchoolYearPhase.ARCHIVED in phases def test_phase_values(self): """Testet Phase-Werte.""" assert SchoolYearPhase.ONBOARDING.value == "onboarding" assert SchoolYearPhase.SEMESTER_END.value == "semester_end" class TestTeacherContext: """Tests für TeacherContext.""" def test_create_context(self): """Testet Erstellung eines Kontexts.""" ctx = TeacherContext( teacher_id="test-teacher", school_id="test-school", school_year_id="test-year" ) assert ctx.teacher_id == "test-teacher" assert ctx.current_phase == SchoolYearPhase.ONBOARDING def test_has_completed_milestone(self): """Testet Meilenstein-Prüfung.""" ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", completed_milestones=["consent_accept", "profile_complete"] ) assert ctx.has_completed_milestone("consent_accept") assert not ctx.has_completed_milestone("school_select") def test_has_learning_units(self): """Testet Lerneinheiten-Prüfung.""" ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", stats=TeacherStats(learning_units_created=0) ) assert not ctx.has_learning_units() ctx.stats.learning_units_created = 5 assert ctx.has_learning_units() def test_to_dict(self): """Testet Konvertierung zu Dict.""" ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test" ) d = ctx.to_dict() assert "teacher_id" in d assert "current_phase" in d assert d["current_phase"] == "onboarding" class TestPhaseService: """Tests für PhaseService.""" def test_get_next_phase(self): """Testet nächste Phase.""" service = PhaseService() assert service.get_next_phase(SchoolYearPhase.ONBOARDING) == SchoolYearPhase.SCHOOL_YEAR_START assert service.get_next_phase(SchoolYearPhase.SCHOOL_YEAR_START) == SchoolYearPhase.TEACHING_SETUP assert service.get_next_phase(SchoolYearPhase.ARCHIVED) is None def test_check_and_transition_onboarding(self): """Testet automatischen Übergang von Onboarding.""" service = PhaseService() # Ohne alle Meilensteine ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.ONBOARDING, completed_milestones=["school_select", "consent_accept"] ) assert service.check_and_transition(ctx) is None # Mit allen Meilensteinen ctx.completed_milestones.append("profile_complete") new_phase = service.check_and_transition(ctx) assert new_phase == SchoolYearPhase.SCHOOL_YEAR_START def test_can_transition_to(self): """Testet Übergangs-Prüfung.""" service = PhaseService() ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.ONBOARDING, completed_milestones=["school_select", "consent_accept", "profile_complete"] ) # Erlaubter Übergang assert service.can_transition_to(ctx, SchoolYearPhase.SCHOOL_YEAR_START) # Nicht erlaubter Übergang (falsche Reihenfolge) assert not service.can_transition_to(ctx, SchoolYearPhase.PERFORMANCE_1) class TestAnticipationEngine: """Tests für AnticipationEngine.""" def test_get_suggestions_no_classes(self): """Testet Vorschläge ohne Klassen.""" engine = AnticipationEngine() ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.SCHOOL_YEAR_START, classes=[] ) suggestions = engine.get_suggestions(ctx) # Sollte "Erste Klasse anlegen" Vorschlag haben assert any(s.id == "create_first_class" for s in suggestions) def test_get_suggestions_priority_order(self): """Testet Priorisierung der Vorschläge.""" engine = AnticipationEngine() ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.ONBOARDING, completed_milestones=[] ) suggestions = engine.get_suggestions(ctx) # Vorschläge sollten nach Priorität sortiert sein if len(suggestions) >= 2: assert suggestions[0].priority.value <= suggestions[1].priority.value def test_get_suggestions_max_limit(self): """Testet Limit von max. 5 Vorschlägen.""" engine = AnticipationEngine(max_suggestions=5) ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.PERFORMANCE_1, stats=TeacherStats( exams_scheduled=5, exams_graded=0, unanswered_messages=10 ), upcoming_events=[ Event( type="exam", title="Test", date=datetime.now() + timedelta(days=3), in_days=3 ) ] ) suggestions = engine.get_suggestions(ctx) assert len(suggestions) <= 5 def test_get_top_suggestion(self): """Testet Top-Vorschlag.""" engine = AnticipationEngine() ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.SCHOOL_YEAR_START, classes=[] ) top = engine.get_top_suggestion(ctx) assert top is not None assert top.priority == SuggestionPriority.URGENT def test_suggestions_by_category(self): """Testet Gruppierung nach Kategorie.""" engine = AnticipationEngine() ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.ONBOARDING ) by_category = engine.get_suggestions_by_category(ctx) assert isinstance(by_category, dict) class TestRules: """Tests für vordefinierte Regeln.""" def test_rules_count(self): """Testet Anzahl der Regeln.""" assert len(RULES) >= 15 # Mindestens 15 Regeln def test_rule_no_classes(self): """Testet 'no_classes' Regel.""" rule = next(r for r in RULES if r.id == "no_classes") ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.SCHOOL_YEAR_START, classes=[] ) suggestion = rule.evaluate(ctx) assert suggestion is not None assert suggestion.id == "create_first_class" # Mit Klassen sollte keine Suggestion kommen ctx.classes.append(ClassSummary( class_id="1", name="7a", grade_level=7, student_count=25, subject="Deutsch" )) assert rule.evaluate(ctx) is None def test_rule_phase_restriction(self): """Testet Phasen-Einschränkung.""" rule = next(r for r in RULES if r.id == "no_classes") # In erlaubter Phase ctx = TeacherContext( teacher_id="test", school_id="test", school_year_id="test", current_phase=SchoolYearPhase.SCHOOL_YEAR_START, classes=[] ) assert rule.evaluate(ctx) is not None # In nicht erlaubter Phase ctx.current_phase = SchoolYearPhase.PERFORMANCE_1 assert rule.evaluate(ctx) is None class TestStateEngineAPI: """Tests für State Engine API.""" def test_get_context(self): """Testet Context-Abruf.""" response = client.get("/api/state/context?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "context" in data assert "phase_info" in data def test_get_phase(self): """Testet Phasen-Abruf.""" response = client.get("/api/state/phase?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "current_phase" in data assert "phase_info" in data def test_get_all_phases(self): """Testet Abruf aller Phasen.""" response = client.get("/api/state/phases") assert response.status_code == 200 data = response.json() assert "phases" in data assert len(data["phases"]) >= 8 def test_get_suggestions(self): """Testet Vorschläge-Abruf.""" response = client.get("/api/state/suggestions?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "suggestions" in data assert "current_phase" in data assert "priority_counts" in data def test_get_top_suggestion(self): """Testet Top-Vorschlag-Abruf.""" response = client.get("/api/state/suggestions/top?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "suggestion" in data or "message" in data def test_get_dashboard(self): """Testet Dashboard-Abruf.""" response = client.get("/api/state/dashboard?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "context" in data assert "suggestions" in data assert "stats" in data assert "progress" in data assert "phases" in data def test_complete_milestone(self): """Testet Meilenstein-Abschluss.""" response = client.post( "/api/state/milestone?teacher_id=milestone-test", json={"milestone": "test_milestone"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert data["milestone"] == "test_milestone" assert "test_milestone" in data["completed_milestones"] def test_get_next_phase(self): """Testet nächste Phase.""" response = client.get("/api/state/next-phase?teacher_id=test-api") assert response.status_code == 200 data = response.json() assert "current_phase" in data assert "next_phase" in data or "message" in data class TestDemoEndpoints: """Tests für Demo-Endpoints.""" def test_demo_add_class(self): """Testet Demo-Klasse hinzufügen.""" response = client.post( "/api/state/demo/add-class?teacher_id=demo-test&name=7a&grade_level=7&student_count=25" ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert data["classes"] >= 1 def test_demo_add_event(self): """Testet Demo-Event hinzufügen.""" response = client.post( "/api/state/demo/add-event?teacher_id=demo-test&event_type=exam&title=Mathe-Test&in_days=7" ) assert response.status_code == 200 data = response.json() assert data["success"] is True def test_demo_update_stats(self): """Testet Demo-Stats aktualisieren.""" response = client.post( "/api/state/demo/update-stats?teacher_id=demo-test&learning_units=5&exams_scheduled=3" ) assert response.status_code == 200 data = response.json() assert data["stats"]["learning_units_created"] == 5 assert data["stats"]["exams_scheduled"] == 3 def test_demo_reset(self): """Testet Demo-Reset.""" response = client.post("/api/state/demo/reset?teacher_id=demo-test") assert response.status_code == 200 data = response.json() assert data["success"] is True class TestPhaseTransitions: """Tests für Phasen-Übergänge.""" def test_manual_transition(self): """Testet manuellen Übergang.""" # Erst Kontext mit allen Voraussetzungen erstellen client.post( "/api/state/milestone?teacher_id=transition-test", json={"milestone": "school_select"} ) client.post( "/api/state/milestone?teacher_id=transition-test", json={"milestone": "consent_accept"} ) client.post( "/api/state/milestone?teacher_id=transition-test", json={"milestone": "profile_complete"} ) # Transition sollte automatisch erfolgt sein oder manuell möglich response = client.get("/api/state/phase?teacher_id=transition-test") assert response.status_code == 200 def test_invalid_transition(self): """Testet ungültigen Übergang.""" response = client.post( "/api/state/transition?teacher_id=invalid-test", json={"target_phase": "archived"} # Nicht erlaubt ) assert response.status_code == 400 def test_invalid_phase_name(self): """Testet ungültigen Phasen-Namen.""" response = client.post( "/api/state/transition?teacher_id=test", json={"target_phase": "invalid_phase"} ) assert response.status_code == 400