package service

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/owncord/server/db"
	"github.com/owncord/server/permissions"
)

// seedReactedMessage posts a message in channel 10 and has each of userIDs
// react to it with emoji, returning the message id.
func seedReactedMessage(t *testing.T, svc *MessageService, database *db.DB, emoji string, userIDs ...int64) int64 {
	t.Helper()
	msgID, err := database.CreateMessage(context.Background(), 10, 1, "react to me", nil)
	if err != nil {
		t.Fatalf("CreateMessage: %v", err)
	}
	for _, uid := range userIDs {
		if _, err := svc.AddReaction(context.Background(), uid, msgID, emoji); err != nil {
			t.Fatalf("AddReaction(%d): %v", uid, err)
		}
	}
	return msgID
}

func TestGetReactionUsers_ReturnsReactors(t *testing.T) {
	svc, database := newTestMessageService(t)
	seedUser(t, database, &db.User{ID: 2, Username: "bob"})
	seedUserRole(t, database, 2, permissions.MemberRoleID)
	msgID := seedReactedMessage(t, svc, database, "👍", 1, 2)

	users, err := svc.GetReactionUsers(context.Background(), 1, 10, msgID, "👍")
	if err != nil {
		t.Fatalf("GetReactionUsers: %v", err)
	}
	if len(users) != 2 {
		t.Fatalf("len(users) = %d, want 2 (%+v)", len(users), users)
	}
	if users[0].Username != "alice" || users[1].Username != "bob" {
		t.Errorf("usernames = [%s %s], want [alice bob]", users[0].Username, users[1].Username)
	}
}

// An emoji nobody used is an empty list, never nil — the handler serialises it
// straight to JSON and `null` is not a list the client can iterate.
func TestGetReactionUsers_EmptyIsNonNil(t *testing.T) {
	svc, database := newTestMessageService(t)
	msgID := seedReactedMessage(t, svc, database, "👍", 1)

	users, err := svc.GetReactionUsers(context.Background(), 1, 10, msgID, "🎉")
	if err != nil {
		t.Fatalf("GetReactionUsers: %v", err)
	}
	if users == nil {
		t.Fatal("users = nil, want an empty slice")
	}
	if len(users) != 0 {
		t.Errorf("len(users) = %d, want 0", len(users))
	}
}

// Reading the reactor list is gated by the same READ_MESSAGES check as reading
// the channel's history: a reaction pill must not leak who is in a channel the
// caller cannot see.
func TestGetReactionUsers_ForbiddenWithoutReadPermission(t *testing.T) {
	svc, database := newTestMessageService(t)
	msgID := seedReactedMessage(t, svc, database, "👍", 1)

	seedRole(t, database, &db.Role{ID: 90, Name: "outsider", Permissions: 0, Position: 1})
	seedUser(t, database, &db.User{ID: 3, Username: "outsider"})
	seedUserRole(t, database, 3, 90)

	_, err := svc.GetReactionUsers(context.Background(), 3, 10, msgID, "👍")
	if !errors.Is(err, ErrForbidden) {
		t.Fatalf("err = %v, want ErrForbidden", err)
	}
}

// The channel in the URL is what the permission check ran against, so a message
// that lives elsewhere must not be answered from that check.
func TestGetReactionUsers_MessageInAnotherChannelIsNotFound(t *testing.T) {
	svc, database := newTestMessageService(t)
	seedChannel(t, database, &db.Channel{ID: 11, Name: "other", Type: "text"})
	msgID := seedReactedMessage(t, svc, database, "👍", 1)

	_, err := svc.GetReactionUsers(context.Background(), 1, 11, msgID, "👍")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

func TestGetReactionUsers_RejectsBadInput(t *testing.T) {
	svc, database := newTestMessageService(t)
	msgID := seedReactedMessage(t, svc, database, "👍", 1)

	tests := []struct {
		name    string
		msgID   int64
		emoji   string
		wantErr error
	}{
		{"zero message id", 0, "👍", ErrBadRequest},
		{"negative message id", -5, "👍", ErrBadRequest},
		{"empty emoji", msgID, "", ErrBadRequest},
		// The cap is derived from MaxShortcodeLen + 2, so a bare ":wave:"-shaped
		// string at that exact length must pass and one rune more must not.
		{"overlong emoji", msgID, strings.Repeat("a", MaxShortcodeLen+3), ErrBadRequest},
		{"control character", msgID, "a\x01b", ErrBadRequest},
		{"unknown message", 999999, "👍", ErrNotFound},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := svc.GetReactionUsers(context.Background(), 1, 10, tt.msgID, tt.emoji)
			if !errors.Is(err, tt.wantErr) {
				t.Fatalf("err = %v, want %v", err, tt.wantErr)
			}
		})
	}
}

// A DM the caller is not a participant of is reported as not-found (matching
// requireChannelRead), so its existence stays hidden.
func TestGetReactionUsers_ForeignDMIsNotFound(t *testing.T) {
	svc, database := newTestMessageService(t)
	seedUser(t, database, &db.User{ID: 2, Username: "bob"})
	seedUser(t, database, &db.User{ID: 3, Username: "carol"})
	seedUserRole(t, database, 2, permissions.MemberRoleID)
	seedUserRole(t, database, 3, permissions.MemberRoleID)

	ch, _, err := database.GetOrCreateDMChannel(context.Background(), 2, 3)
	if err != nil {
		t.Fatalf("GetOrCreateDMChannel: %v", err)
	}
	msgID, err := database.CreateMessage(context.Background(), ch.ID, 2, "psst", nil)
	if err != nil {
		t.Fatalf("CreateMessage: %v", err)
	}
	if _, err := svc.AddReaction(context.Background(), 3, msgID, "👍"); err != nil {
		t.Fatalf("AddReaction: %v", err)
	}

	if _, err := svc.GetReactionUsers(context.Background(), 1, ch.ID, msgID, "👍"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
	// A participant does get the list.
	users, err := svc.GetReactionUsers(context.Background(), 2, ch.ID, msgID, "👍")
	if err != nil {
		t.Fatalf("GetReactionUsers(participant): %v", err)
	}
	if len(users) != 1 || users[0].Username != "carol" {
		t.Errorf("users = %+v, want [carol]", users)
	}
}

// A soft-deleted message must not leak its reactor list. Its siblings in the
// same file/package already refuse a deleted message: handleReaction (this
// file) and GetMessagesAround (message_query.go) both check msg.Deleted, but
// GetReactionUsers had no such guard, so a tombstoned message's reactions
// stayed forever fetchable by direct URL even though the client no longer
// renders the message at all.
func TestGetReactionUsers_DeletedMessageIsNotFound(t *testing.T) {
	svc, database := newTestMessageService(t)
	msgID := seedReactedMessage(t, svc, database, "👍", 1)

	if err := database.DeleteMessage(context.Background(), msgID, 1, false); err != nil {
		t.Fatalf("DeleteMessage: %v", err)
	}

	_, err := svc.GetReactionUsers(context.Background(), 1, 10, msgID, "👍")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

// A custom emoji is reacted with as its ":shortcode:" literal, so the longest
// shortcode the emoji service will accept has to fit inside the reaction length
// cap. Before the cap was derived from MaxShortcodeLen, a 31- or 32-character
// shortcode produced an emoji that rendered in messages but was silently
// refused as a reaction.
func TestReaction_AcceptsLongestCustomShortcode(t *testing.T) {
	svc, database := newTestMessageService(t)
	msgID, err := database.CreateMessage(context.Background(), 10, 1, "react to me", nil)
	if err != nil {
		t.Fatalf("CreateMessage: %v", err)
	}

	longest := ":" + strings.Repeat("a", MaxShortcodeLen) + ":"
	if _, vErr := ValidateShortcode(longest); vErr != nil {
		t.Fatalf("the emoji service would refuse %q: %v", longest, vErr)
	}
	if _, rErr := svc.AddReaction(context.Background(), 1, msgID, longest); rErr != nil {
		t.Fatalf("AddReaction(longest shortcode): %v", rErr)
	}

	// One rune past it is still refused.
	tooLong := ":" + strings.Repeat("a", MaxShortcodeLen+1) + ":"
	if _, rErr := svc.AddReaction(context.Background(), 1, msgID, tooLong); !errors.Is(rErr, ErrBadRequest) {
		t.Fatalf("AddReaction(one past the cap) = %v, want ErrBadRequest", rErr)
	}
}
