package game import ( "strings" "testing" "time" "galaxy/lobby/internal/domain/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func validNewGameInput(now time.Time) NewGameInput { return NewGameInput{ GameID: common.GameID("game-42"), GameName: "Spring Classic", Description: "optional", GameType: GameTypePublic, OwnerUserID: "", MinPlayers: 4, MaxPlayers: 8, StartGapHours: 24, StartGapPlayers: 2, EnrollmentEndsAt: now.Add(7 * 24 * time.Hour), TurnSchedule: "0 18 * * *", TargetEngineVersion: "v1.2.3", Now: now, } } func TestNewGameSucceedsOnHappyPath(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) record, err := New(input) require.NoError(t, err) assert.Equal(t, StatusDraft, record.Status) assert.Equal(t, input.GameID, record.GameID) assert.Equal(t, now.UTC(), record.CreatedAt) assert.Equal(t, now.UTC(), record.UpdatedAt) assert.Nil(t, record.StartedAt) assert.Nil(t, record.FinishedAt) assert.Equal(t, 0, record.RuntimeSnapshot.CurrentTurn) assert.Empty(t, record.RuntimeSnapshot.RuntimeStatus) assert.Empty(t, record.RuntimeSnapshot.EngineHealthSummary) } func TestNewGameValidatesOwnerBinding(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) privateMissingOwner := validNewGameInput(now) privateMissingOwner.GameType = GameTypePrivate privateMissingOwner.OwnerUserID = "" _, err := New(privateMissingOwner) require.Error(t, err) assert.Contains(t, err.Error(), "owner user id must not be empty for private games") publicWithOwner := validNewGameInput(now) publicWithOwner.OwnerUserID = "user-1" _, err = New(publicWithOwner) require.Error(t, err) assert.Contains(t, err.Error(), "owner user id must be empty for public games") privateWithOwner := validNewGameInput(now) privateWithOwner.GameType = GameTypePrivate privateWithOwner.OwnerUserID = "user-1" _, err = New(privateWithOwner) require.NoError(t, err) } func TestNewGameRejectsInvalidSizing(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) cases := map[string]func(*NewGameInput){ "min_players_zero": func(i *NewGameInput) { i.MinPlayers = 0 }, "min_players_negative": func(i *NewGameInput) { i.MinPlayers = -1 }, "max_players_zero": func(i *NewGameInput) { i.MaxPlayers = 0 }, "max_less_than_min": func(i *NewGameInput) { i.MaxPlayers = i.MinPlayers - 1 }, "start_gap_hours_zero": func(i *NewGameInput) { i.StartGapHours = 0 }, "start_gap_players_zero": func(i *NewGameInput) { i.StartGapPlayers = 0 }, } for name, mutate := range cases { t.Run(name, func(t *testing.T) { input := validNewGameInput(now) mutate(&input) _, err := New(input) require.Error(t, err) }) } } func TestNewGameRejectsInvalidEnrollmentDeadline(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) past := validNewGameInput(now) past.EnrollmentEndsAt = now.Add(-time.Hour) _, err := New(past) require.Error(t, err) zero := validNewGameInput(now) zero.EnrollmentEndsAt = time.Time{} _, err = New(zero) require.Error(t, err) } func TestNewGameRejectsInvalidTurnSchedule(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) input.TurnSchedule = "not a cron" _, err := New(input) require.Error(t, err) assert.Contains(t, err.Error(), "turn schedule") } func TestNewGameRejectsInvalidEngineVersion(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) input.TargetEngineVersion = "not-semver" _, err := New(input) require.Error(t, err) assert.Contains(t, err.Error(), "target engine version") input = validNewGameInput(now) input.TargetEngineVersion = "" _, err = New(input) require.Error(t, err) assert.Contains(t, err.Error(), "target engine version") } func TestNewGameRejectsEmptyName(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) input.GameName = " " _, err := New(input) require.Error(t, err) assert.Contains(t, err.Error(), "game name must not be empty") } func TestNewGameRejectsInvalidGameID(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) input.GameID = common.GameID("bogus") _, err := New(input) require.Error(t, err) assert.Contains(t, err.Error(), "game id") } func TestGameValidateAcceptsCanonicalSemverWithoutPrefix(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) input.TargetEngineVersion = "2.0.0" record, err := New(input) require.NoError(t, err) assert.Equal(t, "2.0.0", record.TargetEngineVersion) } func TestGameValidateRuntimeBinding(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) record, err := New(input) require.NoError(t, err) bound := now.Add(time.Minute) record.RuntimeBinding = &RuntimeBinding{ ContainerID: "container-1", EngineEndpoint: "engine.local:9000", RuntimeJobID: "1700000000000-0", BoundAt: bound, } require.NoError(t, record.Validate()) cases := map[string]func(binding *RuntimeBinding){ "empty_container_id": func(b *RuntimeBinding) { b.ContainerID = "" }, "empty_engine_endpoint": func(b *RuntimeBinding) { b.EngineEndpoint = "" }, "empty_runtime_job_id": func(b *RuntimeBinding) { b.RuntimeJobID = "" }, "zero_bound_at": func(b *RuntimeBinding) { b.BoundAt = time.Time{} }, } for name, mutate := range cases { t.Run(name, func(t *testing.T) { input := validNewGameInput(now) rec, err := New(input) require.NoError(t, err) binding := RuntimeBinding{ ContainerID: "container-1", EngineEndpoint: "engine.local:9000", RuntimeJobID: "1700000000000-0", BoundAt: bound, } mutate(&binding) rec.RuntimeBinding = &binding require.Error(t, rec.Validate()) }) } beforeCreated := validNewGameInput(now) rec, err := New(beforeCreated) require.NoError(t, err) earlier := now.Add(-time.Hour) rec.RuntimeBinding = &RuntimeBinding{ ContainerID: "container-1", EngineEndpoint: "engine.local:9000", RuntimeJobID: "1700000000000-0", BoundAt: earlier, } err = rec.Validate() require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), "runtime binding bound at must not be before created at") } func TestGameValidateDetectsStartedBeforeCreated(t *testing.T) { now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC) input := validNewGameInput(now) record, err := New(input) require.NoError(t, err) earlier := now.Add(-time.Minute) record.StartedAt = &earlier err = record.Validate() require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), "started at") }