//go:build integration package inttest import ( "context" "testing" "time" "github.com/google/uuid" "scrabble/backend/internal/account" "scrabble/backend/internal/game" ) // TestMoveDurationAnalytics seeds a game with crafted move timestamps and checks the // admin-console move-duration reports compute the think time (gap to the previous // move, the first move measured from game creation) correctly, per account and per // the account's move ordinal. func TestMoveDurationAnalytics(t *testing.T) { ctx := context.Background() accounts := account.NewStore(testDB) a, err := accounts.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString()) if err != nil { t.Fatalf("provision A: %v", err) } b, err := accounts.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString()) if err != nil { t.Fatalf("provision B: %v", err) } gid := uuid.New() t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) if _, err := testDB.ExecContext(ctx, `INSERT INTO backend.games (game_id, variant, dict_version, seed, players, turn_timeout_secs, created_at) VALUES ($1,'scrabble_en','v1',1,2,86400,$2)`, gid, t0); err != nil { t.Fatalf("insert game: %v", err) } if _, err := testDB.ExecContext(ctx, `INSERT INTO backend.game_players (game_id, seat, account_id) VALUES ($1,0,$2),($1,1,$3)`, gid, a.ID, b.ID); err != nil { t.Fatalf("insert seats: %v", err) } // seq, seat, commit time as seconds from t0. Durations: A:60,50 B:120,200. moves := []struct{ seq, seat, at int }{{0, 0, 60}, {1, 1, 180}, {2, 0, 230}, {3, 1, 430}} for _, m := range moves { if _, err := testDB.ExecContext(ctx, `INSERT INTO backend.game_moves (game_id, seq, seat, action, created_at) VALUES ($1,$2,$3,'play',$4)`, gid, m.seq, m.seat, t0.Add(time.Duration(m.at)*time.Second)); err != nil { t.Fatalf("insert move %d: %v", m.seq, err) } } store := game.NewStore(testDB) stats, err := store.MoveDurationStats(ctx, []uuid.UUID{a.ID, b.ID}) if err != nil { t.Fatalf("stats: %v", err) } if sa := stats[a.ID]; sa.Moves != 2 || sa.MinSecs != 50 || sa.MaxSecs != 60 || sa.AvgSecs != 55 { t.Errorf("A stats = %+v, want min50 max60 avg55 moves2", sa) } if sb := stats[b.ID]; sb.Moves != 2 || sb.MinSecs != 120 || sb.MaxSecs != 200 || sb.AvgSecs != 160 { t.Errorf("B stats = %+v, want min120 max200 avg160 moves2", sb) } byOrd, err := store.MoveDurationByOrdinal(ctx, a.ID) if err != nil { t.Fatalf("by ordinal: %v", err) } want := []game.OrdinalDuration{ {Ordinal: 1, MinSecs: 60, MaxSecs: 60, AvgSecs: 60}, {Ordinal: 2, MinSecs: 50, MaxSecs: 50, AvgSecs: 50}, } if len(byOrd) != len(want) { t.Fatalf("by ordinal = %+v, want %+v", byOrd, want) } for i, w := range want { if byOrd[i] != w { t.Errorf("ordinal[%d] = %+v, want %+v", i, byOrd[i], w) } } }