// +build integration

package server

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"testing"

	"code.justin.tv/live/autohost/internal/hosting/utils/stringslice"
	"code.justin.tv/live/autohost/lib"

	"github.com/stretchr/testify/mock"

	"code.justin.tv/live/autohost/internal/hosting/clients/spade"

	twitchrecs "code.justin.tv/amzn/TwitchRecsTwirp"
	"github.com/twitchtv/twirp"

	"code.justin.tv/live/autohost/rpc/hosting"
	"github.com/stretchr/testify/require"
)

func TestGetEndorsedChannels_InvalidArguments(t *testing.T) {
	t.Run("test when missing target channel id", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"

		// Missing Target Channel
		ctx, cancel := getContextWithTimeout()
		defer cancel()
		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     createUserContext(userID),
			TargetChannelId: "",
		})
		require.Nil(t, response)
		requireTwirpError(t, twirp.InvalidArgument, err)
	})
}

func TestGetEndorsedChannels_OptionalUserContext(t *testing.T) {
	t.Run("test it sorts by view count when missing user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     nil,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})

	t.Run("test it sorts by view count when missing country from user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		userContext := createUserContext(userID)
		userContext.Country = ""

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})

	t.Run("test it sorts by view count when missing device id from user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		userContext := createUserContext(userID)
		userContext.DeviceId = ""

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})

	t.Run("test it sorts by view count when missing platform from user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		userContext := createUserContext(userID)
		userContext.Platform = ""

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})

	t.Run("test it sorts by view count when missing languages from user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		userContext := createUserContext(userID)
		userContext.Languages = nil

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})

	t.Run("test it sorts by view count when empty languages in user context", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"chan_2", "chan_3", "chan_1"}

		userContext := createUserContext(userID)
		userContext.Languages = []string{}

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)
		_, sortedChannelIDs := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           5,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), 3)
		verifyCreatorOrderedEndorsedChannels(t, sortedChannelIDs, response.GetEndorsedChannels())
	})
}

func TestGetEndorsedChannels_RecsOrdering(t *testing.T) {
	t.Run("test basic request", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		// Create context and setup autohost list result
		ctx, cancel := getContextWithTimeout()
		defer cancel()

		recChannelIDs, _ := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			3,
			3,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)

		// Ensure order of response matches recs response
		endorsedChannels := response.GetEndorsedChannels()
		require.Len(t, endorsedChannels, 3)
		for i, endorsedChannel := range endorsedChannels {
			require.Equal(t, recChannelIDs[i], endorsedChannel.GetChannelId())
		}

		// No tracking events should have been sent
		require.Equal(t, 0, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test empty list received when source list is empty", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), 0)
	})

	t.Run("test limit 0 of basic request", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)
		spadeStub := testEnv.spadeStub

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			5,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           0,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), 0)
		require.Equal(t, 0, spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test limit of basic request", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)
		spadeStub := testEnv.spadeStub

		endorsementListLength := 20
		numRecResults := 5
		limit := 10

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		_, backFill := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementListLength,
			numRecResults,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           int64(limit),
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), limit)
		require.Equal(t, 5, spadeStub.GetNumberInsertRecommendationEvents())

		// Ensure we sent events for back fill ids only, and that the list contains no duplicates
		sentEvents := spadeStub.GetInsertedRecommendationTrackingItems()
		testTrackingEventUniqueness(t, sentEvents)

		for _, event := range sentEvents {
			require.True(t, stringslice.Contains(backFill, event.ItemID))
		}
	})

	t.Run("test limit greater than result", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		endorsementListLength := 10
		numRecResults := 5
		limit := 30

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementListLength,
			numRecResults,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           int64(limit),
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), endorsementListLength)
		require.Equal(t, 5, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test num returned recs equal autohost list size", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		endorsementListLength := 10
		numRecResults := 10
		limit := 30

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementListLength,
			numRecResults,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           int64(limit),
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), endorsementListLength)
		require.Equal(t, 0, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test recs result greater than limit", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		endorsementListLength := 30
		numRecResults := 20
		limit := 10

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementListLength,
			numRecResults,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           int64(limit),
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Len(t, response.GetEndorsedChannels(), limit)
		require.Equal(t, 0, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})
}

func TestGetEndorsedChannels_RecsOrdering_BackFill(t *testing.T) {
	t.Run("test back fill works when recs returns subset of list", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		endorsementsListLength := 7
		numRecResults := 3
		limit := 10

		recChannelIDs, _ := setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementsListLength,
			numRecResults,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           int64(limit),
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		endorsedChannels := response.GetEndorsedChannels()
		require.Len(t, endorsedChannels, endorsementsListLength)

		// Ensure order of response matches recs response and they are at the beginning of the list
		for i, recChannelID := range recChannelIDs {
			require.Equal(t, recChannelID, endorsedChannels[i].GetChannelId())
		}

		// Ensure back fill items have tracking ids
		for i := numRecResults; i < endorsementsListLength; i++ {
			require.NotEmpty(t, response.GetEndorsedChannels()[i].ChannelId)
		}

		// Confirm back fill items have unique ids
		testUniqueness(t, response.GetEndorsedChannels())
		require.Equal(t, 4, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test back fill works when recs returns empty list", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		endorsementsListLength := 7
		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementsListLength,
			0,
			false,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), endorsementsListLength)
		require.Equal(t, endorsementsListLength, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})
}

func TestGetEndorsedChannels_RecsOrdering_Errors(t *testing.T) {
	t.Run("test back filled when TwitchRecs errors", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		endorsementsListLength := 7
		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementsListLength,
			0,
			false,
			true)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), endorsementsListLength)
		require.Equal(t, endorsementsListLength, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test back filled when Liveline errors", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		endorsementsListLength := 7
		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			endorsementsListLength,
			0,
			true,
			false)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), endorsementsListLength)
		require.Equal(t, endorsementsListLength, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})

	t.Run("test back filled when TwitchRecs and Liveline errors", func(t *testing.T) {
		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		userID := "1234567"
		targetChannelID := "9876543"
		userContext := createUserContext(userID)

		ctx, cancel := getContextWithTimeout()
		defer cancel()

		numEndorsed := 7
		setupMocksForRecsOrderedEndorsementsTest(
			t,
			ctx,
			testEnv,
			targetChannelID,
			numEndorsed,
			0,
			true,
			true)

		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})

		require.NoError(t, err)
		require.NotNil(t, response)
		require.Equal(t, len(response.GetEndorsedChannels()), numEndorsed)
		require.Equal(t, numEndorsed, testEnv.spadeStub.GetNumberInsertRecommendationEvents())
	})
}

func TestGetEndorsedChannels_CreatorOrdering(t *testing.T) {
	t.Run("GetEndorsedChannels returns channels using creator's ordering, with live channels in front", func(t *testing.T) {
		ctx, cancel := getContextWithTimeout()
		defer cancel()

		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"live_1", "offline_1", "live_2", "offline_2"}
		expectedIDs := []string{"live_1", "live_2", "offline_1", "offline_2"}

		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, false)

		// Set up liveline mock to succeed.
		livelineResp := map[string]bool{
			"live_1": true,
			"live_2": true,
		}

		testEnv.livelineMock.
			On("GetLiveChannelSet", mock.Anything, endorsementList).
			Return(livelineResp, nil)

		// Get endorsed channels, and verify that
		// - live channels appear before offline
		// - the creator's ordering is maintained.
		userContext := createUserContext(userID)
		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})
		require.NoError(t, err)
		require.NotNil(t, response)
		verifyCreatorOrderedEndorsedChannels(t, expectedIDs, response.GetEndorsedChannels())
	})

	t.Run("GetEndorsedChannels returns channels using creator's ordering when creator views their own channel endorsements", func(t *testing.T) {
		ctx, cancel := getContextWithTimeout()
		defer cancel()

		userID := "1234567"
		targetChannelID := "1234567"
		endorsementList := []string{"live_1", "offline_1", "live_2", "offline_2"}
		expectedIDs := []string{"live_1", "live_2", "offline_1", "offline_2"}

		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, true)

		// Set up liveline mock to succeed.
		livelineResp := map[string]bool{
			"live_1": true,
			"live_2": true,
		}

		testEnv.livelineMock.
			On("GetLiveChannelSet", mock.Anything, endorsementList).
			Return(livelineResp, nil)

		// Get endorsed channels, and verify that
		// - live channels appear before offline
		// - the creator's ordering is maintained.
		userContext := createUserContext(userID)
		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})
		require.NoError(t, err)
		require.NotNil(t, response)
		verifyCreatorOrderedEndorsedChannels(t, expectedIDs, response.GetEndorsedChannels())
	})

	t.Run("Returns channels if Liveline errors", func(t *testing.T) {
		ctx, cancel := getContextWithTimeout()
		defer cancel()

		userID := "1234567"
		targetChannelID := "9876543"
		endorsementList := []string{"live_1", "offline_1", "live_2", "offline_2"}

		testEnv := newTestEnv(t)
		hostingClient := testEnv.hostingClient
		setupDBForEndorsementsTest(t, ctx, testEnv, targetChannelID, endorsementList, false)

		// Set up Liveline mock to return an error.
		testEnv.livelineMock.
			On("GetLiveChannelSet", mock.Anything, endorsementList).
			Return(nil, errors.New("expected liveline error"))

		// Get endorsed channels, and verify that it fell back to returning channels
		// using the creator's ordering (offline channels may appear before live).
		userContext := createUserContext(userID)
		response, err := hostingClient.GetEndorsedChannels(ctx, &hosting.GetEndorsedChannelsRequest{
			UserContext:     userContext,
			TargetChannelId: targetChannelID,
			Limit:           10,
		})
		require.NoError(t, err)
		require.NotNil(t, response)

		verifyCreatorOrderedEndorsedChannels(t, endorsementList, response.GetEndorsedChannels())
	})
}

// setupMocksForRecsOrderedEndorsementsTest configures mocks for tests that verify GetEndorsements
// using Recommendations for ordering. It returns,
//   1. the channel IDs in the user's Endorsement list that were ordered by Recommendations
//   2. the remaining channel IDs in the user's Endorsement list, ordered by CCV
func setupMocksForRecsOrderedEndorsementsTest(
	t *testing.T,
	ctx context.Context,
	env *testEnv,
	targetChannelID string,
	endorsementListLength int,
	numRecResults int,
	simulateLivelineError bool,
	simulateRecsError bool) ([]string, []string) {

	idGenerator := &idGenerator{}

	env.authStub.AllowAllCalls = true

	// Generate the channel IDs that Recommendations will return.
	recChannelIDs := idGenerator.createChannelIDs(numRecResults)

	// Generate the remaining channel IDs that are on the endorsement list, but were not
	// returned by Recommendations.
	backfillChannelIDs := idGenerator.createChannelIDs(endorsementListLength - numRecResults)

	// Create the user's complete endorsement list from the lists above.
	endorsementsList := stringslice.Concat(recChannelIDs, backfillChannelIDs)
	rand.Shuffle(endorsementListLength, func(i, j int) {
		a := endorsementsList
		a[j], a[i] = a[i], a[j]
	})

	// Configure the recs client.
	if simulateRecsError {
		env.recsStub.SetErrorResultForGetRecommendedChannels(errors.New("expected error"))
	} else {
		recResults := createRecsResults(recChannelIDs)
		env.recsStub.SetResultsForGetRecommendedChannels(recResults)
	}

	// Configure the Liveline client.
	if simulateLivelineError {
		env.livelineMock.
			On("SortChannelsByCCV", mock.Anything, matchedByUnorderedIDs(backfillChannelIDs)).
			Return(nil, errors.New("expected sort error"))
	} else {
		env.livelineMock.
			On("SortChannelsByCCV", mock.Anything, matchedByUnorderedIDs(backfillChannelIDs)).
			Return(backfillChannelIDs, nil)
	}

	// Add the list and ordering setting to the database.
	setupDBForEndorsementsTest(t, ctx, env, targetChannelID, endorsementsList, true)

	return recChannelIDs, backfillChannelIDs
}

func matchedByUnorderedIDs(expectedIDs []string) interface{} {
	return mock.MatchedBy(func(givenIDs []string) bool {
		r := stringslice.ContainsSameElements(expectedIDs, givenIDs)
		if !r {
			fmt.Println("matchedByUnorderedIDs - IDs did not match")
			fmt.Printf("- expected (in any order): [%s]\n", strings.Join(expectedIDs, ", "))
			fmt.Printf("- actual: [%s]\n", strings.Join(givenIDs, ", "))
		}
		return r
	})
}

func createRecsResults(channels []string) []*twitchrecs.RecommendedItem {
	recsList := make([]*twitchrecs.RecommendedItem, len(channels))
	for i, channel := range channels {
		recsList[i] = &twitchrecs.RecommendedItem{
			ItemId:     channel,
			TrackingId: strconv.Itoa(i),
		}
	}

	return recsList
}

func createUserContext(userID string) *hosting.UserContext {
	return &hosting.UserContext{
		DeviceId:  "deviceId",
		UserId:    userID,
		Platform:  "web",
		Languages: []string{"en_us"},
		Country:   "us",
	}
}

func testUniqueness(t *testing.T, channels []*hosting.EndorsedChannel) {
	// Used to track uniqueness of response and tracking ids
	channelMap := make(map[string]bool)
	trackingIdMap := make(map[string]bool)

	for _, endorsedChannel := range channels {
		channelID := endorsedChannel.ChannelId
		trackingID := endorsedChannel.ModelTrackingId

		_, hasItemId := channelMap[channelID]
		_, hasTrackingId := trackingIdMap[trackingID]

		require.False(t, hasItemId, "Duplicate entry %s found in response", channelID)
		require.False(t, hasTrackingId, "Duplicate tracking id found in response", trackingID)

		channelMap[channelID] = true
		trackingIdMap[trackingID] = true
	}
}

func testTrackingEventUniqueness(t *testing.T, trackingItems []spade.InsertedRecommendationTrackingProperties) {
	// Used to track uniqueness of channel and tracking ids
	channelMap := make(map[string]bool)
	trackingIdMap := make(map[string]bool)

	for _, item := range trackingItems {
		itemID := item.ItemID
		trackingID := item.TrackingID

		_, hasItemId := channelMap[itemID]
		_, hasTrackingId := trackingIdMap[trackingID]

		require.False(t, hasItemId, "Duplicate entry %s found in tracking events", itemID)
		require.False(t, hasTrackingId, "Duplicate tracking id found in response", trackingID)

		channelMap[itemID] = true
		trackingIdMap[trackingID] = true
	}
}

func verifyCreatorOrderedEndorsedChannels(
	t *testing.T, expectedChannelIDs []string, endorsedChannels []*hosting.EndorsedChannel) {

	// Verify that endorsed channels contains the given expected channel IDs.
	endorsedChannelIDs := make([]string, len(endorsedChannels))
	for i, e := range endorsedChannels {
		endorsedChannelIDs[i] = e.ChannelId
	}

	require.Equal(t, expectedChannelIDs, endorsedChannelIDs)

	testUniqueness(t, endorsedChannels)
}

func setupDBForEndorsementsTest(
	t *testing.T,
	ctx context.Context,
	env *testEnv,
	targetChannelID string,
	endorsementsList []string,
	orderWithRecs bool) {

	env.authStub.AllowAllCalls = true

	// Set the user's endorsement list in the DB and cache.
	_, err := env.logic.SetList(ctx, targetChannelID, targetChannelID, endorsementsList)
	require.NoError(t, err)

	// Set the user's settings to control how endorsements are ordered.
	strategy := lib.AutohostStrategyOrdered
	if orderWithRecs {
		strategy = lib.AutohostStrategyRandom
	}
	_, err = env.logic.UpdateSettings(ctx, targetChannelID, targetChannelID, &lib.UpdateSettingsInput{
		Strategy: &strategy,
	})

	require.NoError(t, err)
}

// idGenerator can be used by tests to generate lists of channel IDs.
// E.g. ["chan_1", "chan_2", "chan_3"]
type idGenerator struct {
	lastSuffix int
}

func (g *idGenerator) createChannelIDs(length int) []string {
	ids := make([]string, length)
	for i := 0; i < length; i++ {
		g.lastSuffix++
		ids[i] = fmt.Sprintf("chan_%d", g.lastSuffix)
	}
	return ids
}
