package retrystartgame_test import ( "context" "io" "log/slog" "testing" "time" "galaxy/lobby/internal/adapters/gameinmem" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/domain/game" "galaxy/lobby/internal/service/retrystartgame" "galaxy/lobby/internal/service/shared" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func fixedClock(at time.Time) func() time.Time { return func() time.Time { return at } } func newFailedGame(t *testing.T, gameType game.GameType, ownerID string) (game.Game, time.Time) { t.Helper() now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC) record, err := game.New(game.NewGameInput{ GameID: common.GameID("game-r"), GameName: "test retry game", GameType: gameType, OwnerUserID: ownerID, MinPlayers: 4, MaxPlayers: 8, StartGapHours: 12, StartGapPlayers: 2, EnrollmentEndsAt: now.Add(24 * time.Hour), TurnSchedule: "0 18 * * *", TargetEngineVersion: "v1.0.0", Now: now, }) require.NoError(t, err) record.Status = game.StatusStartFailed return record, now } func newService(t *testing.T, games *gameinmem.Store, at time.Time) *retrystartgame.Service { t.Helper() service, err := retrystartgame.NewService(retrystartgame.Dependencies{ Games: games, Clock: fixedClock(at), Logger: silentLogger(), }) require.NoError(t, err) return service } func TestNewServiceRejectsMissingDeps(t *testing.T) { _, err := retrystartgame.NewService(retrystartgame.Dependencies{}) require.Error(t, err) } func TestRetryStartGameAdminHappyPath(t *testing.T) { record, now := newFailedGame(t, game.GameTypePublic, "") games := gameinmem.NewStore() require.NoError(t, games.Save(context.Background(), record)) service := newService(t, games, now.Add(time.Hour)) updated, err := service.Handle(context.Background(), retrystartgame.Input{ Actor: shared.NewAdminActor(), GameID: record.GameID, }) require.NoError(t, err) assert.Equal(t, game.StatusReadyToStart, updated.Status) } func TestRetryStartGamePrivateOwnerHappyPath(t *testing.T) { record, now := newFailedGame(t, game.GameTypePrivate, "user-owner") games := gameinmem.NewStore() require.NoError(t, games.Save(context.Background(), record)) service := newService(t, games, now.Add(time.Hour)) updated, err := service.Handle(context.Background(), retrystartgame.Input{ Actor: shared.NewUserActor("user-owner"), GameID: record.GameID, }) require.NoError(t, err) assert.Equal(t, game.StatusReadyToStart, updated.Status) } func TestRetryStartGameRejectsNonOwnerUser(t *testing.T) { record, now := newFailedGame(t, game.GameTypePrivate, "user-owner") games := gameinmem.NewStore() require.NoError(t, games.Save(context.Background(), record)) service := newService(t, games, now.Add(time.Hour)) _, err := service.Handle(context.Background(), retrystartgame.Input{ Actor: shared.NewUserActor("user-other"), GameID: record.GameID, }) require.ErrorIs(t, err, shared.ErrForbidden) } func TestRetryStartGameRejectsWrongStatus(t *testing.T) { record, now := newFailedGame(t, game.GameTypePublic, "") record.Status = game.StatusRunning startedAt := now.Add(30 * time.Minute) record.StartedAt = &startedAt games := gameinmem.NewStore() require.NoError(t, games.Save(context.Background(), record)) service := newService(t, games, now.Add(time.Hour)) _, err := service.Handle(context.Background(), retrystartgame.Input{ Actor: shared.NewAdminActor(), GameID: record.GameID, }) require.ErrorIs(t, err, game.ErrConflict) } func TestRetryStartGameRejectsMissingRecord(t *testing.T) { games := gameinmem.NewStore() service := newService(t, games, time.Now().UTC()) _, err := service.Handle(context.Background(), retrystartgame.Input{ Actor: shared.NewAdminActor(), GameID: common.GameID("game-missing"), }) require.ErrorIs(t, err, game.ErrNotFound) }