package services import ( "testing" "github.com/stretchr/testify/assert" ) func TestGradeService_CalculateFinalGrade(t *testing.T) { tests := []struct { name string writtenAvg float64 oralGrade float64 writtenWeight int oralWeight int expectedFinal float64 }{ { name: "standard weights 60/40 - same grades", writtenAvg: 2.0, oralGrade: 2.0, writtenWeight: 60, oralWeight: 40, expectedFinal: 2.0, }, { name: "standard weights 60/40 - different grades", writtenAvg: 2.0, oralGrade: 3.0, writtenWeight: 60, oralWeight: 40, expectedFinal: 2.4, }, { name: "equal weights 50/50", writtenAvg: 2.0, oralGrade: 4.0, writtenWeight: 50, oralWeight: 50, expectedFinal: 3.0, }, { name: "hauptfach weights 70/30", writtenAvg: 1.5, oralGrade: 2.5, writtenWeight: 70, oralWeight: 30, expectedFinal: 1.8, }, { name: "only written (100/0)", writtenAvg: 2.5, oralGrade: 0, writtenWeight: 100, oralWeight: 0, expectedFinal: 2.5, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { finalGrade := calculateFinalGradeWeighted(tt.writtenAvg, tt.oralGrade, tt.writtenWeight, tt.oralWeight) assert.InDelta(t, tt.expectedFinal, finalGrade, 0.01) }) } } func TestGradeService_ValidateOralGrade(t *testing.T) { tests := []struct { name string grade float64 wantErr bool }{ { name: "valid grade 1.0", grade: 1.0, wantErr: false, }, { name: "valid grade 2.5", grade: 2.5, wantErr: false, }, { name: "valid grade 6.0", grade: 6.0, wantErr: false, }, { name: "invalid grade - too low", grade: 0.5, wantErr: true, }, { name: "invalid grade - too high", grade: 6.5, wantErr: true, }, { name: "invalid grade - negative", grade: -1.0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateOralGrade(tt.grade) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestGradeService_ValidateWeights(t *testing.T) { tests := []struct { name string writtenWeight int oralWeight int wantErr bool }{ { name: "valid 60/40", writtenWeight: 60, oralWeight: 40, wantErr: false, }, { name: "valid 50/50", writtenWeight: 50, oralWeight: 50, wantErr: false, }, { name: "valid 100/0", writtenWeight: 100, oralWeight: 0, wantErr: false, }, { name: "invalid - sum not 100", writtenWeight: 60, oralWeight: 50, wantErr: true, }, { name: "invalid - negative weight", writtenWeight: -10, oralWeight: 110, wantErr: true, }, { name: "invalid - both zero", writtenWeight: 0, oralWeight: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateWeights(tt.writtenWeight, tt.oralWeight) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestGradeService_CalculateWrittenAverage(t *testing.T) { tests := []struct { name string grades []float64 expectedAvg float64 }{ { name: "single grade", grades: []float64{2.0}, expectedAvg: 2.0, }, { name: "two grades - same", grades: []float64{2.0, 2.0}, expectedAvg: 2.0, }, { name: "two grades - different", grades: []float64{1.0, 3.0}, expectedAvg: 2.0, }, { name: "multiple grades", grades: []float64{1.0, 2.0, 3.0, 4.0}, expectedAvg: 2.5, }, { name: "empty grades", grades: []float64{}, expectedAvg: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { avg := calculateWrittenAverage(tt.grades) assert.InDelta(t, tt.expectedAvg, avg, 0.01) }) } } func TestGradeService_RoundGrade(t *testing.T) { tests := []struct { name string grade float64 expectedRound float64 }{ { name: "exact 2.0", grade: 2.0, expectedRound: 2.0, }, { name: "2.33 rounds to 2.3", grade: 2.33, expectedRound: 2.3, }, { name: "2.35 rounds to 2.4", grade: 2.35, expectedRound: 2.4, }, { name: "2.44 rounds to 2.4", grade: 2.44, expectedRound: 2.4, }, { name: "2.45 rounds to 2.5", grade: 2.45, expectedRound: 2.5, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rounded := roundGrade(tt.grade) assert.InDelta(t, tt.expectedRound, rounded, 0.01) }) } } func TestGradeService_GradeToOberstufenPoints(t *testing.T) { tests := []struct { name string grade float64 expectedPoints int }{ { name: "Grade 1.0 = 15 points", grade: 1.0, expectedPoints: 15, }, { name: "Grade 1.3 = 14 points", grade: 1.3, expectedPoints: 14, }, { name: "Grade 2.0 = 11 points", grade: 2.0, expectedPoints: 11, }, { name: "Grade 3.0 = 8 points", grade: 3.0, expectedPoints: 8, }, { name: "Grade 4.0 = 5 points", grade: 4.0, expectedPoints: 5, }, { name: "Grade 5.0 = 2 points", grade: 5.0, expectedPoints: 2, }, { name: "Grade 6.0 = 0 points", grade: 6.0, expectedPoints: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { points := gradeToOberstufenPoints(tt.grade) assert.Equal(t, tt.expectedPoints, points) }) } } func TestGradeService_OberstufenPointsToGrade(t *testing.T) { tests := []struct { name string points int expectedGrade float64 }{ { name: "15 points = Grade 1.0", points: 15, expectedGrade: 1.0, }, { name: "12 points = Grade 1.7", points: 12, expectedGrade: 1.7, }, { name: "10 points = Grade 2.3", points: 10, expectedGrade: 2.3, }, { name: "5 points = Grade 4.0", points: 5, expectedGrade: 4.0, }, { name: "0 points = Grade 6.0", points: 0, expectedGrade: 6.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { grade := oberstufenPointsToGrade(tt.points) assert.InDelta(t, tt.expectedGrade, grade, 0.1) }) } } // Helper functions for tests func calculateFinalGradeWeighted(writtenAvg, oralGrade float64, writtenWeight, oralWeight int) float64 { if writtenWeight+oralWeight == 0 { return 0 } return (writtenAvg*float64(writtenWeight) + oralGrade*float64(oralWeight)) / float64(writtenWeight+oralWeight) } func validateOralGrade(grade float64) error { if grade < 1.0 || grade > 6.0 { return assert.AnError } return nil } func validateWeights(writtenWeight, oralWeight int) error { if writtenWeight < 0 || oralWeight < 0 { return assert.AnError } if writtenWeight+oralWeight != 100 { return assert.AnError } return nil } func calculateWrittenAverage(grades []float64) float64 { if len(grades) == 0 { return 0 } sum := 0.0 for _, g := range grades { sum += g } return sum / float64(len(grades)) } func roundGrade(grade float64) float64 { return float64(int(grade*10+0.5)) / 10 } func gradeToOberstufenPoints(grade float64) int { // German grade to Oberstufen points conversion // 1.0 = 15, 1.3 = 14, 1.7 = 13, 2.0 = 11, etc. points := int(17 - (grade * 3)) if grade < 2.0 { points++ } if points > 15 { points = 15 } if points < 0 { points = 0 } return points } func oberstufenPointsToGrade(points int) float64 { // Oberstufen points to grade conversion if points >= 15 { return 1.0 } if points <= 0 { return 6.0 } return float64(17-points) / 3.0 } func TestGradeService_GradeApprovalWorkflow(t *testing.T) { tests := []struct { name string initialStatus string action string expectedStatus string wantErr bool }{ { name: "pending to approved", initialStatus: "pending", action: "approve", expectedStatus: "approved", wantErr: false, }, { name: "pending to locked", initialStatus: "pending", action: "lock", expectedStatus: "", wantErr: true, }, { name: "approved to locked", initialStatus: "approved", action: "lock", expectedStatus: "locked", wantErr: false, }, { name: "locked cannot be changed", initialStatus: "locked", action: "approve", expectedStatus: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { newStatus, err := processGradeAction(tt.initialStatus, tt.action) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedStatus, newStatus) } }) } } func processGradeAction(currentStatus, action string) (string, error) { transitions := map[string]map[string]string{ "pending": { "approve": "approved", }, "approved": { "lock": "locked", "reject": "pending", }, "locked": {}, } actions, exists := transitions[currentStatus] if !exists { return "", assert.AnError } newStatus, valid := actions[action] if !valid { return "", assert.AnError } return newStatus, nil }