Ctrl K
ComunidadGuías…

internal/app/bots/stickersbot_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package bots

import (
	"context"
	"strings"
	"testing"

	"telesrv/internal/domain"
	"telesrv/internal/store/memory"
)

func sendTextToStickers(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User, text string) string {
	t.Helper()
	return sendMessageToStickers(t, svc, messages, owner, domain.Message{Body: text})
}

func sendDocumentToStickers(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User, doc domain.Document) string {
	t.Helper()
	return sendMessageToStickers(t, svc, messages, owner, domain.Message{
		Media: &domain.MessageMedia{
			Kind:     domain.MessageMediaKindDocument,
			Document: &doc,
		},
	})
}

func sendMessageToStickers(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User, msg domain.Message) string {
	t.Helper()
	msg.From = domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
	msg.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: domain.StickersBotUserID}
	svc.respondAsStickers(owner.ID, msg)
	return latestStickersReply(t, messages, owner.ID).Body
}

func latestStickersReply(t *testing.T, messages *memory.MessageStore, userID int64) domain.Message {
	t.Helper()
	list, err := messages.ListByUser(context.Background(), userID, domain.MessageFilter{
		HasPeer: true,
		Peer:    domain.Peer{Type: domain.PeerTypeUser, ID: domain.StickersBotUserID},
		Limit:   100,
	})
	if err != nil {
		t.Fatalf("list stickers history: %v", err)
	}
	var latest domain.Message
	for _, msg := range list.Messages {
		if msg.From.ID == domain.StickersBotUserID && msg.ID > latest.ID {
			latest = msg
		}
	}
	if latest.ID == 0 {
		t.Fatal("no Stickers reply")
	}
	return latest
}

func newStickersBotTestService(t *testing.T) (*Service, *memory.UserStore, *memory.BotStore, *memory.MessageStore, *stickersBotFakeCreator, *stickersBotFakeInstaller) {
	t.Helper()
	users := memory.NewUserStore()
	bots := memory.NewBotStore(users)
	dialogs := memory.NewDialogStore()
	messages := memory.NewMessageStore(dialogs)
	creator := &stickersBotFakeCreator{}
	installer := &stickersBotFakeInstaller{}
	svc := NewService(users, bots, messages, WithStickerSetCreator(creator), WithUserStickerSets(installer))
	return svc, users, bots, messages, creator, installer
}

func stickerBotTestDocument(id, accessHash int64, attr domain.DocumentAttributeKind) domain.Document {
	return domain.Document{
		ID:         id,
		AccessHash: accessHash,
		DCID:       2,
		MimeType:   "image/webp",
		Attributes: []domain.DocumentAttribute{{Kind: attr}},
	}
}

func stickerBotUploadDocument(id, accessHash int64, mimeType, fileName string) domain.Document {
	return domain.Document{
		ID:         id,
		AccessHash: accessHash,
		DCID:       2,
		MimeType:   mimeType,
		Size:       4096,
		Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: fileName}},
	}
}

func stickerBotSetDocument(id, accessHash, setID, setAccessHash int64) domain.Document {
	return domain.Document{
		ID:         id,
		AccessHash: accessHash,
		DCID:       2,
		MimeType:   "image/webp",
		Attributes: []domain.DocumentAttribute{{
			Kind:                 domain.DocAttrSticker,
			StickerSetID:         setID,
			StickerSetAccessHash: setAccessHash,
		}},
	}
}

func stickerBotSetCustomEmojiDocument(id, accessHash, setID, setAccessHash int64) domain.Document {
	return domain.Document{
		ID:         id,
		AccessHash: accessHash,
		DCID:       2,
		MimeType:   "application/x-tgsticker",
		Attributes: []domain.DocumentAttribute{{
			Kind:                 domain.DocAttrCustomEmoji,
			StickerSetID:         setID,
			StickerSetAccessHash: setAccessHash,
		}},
	}
}

func TestStickersBotSystemSeedStartAndCancel(t *testing.T) {
	svc, users, bots, messages, _, _ := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3000")
	ctx := context.Background()

	if !svc.HandlesBot(domain.BotFatherUserID) || !svc.HandlesBot(domain.StickersBotUserID) {
		t.Fatal("service should handle BotFather and Stickers")
	}
	u, found, err := users.ByUsername(ctx, "Stickers")
	if err != nil || !found {
		t.Fatalf("@Stickers user not seeded: found=%v err=%v", found, err)
	}
	if u.ID != domain.StickersBotUserID || !u.Bot || u.BotInfoVersion < 1 {
		t.Fatalf("@Stickers user = %+v, want seeded bot", u)
	}
	profile, found, err := bots.GetBot(ctx, domain.StickersBotUserID)
	if err != nil || !found {
		t.Fatalf("@Stickers profile not seeded: found=%v err=%v", found, err)
	}
	if !botCommandExists(profile.Commands, "newpack") || !botCommandExists(profile.Commands, "newemoji") ||
		!botCommandExists(profile.Commands, "publish") || !botCommandExists(profile.Commands, "addsticker") ||
		!botCommandExists(profile.Commands, "delsticker") {
		t.Fatalf("@Stickers commands = %+v, want newpack/newemoji/publish/addsticker/delsticker", profile.Commands)
	}

	if reply := sendTextToStickers(t, svc, messages, owner, "/start"); !strings.Contains(reply, "/newpack") || !strings.Contains(reply, "/newemoji") || !strings.Contains(reply, "/addsticker") {
		t.Fatalf("/start reply = %q, want help text", reply)
	}
	startReply := latestStickersReply(t, messages, owner.ID)
	assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/newpack")
	assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/newemoji")
	assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/addsticker")
	sendTextToStickers(t, svc, messages, owner, "/newpack")
	if reply := sendTextToStickers(t, svc, messages, owner, "/cancel"); !strings.Contains(reply, "Cancelled") {
		t.Fatalf("/cancel reply = %q, want cancelled", reply)
	}
	if _, found, _ := bots.GetBotChatState(ctx, domain.StickersBotUserID, owner.ID); found {
		t.Fatal("stickers bot state still present after /cancel")
	}
}

func TestStickersBotNewPackNoMaterialAndInvalidEmoji(t *testing.T) {
	svc, users, _, messages, creator, _ := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3001")

	if reply := sendTextToStickers(t, svc, messages, owner, "/newpack"); !strings.Contains(reply, "sticker pack") {
		t.Fatalf("/newpack reply = %q, want sticker pack prompt", reply)
	}
	if reply := sendTextToStickers(t, svc, messages, owner, "My Pack"); !strings.Contains(reply, "Lottie JSON") {
		t.Fatalf("title reply = %q, want document prompt", reply)
	}
	if reply := sendTextToStickers(t, svc, messages, owner, "/publish"); !strings.Contains(reply, "Add at least one") {
		t.Fatalf("empty publish reply = %q, want no material notice", reply)
	}
	if reply := sendTextToStickers(t, svc, messages, owner, "not a document"); !strings.Contains(reply, "WebM/MP4 must include video metadata") {
		t.Fatalf("text in document step reply = %q, want material prompt", reply)
	}
	if reply := sendDocumentToStickers(t, svc, messages, owner, stickerBotTestDocument(101, 1101, domain.DocAttrSticker)); !strings.Contains(reply, "emoji") {
		t.Fatalf("document reply = %q, want emoji prompt", reply)
	}
	if reply := sendTextToStickers(t, svc, messages, owner, "notemoji"); !strings.Contains(reply, "valid emoji") {
		t.Fatalf("invalid emoji reply = %q, want emoji validation", reply)
	}
	if len(creator.created) != 0 {
		t.Fatalf("creator called before valid publish: %+v", creator.created)
	}
}

func TestStickersBotPublishStickerPack(t *testing.T) {
	svc, users, bots, messages, creator, installer := newStickersBotTestService(t)
	hooks := &stickersBotHookRecorder{}
	svc.SetRouterHooks(hooks)
	owner := newOwner(t, users, "+3002")

	sendTextToStickers(t, svc, messages, owner, "/newpack")
	sendTextToStickers(t, svc, messages, owner, "Fresh Pack")
	sendDocumentToStickers(t, svc, messages, owner, stickerBotTestDocument(201, 2201, domain.DocAttrSticker))
	sendTextToStickers(t, svc, messages, owner, "🙂")
	sendTextToStickers(t, svc, messages, owner, "/publish")
	reply := sendTextToStickers(t, svc, messages, owner, "fresh_pack")

	if !strings.Contains(reply, "https://telesrv.net/addstickers/fresh_pack") {
		t.Fatalf("publish reply = %q, want addstickers link", reply)
	}
	publishReply := latestStickersReply(t, messages, owner.ID)
	assertReplyEntityText(t, publishReply, domain.MessageEntityURL, "https://telesrv.net/addstickers/fresh_pack")
	if len(creator.created) != 1 {
		t.Fatalf("created requests = %d, want 1", len(creator.created))
	}
	req := creator.created[0]
	if req.CreatorUserID != owner.ID || req.Title != "Fresh Pack" || req.ShortName != "fresh_pack" || req.Kind != domain.StickerSetKindStickers {
		t.Fatalf("create request = %+v, want owner/title/short/kind", req)
	}
	if len(req.Items) != 1 || req.Items[0].DocumentID != 201 || req.Items[0].DocumentAccessHash != 2201 || req.Items[0].Emoji != "🙂" {
		t.Fatalf("create items = %+v, want doc+emoji", req.Items)
	}
	if len(installer.installs) != 1 || installer.installs[0].userID != owner.ID || installer.installs[0].setID != creator.sets[0].ID {
		t.Fatalf("installs = %+v, want creator install", installer.installs)
	}
	if hooks.userID != owner.ID || hooks.kind != domain.StickerSetKindStickers {
		t.Fatalf("sticker update hook = user %d kind %q, want creator stickers", hooks.userID, hooks.kind)
	}
	if _, found, _ := bots.GetBotChatState(context.Background(), domain.StickersBotUserID, owner.ID); found {
		t.Fatal("stickers bot state still present after publish")
	}
}

func TestStickersBotPublishUploadedTGS(t *testing.T) {
	svc, users, _, messages, creator, installer := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3004")

	sendTextToStickers(t, svc, messages, owner, "/newemoji")
	sendTextToStickers(t, svc, messages, owner, "Local Emoji")
	reply := sendDocumentToStickers(t, svc, messages, owner, stickerBotUploadDocument(401, 4401, "application/octet-stream", "wave.tgs"))
	if !strings.Contains(reply, "emoji") {
		t.Fatalf("uploaded tgs reply = %q, want emoji prompt", reply)
	}
	sendTextToStickers(t, svc, messages, owner, "👋")
	sendTextToStickers(t, svc, messages, owner, "/publish")
	reply = sendTextToStickers(t, svc, messages, owner, "local_emoji")

	if !strings.Contains(reply, "https://telesrv.net/addemoji/local_emoji") {
		t.Fatalf("publish uploaded tgs reply = %q, want addemoji link", reply)
	}
	if len(creator.created) != 1 {
		t.Fatalf("created requests = %d, want 1", len(creator.created))
	}
	req := creator.created[0]
	if req.Kind != domain.StickerSetKindEmoji || len(req.Items) != 1 || req.Items[0].DocumentID != 401 || req.Items[0].DocumentAccessHash != 4401 {
		t.Fatalf("create request = %+v, want uploaded tgs item in emoji pack", req)
	}
	if len(installer.installs) != 1 || installer.installs[0].kind != domain.StickerSetKindEmoji {
		t.Fatalf("installs = %+v, want emoji install", installer.installs)
	}
}

func TestStickersBotPublishUploadedLottieJSON(t *testing.T) {
	svc, users, _, messages, creator, installer := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3005")

	sendTextToStickers(t, svc, messages, owner, "/newpack")
	sendTextToStickers(t, svc, messages, owner, "Lottie Pack")
	reply := sendDocumentToStickers(t, svc, messages, owner, stickerBotUploadDocument(402, 4402, "application/json", "wave.json"))
	if !strings.Contains(reply, "emoji") {
		t.Fatalf("uploaded lottie reply = %q, want emoji prompt", reply)
	}
	sendTextToStickers(t, svc, messages, owner, "👋")
	sendTextToStickers(t, svc, messages, owner, "/publish")
	reply = sendTextToStickers(t, svc, messages, owner, "lottie_pack")

	if !strings.Contains(reply, "https://telesrv.net/addstickers/lottie_pack") {
		t.Fatalf("publish uploaded lottie reply = %q, want addstickers link", reply)
	}
	if len(creator.created) != 1 {
		t.Fatalf("created requests = %d, want 1", len(creator.created))
	}
	req := creator.created[0]
	if req.Kind != domain.StickerSetKindStickers || len(req.Items) != 1 || req.Items[0].DocumentID != 402 || req.Items[0].DocumentAccessHash != 4402 {
		t.Fatalf("create request = %+v, want uploaded lottie item in sticker pack", req)
	}
	if len(installer.installs) != 1 || installer.installs[0].kind != domain.StickerSetKindStickers {
		t.Fatalf("installs = %+v, want sticker install", installer.installs)
	}
}

func TestStickersBotPublishCustomEmojiPack(t *testing.T) {
	svc, users, _, messages, creator, installer := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3003")

	sendTextToStickers(t, svc, messages, owner, "/newemoji")
	sendTextToStickers(t, svc, messages, owner, "Emoji Pack")
	sendDocumentToStickers(t, svc, messages, owner, stickerBotTestDocument(301, 3301, domain.DocAttrCustomEmoji))
	sendTextToStickers(t, svc, messages, owner, "🔥")
	sendTextToStickers(t, svc, messages, owner, "/publish")
	reply := sendTextToStickers(t, svc, messages, owner, "emoji_pack")

	if !strings.Contains(reply, "https://telesrv.net/addemoji/emoji_pack") {
		t.Fatalf("publish emoji reply = %q, want addemoji link", reply)
	}
	if len(creator.created) != 1 || creator.created[0].Kind != domain.StickerSetKindEmoji {
		t.Fatalf("created emoji requests = %+v, want kind emoji", creator.created)
	}
	if len(installer.installs) != 1 || installer.installs[0].kind != domain.StickerSetKindEmoji {
		t.Fatalf("emoji installs = %+v, want emoji kind install", installer.installs)
	}
}

func TestStickersBotAddStickerToExistingPack(t *testing.T) {
	svc, users, bots, messages, manager, _ := newStickersBotTestService(t)
	hooks := &stickersBotHookRecorder{}
	svc.SetRouterHooks(hooks)
	owner := newOwner(t, users, "+3006")
	manager.sets = append(manager.sets, domain.StickerSet{
		ID:            7100,
		AccessHash:    8100,
		ShortName:     "fresh_pack",
		Title:         "Fresh Pack",
		Kind:          domain.StickerSetKindStickers,
		Creator:       true,
		CreatorUserID: owner.ID,
		Count:         1,
		DocumentIDs:   []int64{501},
	})

	if reply := sendTextToStickers(t, svc, messages, owner, "/addsticker"); !strings.Contains(reply, "short name") {
		t.Fatalf("/addsticker reply = %q, want short name prompt", reply)
	}
	if reply := sendTextToStickers(t, svc, messages, owner, "https://telesrv.net/addstickers/fresh_pack"); !strings.Contains(reply, "Selected Fresh Pack") {
		t.Fatalf("select pack reply = %q, want selected pack", reply)
	}
	if reply := sendDocumentToStickers(t, svc, messages, owner, stickerBotUploadDocument(502, 5502, "application/json", "new.json")); !strings.Contains(reply, "emoji") {
		t.Fatalf("add document reply = %q, want emoji prompt", reply)
	}
	reply := sendTextToStickers(t, svc, messages, owner, "😄")
	if !strings.Contains(reply, "Done. Added to Fresh Pack") || !strings.Contains(reply, "https://telesrv.net/addstickers/fresh_pack") {
		t.Fatalf("add final reply = %q, want done link", reply)
	}
	if len(manager.adds) != 1 {
		t.Fatalf("adds = %+v, want one add", manager.adds)
	}
	add := manager.adds[0]
	if add.userID != owner.ID || add.ref.ID != 7100 || add.item.DocumentID != 502 || add.item.DocumentAccessHash != 5502 || add.item.Emoji != "😄" {
		t.Fatalf("add call = %+v, want owner/set/doc/emoji", add)
	}
	if hooks.userID != owner.ID || hooks.kind != domain.StickerSetKindStickers {
		t.Fatalf("hook = user %d kind %q, want owner stickers", hooks.userID, hooks.kind)
	}
	if _, found, _ := bots.GetBotChatState(context.Background(), domain.StickersBotUserID, owner.ID); found {
		t.Fatal("stickers bot state still present after add")
	}
}

func TestStickersBotAddStickerDuplicateStaysInFlow(t *testing.T) {
	svc, users, bots, messages, manager, _ := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3008")
	manager.sets = append(manager.sets, domain.StickerSet{
		ID:            7300,
		AccessHash:    8300,
		ShortName:     "fresh_pack",
		Title:         "Fresh Pack",
		Kind:          domain.StickerSetKindStickers,
		CreatorUserID: owner.ID,
		Count:         1,
		DocumentIDs:   []int64{501},
	})

	sendTextToStickers(t, svc, messages, owner, "/addsticker")
	sendTextToStickers(t, svc, messages, owner, "fresh_pack")
	sendDocumentToStickers(t, svc, messages, owner, stickerBotSetDocument(501, 5501, 7300, 8300))
	reply := sendTextToStickers(t, svc, messages, owner, "😄")

	if !strings.Contains(reply, "already in this pack") {
		t.Fatalf("duplicate add reply = %q, want already-in-pack notice", reply)
	}
	if len(manager.adds) != 0 {
		t.Fatalf("adds = %+v, want no manager add for duplicate", manager.adds)
	}
	state, found, _ := bots.GetBotChatState(context.Background(), domain.StickersBotUserID, owner.ID)
	if !found || state.Step != stickersBotStepDocument {
		t.Fatalf("state after duplicate = %+v found=%v, want document step", state, found)
	}
}

func TestStickersBotDeleteStickerFromExistingPack(t *testing.T) {
	svc, users, bots, messages, manager, _ := newStickersBotTestService(t)
	hooks := &stickersBotHookRecorder{}
	svc.SetRouterHooks(hooks)
	owner := newOwner(t, users, "+3007")
	manager.sets = append(manager.sets, domain.StickerSet{
		ID:            7200,
		AccessHash:    8200,
		ShortName:     "old_pack",
		Title:         "Old Pack",
		Kind:          domain.StickerSetKindStickers,
		Creator:       true,
		CreatorUserID: owner.ID,
		Count:         2,
		DocumentIDs:   []int64{601, 602},
	})

	sendTextToStickers(t, svc, messages, owner, "/delsticker")
	if reply := sendTextToStickers(t, svc, messages, owner, "old_pack"); !strings.Contains(reply, "Selected Old Pack") {
		t.Fatalf("select delete pack reply = %q, want selected pack", reply)
	}
	doc := stickerBotSetDocument(601, 6601, 7200, 8200)
	reply := sendDocumentToStickers(t, svc, messages, owner, doc)
	if !strings.Contains(reply, "Done. Removed from Old Pack") || !strings.Contains(reply, "https://telesrv.net/addstickers/old_pack") {
		t.Fatalf("delete final reply = %q, want done link", reply)
	}
	if len(manager.removes) != 1 || manager.removes[0].documentID != 601 || manager.removes[0].accessHash != 6601 {
		t.Fatalf("removes = %+v, want document 601", manager.removes)
	}
	if hooks.userID != owner.ID || hooks.kind != domain.StickerSetKindStickers {
		t.Fatalf("hook = user %d kind %q, want owner stickers", hooks.userID, hooks.kind)
	}
	if _, found, _ := bots.GetBotChatState(context.Background(), domain.StickersBotUserID, owner.ID); found {
		t.Fatal("stickers bot state still present after delete")
	}
}

func TestStickersBotDeleteCustomEmojiEntityFromExistingPack(t *testing.T) {
	svc, users, bots, messages, manager, _ := newStickersBotTestService(t)
	owner := newOwner(t, users, "+3009")
	manager.sets = append(manager.sets, domain.StickerSet{
		ID:            7400,
		AccessHash:    8400,
		ShortName:     "emoji_pack",
		Title:         "Emoji Pack",
		Kind:          domain.StickerSetKindEmoji,
		Emojis:        true,
		CreatorUserID: owner.ID,
		Count:         2,
		DocumentIDs:   []int64{701, 702},
	})
	manager.docs = map[int64]domain.Document{
		701: stickerBotSetCustomEmojiDocument(701, 7701, 7400, 8400),
	}

	sendTextToStickers(t, svc, messages, owner, "/delsticker")
	sendTextToStickers(t, svc, messages, owner, "emoji_pack")
	reply := sendMessageToStickers(t, svc, messages, owner, domain.Message{
		Body: "🔥",
		Entities: []domain.MessageEntity{{
			Type:       domain.MessageEntityCustomEmoji,
			Offset:     0,
			Length:     2,
			DocumentID: 701,
		}},
	})

	if !strings.Contains(reply, "Done. Removed from Emoji Pack") || !strings.Contains(reply, "https://telesrv.net/addemoji/emoji_pack") {
		t.Fatalf("delete custom emoji reply = %q, want done emoji link", reply)
	}
	if len(manager.removes) != 1 || manager.removes[0].documentID != 701 || manager.removes[0].accessHash != 7701 {
		t.Fatalf("removes = %+v, want custom emoji document 701", manager.removes)
	}
	if _, found, _ := bots.GetBotChatState(context.Background(), domain.StickersBotUserID, owner.ID); found {
		t.Fatal("stickers bot state still present after custom emoji delete")
	}
}

func botCommandExists(commands []domain.BotCommand, want string) bool {
	for _, c := range commands {
		if c.Command == want {
			return true
		}
	}
	return false
}

func assertReplyEntityText(t *testing.T, msg domain.Message, typ domain.MessageEntityType, want string) {
	t.Helper()
	for _, entity := range msg.Entities {
		if entity.Type != typ {
			continue
		}
		if entity.Offset < 0 || entity.Length < 0 || entity.Offset+entity.Length > len(msg.Body) {
			t.Fatalf("entity %+v out of ASCII bounds for %q", entity, msg.Body)
		}
		if got := msg.Body[entity.Offset : entity.Offset+entity.Length]; got == want {
			return
		}
	}
	t.Fatalf("message %q entities %+v missing %s entity for %q", msg.Body, msg.Entities, typ, want)
}

type stickersBotFakeCreator struct {
	created []domain.CreateStickerSetRequest
	sets    []domain.StickerSet
	docs    map[int64]domain.Document
	adds    []stickersBotAdd
	removes []stickersBotRemove
}

type stickersBotAdd struct {
	userID int64
	ref    domain.StickerSetRef
	item   domain.StickerSetItemInput
}

type stickersBotRemove struct {
	userID     int64
	documentID int64
	accessHash int64
}

func (f *stickersBotFakeCreator) CreateStickerSet(_ context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error) {
	f.created = append(f.created, req)
	docIDs := make([]int64, 0, len(req.Items))
	for _, item := range req.Items {
		docIDs = append(docIDs, item.DocumentID)
	}
	kind := req.Kind
	if kind == "" {
		kind = domain.StickerSetKindStickers
	}
	set := domain.StickerSet{
		ID:            7000 + int64(len(f.sets)),
		AccessHash:    8000 + int64(len(f.sets)),
		ShortName:     strings.ToLower(strings.TrimSpace(req.ShortName)),
		Title:         req.Title,
		Kind:          kind,
		Emojis:        kind == domain.StickerSetKindEmoji,
		Creator:       true,
		CreatorUserID: req.CreatorUserID,
		Count:         len(docIDs),
		DocumentIDs:   docIDs,
	}
	f.sets = append(f.sets, set)
	return set, nil, nil
}

func (f *stickersBotFakeCreator) ResolveStickerSet(_ context.Context, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error) {
	idx := f.indexSet(ref)
	if idx < 0 {
		return domain.StickerSet{}, nil, false, nil
	}
	return f.sets[idx], nil, true, nil
}

func (f *stickersBotFakeCreator) ListCreatedStickerSets(_ context.Context, userID int64, _ int64, limit int) ([]domain.StickerSet, int, error) {
	var out []domain.StickerSet
	for _, set := range f.sets {
		if set.CreatorUserID == userID {
			out = append(out, set)
		}
	}
	total := len(out)
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, total, nil
}

func (f *stickersBotFakeCreator) GetDocuments(_ context.Context, ids []int64) ([]domain.Document, error) {
	out := make([]domain.Document, 0, len(ids))
	for _, id := range ids {
		if doc, ok := f.docs[id]; ok {
			out = append(out, doc)
		}
	}
	return out, nil
}

func (f *stickersBotFakeCreator) AddStickerToSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error) {
	f.adds = append(f.adds, stickersBotAdd{userID: actorUserID, ref: ref, item: item})
	idx := f.indexSet(ref)
	if idx < 0 || f.sets[idx].CreatorUserID != actorUserID {
		return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
	}
	set := f.sets[idx]
	if !stickersBotTestContainsInt64(set.DocumentIDs, item.DocumentID) {
		set.DocumentIDs = append(set.DocumentIDs, item.DocumentID)
		set.Count = len(set.DocumentIDs)
	}
	f.sets[idx] = set
	return set, nil, nil
}

func (f *stickersBotFakeCreator) RemoveStickerFromSet(_ context.Context, actorUserID int64, documentID int64, accessHash int64) (domain.StickerSet, []domain.Document, error) {
	f.removes = append(f.removes, stickersBotRemove{userID: actorUserID, documentID: documentID, accessHash: accessHash})
	for i, set := range f.sets {
		if set.CreatorUserID != actorUserID {
			continue
		}
		for idx, id := range set.DocumentIDs {
			if id != documentID {
				continue
			}
			set.DocumentIDs = append(append([]int64(nil), set.DocumentIDs[:idx]...), set.DocumentIDs[idx+1:]...)
			set.Count = len(set.DocumentIDs)
			f.sets[i] = set
			return set, nil, nil
		}
	}
	return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
}

func (f *stickersBotFakeCreator) indexSet(ref domain.StickerSetRef) int {
	for i, set := range f.sets {
		switch ref.Kind {
		case domain.StickerSetRefByID:
			if set.ID == ref.ID && (ref.AccessHash == 0 || set.AccessHash == ref.AccessHash) {
				return i
			}
		case domain.StickerSetRefByShortName:
			if strings.EqualFold(set.ShortName, ref.ShortName) {
				return i
			}
		}
	}
	return -1
}

func stickersBotTestContainsInt64(values []int64, want int64) bool {
	for _, value := range values {
		if value == want {
			return true
		}
	}
	return false
}

type stickersBotFakeInstaller struct {
	installs []stickersBotInstall
}

type stickersBotInstall struct {
	userID int64
	setID  int64
	kind   domain.StickerSetKind
}

func (f *stickersBotFakeInstaller) InstallUserStickerSet(_ context.Context, userID int64, setID int64, kind domain.StickerSetKind, _ bool, _ int) error {
	f.installs = append(f.installs, stickersBotInstall{userID: userID, setID: setID, kind: kind})
	return nil
}

type stickersBotHookRecorder struct {
	userID int64
	kind   domain.StickerSetKind
}

func (h *stickersBotHookRecorder) RevokeBotSessions(context.Context, int64) error {
	return nil
}

func (h *stickersBotHookRecorder) PushBotCommandsChanged(context.Context, int64, []domain.BotCommand) {
}

func (h *stickersBotHookRecorder) PushStickerSetsChanged(_ context.Context, userID int64, kind domain.StickerSetKind) {
	h.userID = userID
	h.kind = kind
}

func TestNormalizeStickersBotShortNameAcceptsHostBasedAppLinks(t *testing.T) {
	for _, tc := range []struct {
		raw  string
		want string
	}{
		{raw: "telesrv://addstickers?set=Legacy_Pack", want: "legacy_pack"},
		{raw: "owpg://tenant.example.test/addstickers?set=Hosted_Pack", want: "hosted_pack"},
		{raw: "owpg://tenant.example.test/addemoji?set=Emoji_Pack", want: "emoji_pack"},
		{raw: "https://telesrv.net/addstickers/Web_Pack", want: "web_pack"},
	} {
		if got := normalizeStickersBotShortName(tc.raw); got != tc.want {
			t.Fatalf("normalizeStickersBotShortName(%q) = %q, want %q", tc.raw, got, tc.want)
		}
	}
}