package services

import (
	"testing"
	"time"

	"github.com/breakpilot/consent-service/internal/models"

	"github.com/google/uuid"
)

// TestValidateGrade tests grade validation
func TestValidateGrade(t *testing.T) {
	schoolYearID := uuid.New()
	gradeScaleID := uuid.New()

	tests := []struct {
		name        string
		grade       models.Grade
		expectValid bool
	}{
		{
			name: "valid grade 1",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        1.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: true,
		},
		{
			name: "valid grade 6",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        6.0,
				Type:         models.GradeTypeOral,
				Weight:       0.5,
				Date:         time.Now(),
				Semester:     2,
			},
			expectValid: true,
		},
		{
			name: "valid grade with plus (1.3)",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        1.3,
				Type:         models.GradeTypeTest,
				Weight:       0.25,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: true,
		},
		{
			name: "invalid grade 0",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        0.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: false,
		},
		{
			name: "invalid grade 7",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        7.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: false,
		},
		{
			name: "missing student ID",
			grade: models.Grade{
				StudentID:    uuid.Nil,
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        2.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: false,
		},
		{
			name: "invalid weight negative",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        2.0,
				Type:         models.GradeTypeExam,
				Weight:       -0.5,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: false,
		},
		{
			name: "invalid semester 0",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        2.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     0,
			},
			expectValid: false,
		},
		{
			name: "invalid semester 3",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        2.0,
				Type:         models.GradeTypeExam,
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     3,
			},
			expectValid: false,
		},
		{
			name: "invalid type",
			grade: models.Grade{
				StudentID:    uuid.New(),
				SubjectID:    uuid.New(),
				TeacherID:    uuid.New(),
				SchoolYearID: schoolYearID,
				GradeScaleID: gradeScaleID,
				Value:        2.0,
				Type:         "invalid_type",
				Weight:       1.0,
				Date:         time.Now(),
				Semester:     1,
			},
			expectValid: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			isValid := validateGrade(tt.grade)
			if isValid != tt.expectValid {
				t.Errorf("expected valid=%v, got valid=%v", tt.expectValid, isValid)
			}
		})
	}
}

// validateGrade validates a grade
func validateGrade(grade models.Grade) bool {
	if grade.StudentID == uuid.Nil {
		return false
	}
	if grade.SubjectID == uuid.Nil {
		return false
	}
	if grade.TeacherID == uuid.Nil {
		return false
	}
	// German grading scale: 1 (best) to 6 (worst)
	if grade.Value < 1.0 || grade.Value > 6.0 {
		return false
	}
	if grade.Weight < 0 {
		return false
	}
	if grade.Semester < 1 || grade.Semester > 2 {
		return false
	}

	// Validate type
	validTypes := map[string]bool{
		models.GradeTypeExam:          true,
		models.GradeTypeTest:          true,
		models.GradeTypeOral:          true,
		models.GradeTypeHomework:      true,
		models.GradeTypeProject:       true,
		models.GradeTypeParticipation: true,
		models.GradeTypeSemester:      true,
		models.GradeTypeFinal:         true,
	}

	if !validTypes[grade.Type] {
		return false
	}

	return true
}

// TestCalculateWeightedAverage tests weighted average calculation
func TestCalculateWeightedAverage(t *testing.T) {
	tests := []struct {
		name            string
		grades          []models.Grade
		expectedAverage float64
	}{
		{
			name: "simple average equal weights",
			grades: []models.Grade{
				{Value: 1.0, Weight: 1.0},
				{Value: 2.0, Weight: 1.0},
				{Value: 3.0, Weight: 1.0},
			},
			expectedAverage: 2.0,
		},
		{
			name: "weighted average different weights",
			grades: []models.Grade{
				{Value: 1.0, Weight: 2.0}, // Exam counts double
				{Value: 3.0, Weight: 1.0},
			},
			// (1*2 + 3*1) / (2+1) = 5/3 = 1.67
			expectedAverage: 1.67,
		},
		{
			name: "single grade",
			grades: []models.Grade{
				{Value: 2.5, Weight: 1.0},
			},
			expectedAverage: 2.5,
		},
		{
			name:            "empty grades",
			grades:          []models.Grade{},
			expectedAverage: 0.0,
		},
		{
			name: "all same grades",
			grades: []models.Grade{
				{Value: 2.0, Weight: 1.0},
				{Value: 2.0, Weight: 1.0},
				{Value: 2.0, Weight: 1.0},
			},
			expectedAverage: 2.0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			avg := calculateWeightedAverage(tt.grades)
			// Allow small floating point differences
			if avg < tt.expectedAverage-0.01 || avg > tt.expectedAverage+0.01 {
				t.Errorf("expected average=%.2f, got average=%.2f", tt.expectedAverage, avg)
			}
		})
	}
}

// calculateWeightedAverage calculates weighted average of grades
func calculateWeightedAverage(grades []models.Grade) float64 {
	if len(grades) == 0 {
		return 0.0
	}

	var weightedSum float64
	var totalWeight float64

	for _, g := range grades {
		weightedSum += g.Value * g.Weight
		totalWeight += g.Weight
	}

	if totalWeight == 0 {
		return 0.0
	}

	avg := weightedSum / totalWeight
	// Round to 2 decimal places
	return float64(int(avg*100)) / 100
}

// TestGradeDistribution tests grade distribution calculation
func TestGradeDistribution(t *testing.T) {
	tests := []struct {
		name         string
		grades       []models.Grade
		expectedDist map[int]int
	}{
		{
			name: "varied distribution",
			grades: []models.Grade{
				{Value: 1.0}, {Value: 1.3},
				{Value: 2.0}, {Value: 2.0}, {Value: 2.7},
				{Value: 3.0}, {Value: 3.0}, {Value: 3.0},
				{Value: 4.0}, {Value: 4.3},
				{Value: 5.0},
			},
			expectedDist: map[int]int{
				1: 2, // 1.0, 1.3 (rounded: 1, 1)
				2: 2, // 2.0, 2.0 (rounded: 2, 2)
				3: 4, // 2.7, 3.0, 3.0, 3.0 (rounded: 3, 3, 3, 3)
				4: 2, // 4.0, 4.3 (rounded: 4, 4)
				5: 1, // 5.0 (rounded: 5)
				6: 0,
			},
		},
		{
			name:         "empty grades",
			grades:       []models.Grade{},
			expectedDist: map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0},
		},
		{
			name: "all same grade",
			grades: []models.Grade{
				{Value: 2.0},
				{Value: 2.0},
				{Value: 2.0},
			},
			expectedDist: map[int]int{1: 0, 2: 3, 3: 0, 4: 0, 5: 0, 6: 0},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			dist := calculateGradeDistribution(tt.grades)
			for grade, count := range tt.expectedDist {
				if dist[grade] != count {
					t.Errorf("grade %d: expected count=%d, got count=%d", grade, count, dist[grade])
				}
			}
		})
	}
}

// calculateGradeDistribution calculates how many grades fall into each category
func calculateGradeDistribution(grades []models.Grade) map[int]int {
	dist := map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0}

	for _, g := range grades {
		// Round to nearest integer for distribution
		roundedGrade := int(g.Value + 0.5)
		if roundedGrade < 1 {
			roundedGrade = 1
		}
		if roundedGrade > 6 {
			roundedGrade = 6
		}
		dist[roundedGrade]++
	}

	return dist
}

// TestGradePointConversion tests conversion between grades and points (Oberstufe)
func TestGradePointConversion(t *testing.T) {
	tests := []struct {
		name           string
		grade          float64
		expectedPoints int
	}{
		{"grade 1.0 = 15 points", 1.0, 15},
		{"grade 1.3 = 14 points", 1.3, 14},
		{"grade 1.7 = 13 points", 1.7, 13},
		{"grade 2.0 = 12 points", 2.0, 12},
		{"grade 2.3 = 11 points", 2.3, 11},
		{"grade 2.7 = 10 points", 2.7, 10},
		{"grade 3.0 = 9 points", 3.0, 9},
		{"grade 3.3 = 8 points", 3.3, 8},
		{"grade 3.7 = 7 points", 3.7, 7},
		{"grade 4.0 = 6 points", 4.0, 6},
		{"grade 4.3 = 5 points", 4.3, 5},
		{"grade 4.7 = 4 points", 4.7, 4},
		{"grade 5.0 = 3 points", 5.0, 3},
		{"grade 5.3 = 2 points", 5.3, 2},
		{"grade 5.7 = 1 point", 5.7, 1},
		{"grade 6.0 = 0 points", 6.0, 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			points := gradeToPoints(tt.grade)
			if points != tt.expectedPoints {
				t.Errorf("expected points=%d, got points=%d", tt.expectedPoints, points)
			}
		})
	}
}

// gradeToPoints converts German grade (1-6) to Oberstufe points (0-15)
func gradeToPoints(grade float64) int {
	// Mapping based on German school system
	if grade <= 1.0 {
		return 15
	} else if grade <= 1.3 {
		return 14
	} else if grade <= 1.7 {
		return 13
	} else if grade <= 2.0 {
		return 12
	} else if grade <= 2.3 {
		return 11
	} else if grade <= 2.7 {
		return 10
	} else if grade <= 3.0 {
		return 9
	} else if grade <= 3.3 {
		return 8
	} else if grade <= 3.7 {
		return 7
	} else if grade <= 4.0 {
		return 6
	} else if grade <= 4.3 {
		return 5
	} else if grade <= 4.7 {
		return 4
	} else if grade <= 5.0 {
		return 3
	} else if grade <= 5.3 {
		return 2
	} else if grade <= 5.7 {
		return 1
	}
	return 0
}

// TestFindBestAndWorstGrade tests finding best and worst grades
func TestFindBestAndWorstGrade(t *testing.T) {
	tests := []struct {
		name          string
		grades        []models.Grade
		expectedBest  float64
		expectedWorst float64
	}{
		{
			name: "varied grades",
			grades: []models.Grade{
				{Value: 2.0},
				{Value: 1.0},
				{Value: 3.0},
				{Value: 5.0},
				{Value: 2.0},
			},
			expectedBest:  1.0,
			expectedWorst: 5.0,
		},
		{
			name: "all same",
			grades: []models.Grade{
				{Value: 2.0},
				{Value: 2.0},
			},
			expectedBest:  2.0,
			expectedWorst: 2.0,
		},
		{
			name: "single grade",
			grades: []models.Grade{
				{Value: 3.0},
			},
			expectedBest:  3.0,
			expectedWorst: 3.0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			best, worst := findBestAndWorstGrade(tt.grades)
			if best != tt.expectedBest {
				t.Errorf("expected best=%.1f, got best=%.1f", tt.expectedBest, best)
			}
			if worst != tt.expectedWorst {
				t.Errorf("expected worst=%.1f, got worst=%.1f", tt.expectedWorst, worst)
			}
		})
	}
}

// findBestAndWorstGrade finds the best (lowest) and worst (highest) grade
func findBestAndWorstGrade(grades []models.Grade) (best, worst float64) {
	if len(grades) == 0 {
		return 0, 0
	}

	best = grades[0].Value
	worst = grades[0].Value

	for _, g := range grades[1:] {
		if g.Value < best {
			best = g.Value
		}
		if g.Value > worst {
			worst = g.Value
		}
	}

	return best, worst
}
