"""
Unit Tests for Unit Analytics API
Tests for learning gain, misconception tracking, and export endpoints
"""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from fastapi.testclient import TestClient
from datetime import datetime, timedelta
import uuid
import sys

sys.path.insert(0, '..')

from unit_analytics_api import (
    router,
    TimeRange,
    ExportFormat,
    LearningGainData,
    LearningGainSummary,
    StopPerformance,
    UnitPerformanceDetail,
    MisconceptionEntry,
    MisconceptionReport,
    StudentProgressTimeline,
    ClassComparisonData,
    calculate_gain_distribution,
    calculate_trend,
    calculate_difficulty_rating,
)
from fastapi import FastAPI

# Create test app
app = FastAPI()
app.include_router(router)
client = TestClient(app)


class TestHelperFunctions:
    """Test helper functions"""

    def test_calculate_gain_distribution_empty(self):
        """Test gain distribution with empty list"""
        result = calculate_gain_distribution([])
        assert all(v == 0 for v in result.values())

    def test_calculate_gain_distribution_positive_gains(self):
        """Test gain distribution with positive gains"""
        gains = [0.05, 0.15, 0.25, 0.35]  # 5%, 15%, 25%, 35%
        result = calculate_gain_distribution(gains)
        assert result["0% to 10%"] == 1
        assert result["10% to 20%"] == 1
        assert result["> 20%"] == 2

    def test_calculate_gain_distribution_negative_gains(self):
        """Test gain distribution with negative gains"""
        gains = [-0.25, -0.15, -0.05]  # -25%, -15%, -5%
        result = calculate_gain_distribution(gains)
        assert result["< -20%"] == 1
        assert result["-20% to -10%"] == 1
        assert result["-10% to 0%"] == 1

    def test_calculate_gain_distribution_mixed(self):
        """Test gain distribution with mixed gains"""
        gains = [-0.30, -0.05, 0.05, 0.15, 0.30]
        result = calculate_gain_distribution(gains)
        assert result["< -20%"] == 1
        assert result["-10% to 0%"] == 1
        assert result["0% to 10%"] == 1
        assert result["10% to 20%"] == 1
        assert result["> 20%"] == 1

    def test_calculate_trend_insufficient_data(self):
        """Test trend calculation with insufficient data"""
        result = calculate_trend([0.5, 0.6])
        assert result == "insufficient_data"

    def test_calculate_trend_improving(self):
        """Test trend calculation for improving scores"""
        scores = [0.4, 0.5, 0.6, 0.7, 0.8]
        result = calculate_trend(scores)
        assert result == "improving"

    def test_calculate_trend_declining(self):
        """Test trend calculation for declining scores"""
        scores = [0.8, 0.7, 0.6, 0.5, 0.4]
        result = calculate_trend(scores)
        assert result == "declining"

    def test_calculate_trend_stable(self):
        """Test trend calculation for stable scores"""
        scores = [0.5, 0.51, 0.49, 0.5, 0.5]
        result = calculate_trend(scores)
        assert result == "stable"

    def test_calculate_difficulty_rating_easy(self):
        """Test difficulty rating for easy stop"""
        rating = calculate_difficulty_rating(success_rate=0.95, avg_attempts=1.0)
        assert rating < 2.0

    def test_calculate_difficulty_rating_hard(self):
        """Test difficulty rating for hard stop"""
        rating = calculate_difficulty_rating(success_rate=0.3, avg_attempts=3.0)
        assert rating > 3.0

    def test_calculate_difficulty_rating_bounds(self):
        """Test difficulty rating stays within bounds"""
        # Very hard
        rating_hard = calculate_difficulty_rating(success_rate=0.0, avg_attempts=5.0)
        assert rating_hard <= 5.0

        # Very easy
        rating_easy = calculate_difficulty_rating(success_rate=1.0, avg_attempts=1.0)
        assert rating_easy >= 1.0


class TestLearningGainEndpoints:
    """Test learning gain analysis endpoints"""

    def test_get_learning_gain_empty(self):
        """Test learning gain with no data"""
        response = client.get("/api/analytics/learning-gain/test_unit")

        assert response.status_code == 200
        data = response.json()
        assert data["unit_id"] == "test_unit"
        assert data["total_students"] == 0
        assert data["avg_gain"] == 0.0

    def test_get_learning_gain_structure(self):
        """Test learning gain response structure"""
        response = client.get("/api/analytics/learning-gain/demo_unit_v1")

        assert response.status_code == 200
        data = response.json()
        assert "unit_id" in data
        assert "unit_title" in data
        assert "total_students" in data
        assert "avg_precheck" in data
        assert "avg_postcheck" in data
        assert "avg_gain" in data
        assert "median_gain" in data
        assert "std_deviation" in data
        assert "positive_gain_count" in data
        assert "negative_gain_count" in data
        assert "no_change_count" in data
        assert "gain_distribution" in data
        assert "individual_gains" in data

    def test_get_learning_gain_with_class_filter(self):
        """Test learning gain with class filter"""
        response = client.get(
            "/api/analytics/learning-gain/demo_unit_v1?class_id=class-5a"
        )

        assert response.status_code == 200
        data = response.json()
        assert "unit_id" in data

    def test_get_learning_gain_with_time_range(self):
        """Test learning gain with different time ranges"""
        for time_range in ["week", "month", "quarter", "all"]:
            response = client.get(
                f"/api/analytics/learning-gain/demo_unit_v1?time_range={time_range}"
            )
            assert response.status_code == 200

    def test_compare_learning_gains(self):
        """Test comparing learning gains across units"""
        response = client.get(
            "/api/analytics/learning-gain/compare?unit_ids=unit_a,unit_b,unit_c"
        )

        assert response.status_code == 200
        data = response.json()
        assert "comparisons" in data
        assert "time_range" in data
        assert isinstance(data["comparisons"], list)

    def test_compare_learning_gains_with_class(self):
        """Test comparing learning gains with class filter"""
        response = client.get(
            "/api/analytics/learning-gain/compare?unit_ids=unit_a,unit_b&class_id=class-5a"
        )

        assert response.status_code == 200
        data = response.json()
        assert data["class_id"] == "class-5a"


class TestStopAnalyticsEndpoints:
    """Test stop-level analytics endpoints"""

    def test_get_stop_analytics(self):
        """Test getting stop-level analytics"""
        response = client.get("/api/analytics/unit/demo_unit_v1/stops")

        assert response.status_code == 200
        data = response.json()
        assert "unit_id" in data
        assert "unit_title" in data
        assert "template" in data
        assert "total_sessions" in data
        assert "completed_sessions" in data
        assert "completion_rate" in data
        assert "avg_duration_minutes" in data
        assert "stops" in data
        assert "bottleneck_stops" in data
        assert isinstance(data["stops"], list)

    def test_get_stop_analytics_with_filters(self):
        """Test stop analytics with class and time range filters"""
        response = client.get(
            "/api/analytics/unit/demo_unit_v1/stops?class_id=class-5a&time_range=month"
        )

        assert response.status_code == 200


class TestMisconceptionEndpoints:
    """Test misconception tracking endpoints"""

    def test_get_misconception_report(self):
        """Test getting misconception report"""
        response = client.get("/api/analytics/misconceptions")

        assert response.status_code == 200
        data = response.json()
        assert "time_range" in data
        assert "total_misconceptions" in data
        assert "unique_concepts" in data
        assert "most_common" in data
        assert "by_unit" in data
        assert "trending_up" in data
        assert "resolved" in data

    def test_get_misconception_report_with_filters(self):
        """Test misconception report with filters"""
        response = client.get(
            "/api/analytics/misconceptions?class_id=class-5a&unit_id=demo_unit_v1&limit=10"
        )

        assert response.status_code == 200

    def test_get_misconception_report_limit(self):
        """Test misconception report respects limit"""
        response = client.get("/api/analytics/misconceptions?limit=5")

        assert response.status_code == 200
        data = response.json()
        assert len(data["most_common"]) <= 10  # capped at 10 in most_common

    def test_get_student_misconceptions(self):
        """Test getting misconceptions for specific student"""
        student_id = str(uuid.uuid4())
        response = client.get(f"/api/analytics/misconceptions/student/{student_id}")

        assert response.status_code == 200
        data = response.json()
        assert data["student_id"] == student_id
        assert "misconceptions" in data
        assert "recommended_remediation" in data

    def test_get_student_misconceptions_with_time_range(self):
        """Test student misconceptions with time range"""
        student_id = str(uuid.uuid4())
        response = client.get(
            f"/api/analytics/misconceptions/student/{student_id}?time_range=all"
        )

        assert response.status_code == 200


class TestStudentTimelineEndpoints:
    """Test student progress timeline endpoints"""

    def test_get_student_timeline(self):
        """Test getting student progress timeline"""
        student_id = str(uuid.uuid4())
        response = client.get(f"/api/analytics/student/{student_id}/timeline")

        assert response.status_code == 200
        data = response.json()
        assert data["student_id"] == student_id
        assert "student_name" in data
        assert "units_completed" in data
        assert "total_time_minutes" in data
        assert "avg_score" in data
        assert "trend" in data
        assert "timeline" in data
        assert isinstance(data["timeline"], list)

    def test_get_student_timeline_with_time_range(self):
        """Test student timeline with different time ranges"""
        student_id = str(uuid.uuid4())
        for time_range in ["week", "month", "quarter", "all"]:
            response = client.get(
                f"/api/analytics/student/{student_id}/timeline?time_range={time_range}"
            )
            assert response.status_code == 200


class TestClassComparisonEndpoints:
    """Test class comparison endpoints"""

    def test_compare_classes(self):
        """Test comparing multiple classes"""
        response = client.get(
            "/api/analytics/compare/classes?class_ids=class-5a,class-5b,class-6a"
        )

        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)

    def test_compare_classes_with_time_range(self):
        """Test class comparison with time range"""
        response = client.get(
            "/api/analytics/compare/classes?class_ids=class-5a,class-5b&time_range=quarter"
        )

        assert response.status_code == 200


class TestExportEndpoints:
    """Test export endpoints"""

    def test_export_learning_gains_json(self):
        """Test exporting learning gains as JSON"""
        response = client.get("/api/analytics/export/learning-gains?format=json")

        assert response.status_code == 200
        data = response.json()
        assert "export_date" in data
        assert "filters" in data
        assert "data" in data

    def test_export_learning_gains_csv(self):
        """Test exporting learning gains as CSV"""
        response = client.get("/api/analytics/export/learning-gains?format=csv")

        assert response.status_code == 200
        assert response.headers["content-type"] == "text/csv; charset=utf-8"
        assert "attachment" in response.headers.get("content-disposition", "")
        # CSV should have header row
        assert "student_id,unit_id,precheck,postcheck,gain" in response.text

    def test_export_learning_gains_with_filters(self):
        """Test export with filters"""
        response = client.get(
            "/api/analytics/export/learning-gains?unit_id=demo_unit_v1&class_id=class-5a&format=json"
        )

        assert response.status_code == 200
        data = response.json()
        assert data["filters"]["unit_id"] == "demo_unit_v1"
        assert data["filters"]["class_id"] == "class-5a"

    def test_export_misconceptions_json(self):
        """Test exporting misconceptions as JSON"""
        response = client.get("/api/analytics/export/misconceptions?format=json")

        assert response.status_code == 200
        data = response.json()
        assert "export_date" in data
        assert "data" in data

    def test_export_misconceptions_csv(self):
        """Test exporting misconceptions as CSV"""
        response = client.get("/api/analytics/export/misconceptions?format=csv")

        assert response.status_code == 200
        assert response.headers["content-type"] == "text/csv; charset=utf-8"
        # CSV should have header row
        assert "concept_id" in response.text

    def test_export_misconceptions_with_class(self):
        """Test export misconceptions filtered by class"""
        response = client.get(
            "/api/analytics/export/misconceptions?class_id=class-5a&format=json"
        )

        assert response.status_code == 200
        data = response.json()
        assert data["class_id"] == "class-5a"


class TestDashboardEndpoints:
    """Test dashboard overview endpoints"""

    def test_get_dashboard_overview(self):
        """Test getting analytics dashboard overview"""
        response = client.get("/api/analytics/dashboard/overview")

        assert response.status_code == 200
        data = response.json()
        assert "time_range" in data
        assert "total_sessions" in data
        assert "unique_students" in data
        assert "avg_completion_rate" in data
        assert "avg_learning_gain" in data
        assert "most_played_units" in data
        assert "struggling_concepts" in data
        assert "active_classes" in data

    def test_get_dashboard_overview_with_time_range(self):
        """Test dashboard overview with different time ranges"""
        for time_range in ["week", "month", "quarter", "all"]:
            response = client.get(
                f"/api/analytics/dashboard/overview?time_range={time_range}"
            )
            assert response.status_code == 200
            data = response.json()
            assert data["time_range"] == time_range


class TestHealthEndpoint:
    """Test health check endpoint"""

    def test_health_check(self):
        """Test health check endpoint"""
        response = client.get("/api/analytics/health")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert data["service"] == "unit-analytics"
        assert "database" in data


class TestPydanticModels:
    """Test Pydantic model validation"""

    def test_time_range_enum(self):
        """Test TimeRange enum values"""
        assert TimeRange.WEEK == "week"
        assert TimeRange.MONTH == "month"
        assert TimeRange.QUARTER == "quarter"
        assert TimeRange.ALL == "all"

    def test_export_format_enum(self):
        """Test ExportFormat enum values"""
        assert ExportFormat.JSON == "json"
        assert ExportFormat.CSV == "csv"

    def test_learning_gain_data_model(self):
        """Test LearningGainData model"""
        data = LearningGainData(
            student_id="test-student",
            student_name="Max Mustermann",
            unit_id="test_unit",
            precheck_score=0.5,
            postcheck_score=0.8,
            learning_gain=0.3,
        )
        assert data.student_id == "test-student"
        assert data.learning_gain == 0.3
        assert data.percentile is None

    def test_learning_gain_data_with_percentile(self):
        """Test LearningGainData model with percentile"""
        data = LearningGainData(
            student_id="test-student",
            student_name="Max",
            unit_id="test_unit",
            precheck_score=0.5,
            postcheck_score=0.8,
            learning_gain=0.3,
            percentile=75.0,
        )
        assert data.percentile == 75.0

    def test_stop_performance_model(self):
        """Test StopPerformance model"""
        stop = StopPerformance(
            stop_id="lens",
            stop_label="Linse",
            attempts_total=100,
            success_rate=0.85,
            avg_time_seconds=45.0,
            avg_attempts_before_success=1.2,
            common_errors=["wrong_direction", "timeout"],
            difficulty_rating=2.5,
        )
        assert stop.stop_id == "lens"
        assert stop.success_rate == 0.85
        assert len(stop.common_errors) == 2

    def test_misconception_entry_model(self):
        """Test MisconceptionEntry model"""
        entry = MisconceptionEntry(
            concept_id="pupil_focus",
            concept_label="Pupillenfokus",
            misconception_text="Die Pupille macht scharf",
            frequency=15,
            affected_student_ids=["s1", "s2", "s3"],
            unit_id="bio_eye_v1",
            stop_id="iris",
            detected_via="precheck",
            first_detected=datetime.utcnow(),
            last_detected=datetime.utcnow(),
        )
        assert entry.concept_id == "pupil_focus"
        assert entry.frequency == 15
        assert len(entry.affected_student_ids) == 3

    def test_student_progress_timeline_model(self):
        """Test StudentProgressTimeline model"""
        timeline = StudentProgressTimeline(
            student_id="test-student",
            student_name="Max Mustermann",
            units_completed=5,
            total_time_minutes=45,
            avg_score=0.78,
            trend="improving",
            timeline=[
                {"date": "2026-01-01", "unit_id": "unit_1", "score": 0.7},
                {"date": "2026-01-05", "unit_id": "unit_2", "score": 0.8},
            ],
        )
        assert timeline.units_completed == 5
        assert timeline.trend == "improving"
        assert len(timeline.timeline) == 2

    def test_class_comparison_data_model(self):
        """Test ClassComparisonData model"""
        data = ClassComparisonData(
            class_id="class-5a",
            class_name="Klasse 5a",
            student_count=25,
            units_assigned=10,
            avg_completion_rate=0.85,
            avg_learning_gain=0.15,
            avg_time_per_unit=8.5,
        )
        assert data.class_id == "class-5a"
        assert data.student_count == 25


class TestEdgeCases:
    """Test edge cases and error handling"""

    def test_learning_gain_nonexistent_unit(self):
        """Test learning gain for non-existent unit"""
        response = client.get(
            "/api/analytics/learning-gain/nonexistent_unit_xyz"
        )

        # Should return empty summary, not 404
        assert response.status_code == 200
        data = response.json()
        assert data["total_students"] == 0

    def test_compare_single_unit(self):
        """Test comparison with single unit"""
        response = client.get(
            "/api/analytics/learning-gain/compare?unit_ids=single_unit"
        )

        assert response.status_code == 200
        data = response.json()
        assert isinstance(data["comparisons"], list)

    def test_compare_empty_units(self):
        """Test comparison with empty unit list"""
        response = client.get(
            "/api/analytics/learning-gain/compare?unit_ids="
        )

        # Should handle gracefully
        assert response.status_code in [200, 422]

    def test_invalid_time_range(self):
        """Test with invalid time range"""
        response = client.get(
            "/api/analytics/learning-gain/demo_unit_v1?time_range=invalid"
        )

        # FastAPI should reject invalid enum value
        assert response.status_code == 422

    def test_invalid_export_format(self):
        """Test with invalid export format"""
        response = client.get(
            "/api/analytics/export/learning-gains?format=xml"
        )

        # FastAPI should reject invalid enum value
        assert response.status_code == 422

    def test_misconceptions_limit_bounds(self):
        """Test misconceptions limit parameter bounds"""
        # Too low
        response = client.get("/api/analytics/misconceptions?limit=0")
        assert response.status_code == 422

        # Too high
        response = client.get("/api/analytics/misconceptions?limit=200")
        assert response.status_code == 422

        # Valid bounds
        response = client.get("/api/analytics/misconceptions?limit=1")
        assert response.status_code == 200
        response = client.get("/api/analytics/misconceptions?limit=100")
        assert response.status_code == 200

    def test_compare_classes_empty(self):
        """Test class comparison with empty list"""
        response = client.get("/api/analytics/compare/classes?class_ids=")

        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)

    def test_student_timeline_new_student(self):
        """Test timeline for student with no history"""
        new_student_id = str(uuid.uuid4())
        response = client.get(f"/api/analytics/student/{new_student_id}/timeline")

        assert response.status_code == 200
        data = response.json()
        assert data["units_completed"] == 0
        assert data["trend"] == "insufficient_data"
        assert data["timeline"] == []

    def test_special_characters_in_ids(self):
        """Test handling of special characters in IDs"""
        # URL-encoded special characters - slashes in path params are problematic
        # because FastAPI/Starlette decodes them before routing
        response = client.get(
            "/api/analytics/learning-gain/unit%2Fwith%2Fslashes"
        )
        # Slashes in path params result in 404 as the decoded path doesn't match
        # This is expected behavior - use URL-safe IDs in practice
        assert response.status_code in [200, 404]

