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 }