package worker_test

import (
	"context"
	"fmt"
	"testing"

	"code.justin.tv/creator-collab/log"
	dbMocks "code.justin.tv/live/autohost/internal/database/mocks"
	"code.justin.tv/live/autohost/internal/metrics"
	"code.justin.tv/live/autohost/internal/worker"
	"code.justin.tv/live/autohost/internal/worker/clients/clue"
	"code.justin.tv/live/autohost/internal/worker/mocks"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

func Test_ProcessOnce_UnhostsNewlyLiveChannels(t *testing.T) {
	mockDB := &dbMocks.Database{}
	mockClue := &clue.MockClient{}
	mockMultiplex := &mocks.LiveService{}
	mockUsers := &mocks.UsersService{}

	a := worker.NewUnhoster(worker.UnhosterParams{
		DB:             mockDB,
		Clue:           mockClue,
		Logger:         log.NewDevelopmentLogger(),
		Multiplex:      mockMultiplex,
		Users:          mockUsers,
		SampleReporter: metrics.NewNoopSampleReporter(),
	})

	// First loop iteration.
	{
		mockDB.On("GetCachedLiveUsers", mock.Anything).Return(map[string]bool{
			"a": true,
			"b": true,
		}, nil).Once()
		newLive := map[string]bool{
			// Previously found to be live.
			"a": true,
			"c": true,
			// Recently went live.
			"d": true,
		}
		mockMultiplex.On("LiveChannels", mock.Anything).Return(newLive, nil).Once()
		mockUsers.
			On("GetChannelIDs", mock.Anything, matchAnyOrder([]string{"c", "d"})).
			Return(map[string]string{
				"c": "333333333",
				"d": "444444444",
			}, nil).
			Once()
		mockClue.
			On("GetHostTargets", mock.Anything, matchAnyOrder([]string{"333333333", "444444444"})).
			Return(map[string]string{
				"333333333": "",
				"444444444": "9999999999",
			}, nil).
			Once()
		mockClue.
			On("Unhost", mock.Anything, "444444444").
			Return(nil, nil).
			Once()

		mockDB.On("CacheLiveUsers", mock.Anything, newLive).Return(nil).Once()

		err := a.ProcessOnce(context.Background())
		require.NoError(t, err)

		mockDB.AssertExpectations(t)
		mockClue.AssertExpectations(t)
		mockMultiplex.AssertExpectations(t)
		mockUsers.AssertExpectations(t)
	}

	// Second loop iteration.
	// Verify that we don't read from the database.
	{
		newLive := map[string]bool{
			// Previously found to be live.
			"d": true,
			// Recently went live.
			"e": true,
			"f": true,
			"g": true,
		}
		mockMultiplex.On("LiveChannels", mock.Anything).Return(newLive, nil).Once()
		mockUsers.
			On("GetChannelIDs", mock.Anything, matchAnyOrder([]string{"e", "f", "g"})).
			Return(map[string]string{
				"e": "555555555",
				"f": "66666666",
				"g": "777777777",
			}, nil).
			Once()
		mockClue.
			On("GetHostTargets", mock.Anything, matchAnyOrder([]string{"555555555", "66666666", "777777777"})).
			Return(map[string]string{
				"555555555": "9999999999",
				"66666666":  "",
			}, nil).
			Once()
		mockClue.
			On("Unhost", mock.Anything, "555555555").
			Return(nil, nil).
			Once()
		mockClue.
			On("Unhost", mock.Anything, "777777777").
			Return(nil, nil).
			Once()

		mockDB.On("CacheLiveUsers", mock.Anything, newLive).Return(nil).Once()

		err := a.ProcessOnce(context.Background())
		require.NoError(t, err)

		mockDB.AssertExpectations(t)
		mockClue.AssertExpectations(t)
		mockMultiplex.AssertExpectations(t)
		mockUsers.AssertExpectations(t)
	}
}

func Test_ProcessOnce_NoChanges(t *testing.T) {
	mockDB := &dbMocks.Database{}
	mockClue := &clue.MockClient{}
	mockMultiplex := &mocks.LiveService{}
	mockUsers := &mocks.UsersService{}

	a := worker.NewUnhoster(worker.UnhosterParams{
		DB:             mockDB,
		Clue:           mockClue,
		Logger:         log.NewDevelopmentLogger(),
		Multiplex:      mockMultiplex,
		Users:          mockUsers,
		SampleReporter: metrics.NewNoopSampleReporter(),
	})

	mockDB.On("GetCachedLiveUsers", mock.Anything).Return(map[string]bool{
		"a": true,
		"b": true,
	}, nil).Once()
	newLive := map[string]bool{
		// All previously found to be live.
		"a": true,
		"b": true,
	}
	mockMultiplex.On("LiveChannels", mock.Anything).Return(newLive, nil).Once()
	mockUsers.
		On("GetChannelIDs", mock.Anything, matchAnyOrder([]string{})).
		Return(map[string]string{}, nil).
		Once()
	mockClue.
		On("GetHostTargets", mock.Anything, matchAnyOrder([]string{})).
		Return(map[string]string{}, nil).
		Once()
	mockDB.On("CacheLiveUsers", mock.Anything, newLive).Return(nil).Once()

	err := a.ProcessOnce(context.Background())
	require.NoError(t, err)

	mockDB.AssertExpectations(t)
	mockClue.AssertExpectations(t)
	mockMultiplex.AssertExpectations(t)
	mockUsers.AssertExpectations(t)
}

// Returns a mock matcher that matches if the given string slice has the same elements as the configured
// string slice, regardless of the order.
func matchAnyOrder(expected []string) interface{} {
	return mock.MatchedBy(func(actual []string) bool {
		if len(expected) != len(actual) {
			return false
		}

		expectedMap := make(map[string]bool, len(expected))
		for _, s := range expected {
			expectedMap[s] = true
		}

		for _, a := range actual {
			if !expectedMap[a] {
				fmt.Sprintf("")
				return false
			}
		}

		return true
	})
}
