package robot import ( "errors" "testing" "github.com/google/uuid" "scrabble/backend/internal/engine" ) // TestComposeName covers the three rendering forms, including a Cyrillic initial. func TestComposeName(t *testing.T) { cases := []struct { first, surname string form int want string }{ {"Anna", "Carter", nameFormFirstOnly, "Anna"}, {"Anna", "Carter", nameFormInitial, "Anna C."}, {"Anna", "Carter", nameFormFull, "Anna Carter"}, {"Маша", "Суханова", nameFormInitial, "Маша С."}, {"Маша", "Суханова", nameFormFull, "Маша Суханова"}, } for _, c := range cases { if got := composeName(c.first, c.surname, c.form); got != c.want { t.Errorf("composeName(%q,%q,%d) = %q, want %q", c.first, c.surname, c.form, got, c.want) } } } // TestNamePoolsPaired checks the full and colloquial first-name pools line up by // index (so a slot's gender and person are consistent) and the surname forms differ. func TestNamePoolsPaired(t *testing.T) { if len(firstNamesFullEN) != robotPoolSize || len(firstNamesShortEN) != robotPoolSize { t.Fatalf("EN first-name pools must each hold %d names", robotPoolSize) } if len(firstNamesFullRU) != robotPoolSize || len(firstNamesShortRU) != robotPoolSize { t.Fatalf("RU first-name pools must each hold %d names", robotPoolSize) } for i := range firstNamesFullRU { if firstNamesFullRU[i].female != firstNamesShortRU[i].female { t.Errorf("RU pair %d disagrees on gender: %q vs %q", i, firstNamesFullRU[i].name, firstNamesShortRU[i].name) } } for _, sp := range surnamesRU { if sp.m == sp.f { t.Errorf("RU surname forms should differ: %q", sp.m) } } } // TestRobotDisplayNames checks the generated pools are the right size, non-empty and // deterministic — durable robot accounts must keep a stable name across restarts. func TestRobotDisplayNames(t *testing.T) { en1, en2 := robotDisplayNamesEN(), robotDisplayNamesEN() ru1, ru2 := robotDisplayNamesRU(), robotDisplayNamesRU() if len(en1) != robotPoolSize || len(ru1) != robotPoolSize { t.Fatalf("pool sizes en=%d ru=%d, want %d", len(en1), len(ru1), robotPoolSize) } for i := range en1 { if en1[i] != en2[i] || ru1[i] != ru2[i] { t.Fatalf("pool not deterministic at %d: en %q/%q ru %q/%q", i, en1[i], en2[i], ru1[i], ru2[i]) } if en1[i] == "" || ru1[i] == "" { t.Fatalf("empty composed name at index %d", i) } } } // TestPickVariantRouting checks English games draw the Latin pool and Russian games // draw mostly Russian names with a Latin minority. func TestPickVariantRouting(t *testing.T) { enID, ruID := uuid.New(), uuid.New() s := &Service{poolEN: []uuid.UUID{enID}, poolRU: []uuid.UUID{ruID}} for i := 0; i < 200; i++ { if got, err := s.Pick(engine.VariantEnglish); err != nil || got != enID { t.Fatalf("english Pick = (%v, %v), want (%v, nil)", got, err, enID) } } var en, ru int for i := 0; i < 4000; i++ { got, err := s.Pick(engine.VariantRussianScrabble) if err != nil { t.Fatalf("russian Pick: %v", err) } switch got { case enID: en++ case ruID: ru++ } } if ru <= en { t.Errorf("russian names should dominate a Russian game: ru=%d en=%d", ru, en) } if en == 0 { t.Errorf("some Latin names should appear in Russian games (got 0 of 4000)") } // Эрудит routes like Russian Scrabble. if _, err := s.Pick(engine.VariantErudit); err != nil { t.Errorf("erudit Pick: %v", err) } } // TestPickFallback checks an empty side falls back to the other pool and an empty pool // errors. func TestPickFallback(t *testing.T) { id := uuid.New() if got, err := (&Service{poolEN: []uuid.UUID{id}}).Pick(engine.VariantRussianScrabble); err != nil || got != id { t.Errorf("russian fallback to EN = (%v, %v), want (%v, nil)", got, err, id) } if got, err := (&Service{poolRU: []uuid.UUID{id}}).Pick(engine.VariantEnglish); err != nil || got != id { t.Errorf("english fallback to RU = (%v, %v), want (%v, nil)", got, err, id) } if _, err := (&Service{}).Pick(engine.VariantEnglish); !errors.Is(err, ErrNoRobotAvailable) { t.Errorf("empty pool err = %v, want ErrNoRobotAvailable", err) } }