package service import ( "context" "errors" "testing" "time" "datarush/internal/results/domain" "datarush/internal/results/repository" "github.com/google/uuid" ) type MockRepository struct { CreateFunc func(ctx context.Context, result *domain.Result) error GetFunc func(ctx context.Context, id uuid.UUID) (*domain.Result, error) UpdateFunc func(ctx context.Context, result *domain.Result) error DeleteFunc func(ctx context.Context, id uuid.UUID) error ListFunc func(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error) GetByCompetitionAndUserFunc func(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) GetLeaderboardFunc func(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) UpdateStatusFunc func(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) RecalculateRanksFunc func(ctx context.Context, competitionID uuid.UUID) error } func (m *MockRepository) Create(ctx context.Context, result *domain.Result) error { if m.CreateFunc != nil { return m.CreateFunc(ctx, result) } return nil } func (m *MockRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Result, error) { if m.GetFunc != nil { return m.GetFunc(ctx, id) } return nil, errors.New("not implemented") } func (m *MockRepository) Update(ctx context.Context, result *domain.Result) error { if m.UpdateFunc != nil { return m.UpdateFunc(ctx, result) } return nil } func (m *MockRepository) Delete(ctx context.Context, id uuid.UUID) error { if m.DeleteFunc != nil { return m.DeleteFunc(ctx, id) } return nil } func (m *MockRepository) List(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error) { if m.ListFunc != nil { return m.ListFunc(ctx, opts) } return nil, 0, errors.New("not implemented") } func (m *MockRepository) GetByCompetitionAndUser( ctx context.Context, competitionID uuid.UUID, userID uuid.UUID, ) (*domain.Result, error) { if m.GetByCompetitionAndUserFunc != nil { return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID) } return nil, errors.New("not implemented") } func (m *MockRepository) GetLeaderboard( ctx context.Context, competitionID uuid.UUID, limit int, offset int, ) ([]domain.Result, int, error) { if m.GetLeaderboardFunc != nil { return m.GetLeaderboardFunc(ctx, competitionID, limit, offset) } return nil, 0, errors.New("not implemented") } func (m *MockRepository) UpdateStatus( ctx context.Context, id uuid.UUID, status domain.ResultStatus, ) (*domain.Result, error) { if m.UpdateStatusFunc != nil { return m.UpdateStatusFunc(ctx, id, status) } return nil, errors.New("not implemented") } func (m *MockRepository) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error { if m.RecalculateRanksFunc != nil { return m.RecalculateRanksFunc(ctx, competitionID) } return nil } func TestNewService(t *testing.T) { repo := &MockRepository{} svc := NewService(repo) if svc == nil { t.Fatal("Expected service to be created, got nil") } if svc.repo == nil { t.Error("Expected repository to be set") } } func TestCreateResult(t *testing.T) { tests := []struct { name string input *domain.Result repoError error expectError bool }{ { name: "successful creation with new ID", input: &domain.Result{ CompetitionID: uuid.New(), UserID: uuid.New(), Score: 95.5, Status: domain.ResultStatusCompleted, }, repoError: nil, expectError: false, }, { name: "successful creation with existing ID", input: &domain.Result{ ID: uuid.New(), CompetitionID: uuid.New(), UserID: uuid.New(), Score: 95.5, Status: domain.ResultStatusCompleted, }, repoError: nil, expectError: false, }, { name: "repository error", input: &domain.Result{ CompetitionID: uuid.New(), UserID: uuid.New(), Score: 95.5, }, repoError: errors.New("database error"), expectError: true, }, { name: "empty status defaults to pending", input: &domain.Result{ CompetitionID: uuid.New(), UserID: uuid.New(), Score: 95.5, }, repoError: nil, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ CreateFunc: func(ctx context.Context, result *domain.Result) error { return tt.repoError }, } svc := NewService(repo) result, err := svc.CreateResult(context.Background(), tt.input) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if result.ID == uuid.Nil { t.Error("Expected ID to be set") } if result.CreatedAt.IsZero() { t.Error("Expected CreatedAt to be set") } if result.UpdatedAt.IsZero() { t.Error("Expected UpdatedAt to be set") } if tt.input.Status == "" && result.Status != domain.ResultStatusPending { t.Errorf("Expected status to default to PENDING, got %v", result.Status) } }) } } func TestGetResult(t *testing.T) { resultID := uuid.New() expectedResult := &domain.Result{ ID: resultID, CompetitionID: uuid.New(), UserID: uuid.New(), Score: 95.5, Status: domain.ResultStatusCompleted, CreatedAt: time.Now(), UpdatedAt: time.Now(), } tests := []struct { name string resultID uuid.UUID repoResult *domain.Result repoError error expectError bool }{ { name: "successful get", resultID: resultID, repoResult: expectedResult, repoError: nil, expectError: false, }, { name: "result not found", resultID: uuid.New(), repoResult: nil, repoError: domain.ErrResultNotFound, expectError: true, }, { name: "repository error", resultID: resultID, repoResult: nil, repoError: errors.New("database error"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ GetFunc: func(ctx context.Context, id uuid.UUID) (*domain.Result, error) { if id != tt.resultID { return nil, domain.ErrResultNotFound } return tt.repoResult, tt.repoError }, } svc := NewService(repo) result, err := svc.GetResult(context.Background(), tt.resultID) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if result.ID != expectedResult.ID { t.Errorf("Expected ID %v, got %v", expectedResult.ID, result.ID) } }) } } func TestUpdateResult(t *testing.T) { resultID := uuid.New() originalTime := time.Now().Add(-1 * time.Hour) tests := []struct { name string input *domain.Result repoError error expectError bool }{ { name: "successful update", input: &domain.Result{ ID: resultID, CompetitionID: uuid.New(), UserID: uuid.New(), Score: 100.0, Status: domain.ResultStatusCompleted, CreatedAt: originalTime, UpdatedAt: originalTime, }, repoError: nil, expectError: false, }, { name: "repository error", input: &domain.Result{ ID: resultID, CompetitionID: uuid.New(), UserID: uuid.New(), Score: 100.0, }, repoError: errors.New("update failed"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ UpdateFunc: func(ctx context.Context, result *domain.Result) error { return tt.repoError }, GetFunc: func(ctx context.Context, id uuid.UUID) (*domain.Result, error) { if tt.repoError != nil { return nil, tt.repoError } return tt.input, nil }, } svc := NewService(repo) beforeUpdate := time.Now() result, err := svc.UpdateResult(context.Background(), tt.input) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if !result.UpdatedAt.After(originalTime) { t.Error("Expected UpdatedAt to be updated") } if result.UpdatedAt.Before(beforeUpdate) { t.Error("Expected UpdatedAt to be recent") } }) } } func TestDeleteResult(t *testing.T) { resultID := uuid.New() tests := []struct { name string resultID uuid.UUID repoError error expectError bool }{ { name: "successful delete", resultID: resultID, repoError: nil, expectError: false, }, { name: "result not found", resultID: uuid.New(), repoError: domain.ErrResultNotFound, expectError: true, }, { name: "repository error", resultID: resultID, repoError: errors.New("delete failed"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ DeleteFunc: func(ctx context.Context, id uuid.UUID) error { return tt.repoError }, } svc := NewService(repo) err := svc.DeleteResult(context.Background(), tt.resultID) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } }) } } func TestListResults(t *testing.T) { competitionID := uuid.New() userID := uuid.New() status := domain.ResultStatusCompleted minScore := 50.0 maxScore := 100.0 results := []domain.Result{ { ID: uuid.New(), CompetitionID: competitionID, UserID: userID, Score: 95.0, Status: status, }, { ID: uuid.New(), CompetitionID: competitionID, UserID: uuid.New(), Score: 85.0, Status: status, }, } tests := []struct { name string pageSize int32 pageToken int32 competitionID *uuid.UUID userID *uuid.UUID status *domain.ResultStatus minScore *float64 maxScore *float64 repoResults []domain.Result repoTotal int repoError error expectError bool expectedTotal int32 expectedNext int32 }{ { name: "successful list with filters", pageSize: 10, pageToken: 0, competitionID: &competitionID, userID: &userID, status: &status, minScore: &minScore, maxScore: &maxScore, repoResults: results, repoTotal: 2, repoError: nil, expectError: false, expectedTotal: 2, expectedNext: 0, }, { name: "pagination with next page", pageSize: 10, pageToken: 0, competitionID: &competitionID, repoResults: results, repoTotal: 25, repoError: nil, expectError: false, expectedTotal: 25, expectedNext: 1, }, { name: "repository error", pageSize: 10, pageToken: 0, repoResults: nil, repoTotal: 0, repoError: errors.New("database error"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ ListFunc: func(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error) { return tt.repoResults, tt.repoTotal, tt.repoError }, } svc := NewService(repo) results, total, nextPage, err := svc.ListResults( context.Background(), tt.pageSize, tt.pageToken, tt.competitionID, tt.userID, tt.status, tt.minScore, tt.maxScore, ) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if len(results) != len(tt.repoResults) { t.Errorf("Expected %d results, got %d", len(tt.repoResults), len(results)) } if total != tt.expectedTotal { t.Errorf("Expected total %d, got %d", tt.expectedTotal, total) } if nextPage != tt.expectedNext { t.Errorf("Expected next page token %d, got %d", tt.expectedNext, nextPage) } }) } } func TestGetByCompetitionAndUser(t *testing.T) { competitionID := uuid.New() userID := uuid.New() expectedResult := &domain.Result{ ID: uuid.New(), CompetitionID: competitionID, UserID: userID, Score: 95.5, Status: domain.ResultStatusCompleted, } tests := []struct { name string competitionID uuid.UUID userID uuid.UUID repoResult *domain.Result repoError error expectError bool }{ { name: "successful get", competitionID: competitionID, userID: userID, repoResult: expectedResult, repoError: nil, expectError: false, }, { name: "result not found", competitionID: uuid.New(), userID: uuid.New(), repoResult: nil, repoError: domain.ErrResultNotFound, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ GetByCompetitionAndUserFunc: func(ctx context.Context, compID uuid.UUID, uID uuid.UUID) (*domain.Result, error) { return tt.repoResult, tt.repoError }, } svc := NewService(repo) result, err := svc.GetByCompetitionAndUser(context.Background(), tt.competitionID, tt.userID) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if result.ID != expectedResult.ID { t.Errorf("Expected ID %v, got %v", expectedResult.ID, result.ID) } }) } } func TestGetLeaderboard(t *testing.T) { competitionID := uuid.New() rank1, rank2, rank3 := 1, 2, 3 leaderboard := []domain.Result{ { ID: uuid.New(), CompetitionID: competitionID, UserID: uuid.New(), Score: 100.0, Rank: &rank1, Status: domain.ResultStatusCompleted, }, { ID: uuid.New(), CompetitionID: competitionID, UserID: uuid.New(), Score: 95.0, Rank: &rank2, Status: domain.ResultStatusCompleted, }, { ID: uuid.New(), CompetitionID: competitionID, UserID: uuid.New(), Score: 90.0, Rank: &rank3, Status: domain.ResultStatusCompleted, }, } tests := []struct { name string competitionID uuid.UUID limit int32 offset int32 repoResults []domain.Result repoTotal int repoError error expectError bool }{ { name: "successful leaderboard", competitionID: competitionID, limit: 10, offset: 0, repoResults: leaderboard, repoTotal: 3, repoError: nil, expectError: false, }, { name: "empty leaderboard", competitionID: uuid.New(), limit: 10, offset: 0, repoResults: []domain.Result{}, repoTotal: 0, repoError: nil, expectError: false, }, { name: "repository error", competitionID: competitionID, limit: 10, offset: 0, repoResults: nil, repoTotal: 0, repoError: errors.New("database error"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ GetLeaderboardFunc: func(ctx context.Context, compID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) { return tt.repoResults, tt.repoTotal, tt.repoError }, } svc := NewService(repo) results, total, err := svc.GetLeaderboard(context.Background(), tt.competitionID, tt.limit, tt.offset) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if len(results) != len(tt.repoResults) { t.Errorf("Expected %d results, got %d", len(tt.repoResults), len(results)) } if int32(total) != int32(tt.repoTotal) { t.Errorf("Expected total %d, got %d", tt.repoTotal, total) } for i := 0; i < len(results)-1; i++ { if results[i].Rank != nil && results[i+1].Rank != nil { if *results[i].Rank > *results[i+1].Rank { t.Error("Leaderboard should be sorted by rank ascending") } } } }) } } func TestUpdateStatus(t *testing.T) { resultID := uuid.New() updatedResult := &domain.Result{ ID: resultID, Status: domain.ResultStatusCompleted, UpdatedAt: time.Now(), } tests := []struct { name string resultID uuid.UUID status domain.ResultStatus repoResult *domain.Result repoError error expectError bool }{ { name: "successful status update", resultID: resultID, status: domain.ResultStatusCompleted, repoResult: updatedResult, repoError: nil, expectError: false, }, { name: "result not found", resultID: uuid.New(), status: domain.ResultStatusCompleted, repoResult: nil, repoError: domain.ErrResultNotFound, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ UpdateStatusFunc: func(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) { return tt.repoResult, tt.repoError }, } svc := NewService(repo) result, err := svc.UpdateStatus(context.Background(), tt.resultID, tt.status) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } if result.Status != tt.status { t.Errorf("Expected status %v, got %v", tt.status, result.Status) } }) } } func TestRecalculateRanks(t *testing.T) { competitionID := uuid.New() tests := []struct { name string competitionID uuid.UUID repoError error expectError bool }{ { name: "successful recalculation", competitionID: competitionID, repoError: nil, expectError: false, }, { name: "repository error", competitionID: competitionID, repoError: errors.New("recalculation failed"), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &MockRepository{ RecalculateRanksFunc: func(ctx context.Context, compID uuid.UUID) error { return tt.repoError }, } svc := NewService(repo) err := svc.RecalculateRanks(context.Background(), tt.competitionID) if tt.expectError { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Errorf("Expected no error, got %v", err) } }) } }