package syncz

import (
	"context"
	"fmt"
	"log/slog"
	"slices"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"go.uber.org/mock/gomock"
)

func TestStartsWorkersAccordingToConfiguration(t *testing.T) {
	for caseNum, config := range testConfigurations() {
		t.Run(fmt.Sprintf("case %d", caseNum), func(t *testing.T) {
			expectedNumberOfWorkers := len(config)
			wm, ctrl, factory := setupWM(t)
			worker := NewMockWorker(ctrl)
			for i := range expectedNumberOfWorkers {
				factory.EXPECT().
					New(config[i]).
					Return(worker)
			}
			worker.EXPECT().
				Run(gomock.Any()).
				Times(expectedNumberOfWorkers)
			err := wm.ApplyConfiguration(config)
			require.NoError(t, err)
		})
	}
}

func TestUpdatesWorkersAccordingToConfiguration(t *testing.T) {
	normalOrder := testConfigurations()
	reverseOrder := testConfigurations()
	slices.Reverse(reverseOrder)
	tests := []struct {
		name    string
		configs [][]WorkSource[string, int]
	}{
		{
			name:    "normal order",
			configs: normalOrder,
		},
		{
			name:    "reverse order",
			configs: reverseOrder,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			numProjects := numUniqueConfigs(tc.configs)
			wm, ctrl, factory := setupWM(t)
			worker := NewMockWorker(ctrl)
			worker.EXPECT().
				Run(gomock.Any()).
				Do(func(ctx context.Context) {
					<-ctx.Done()
				}).
				Times(numProjects)
			factory.EXPECT().
				New(gomock.Any()).
				Return(worker).
				Times(numProjects)
			for _, config := range tc.configs {
				err := wm.ApplyConfiguration(config)
				require.NoError(t, err)
				// It's hard to test concurrent code. Just wait and let the right number of workers to start.
				time.Sleep(100 * time.Millisecond)
			}
		})
	}
}

func TestErrorsOnDuplicateSourceID(t *testing.T) {
	wm, _, _ := setupWM(t)
	cfg := []WorkSource[string, int]{
		{
			ID:            "123",
			Configuration: 123,
		},
		{
			ID:            "123",
			Configuration: 234,
		},
	}
	err := wm.ApplyConfiguration(cfg)
	assert.EqualError(t, err, "duplicate source id: 123")
}

func setupWM(t *testing.T) (*WorkerManager[string, int], *gomock.Controller, *MockWorkerFactory[string, int]) {
	ctrl := gomock.NewController(t)
	workerFactory := NewMockWorkerFactory[string, int](ctrl)
	wm := NewWorkerManager[string, int](testlogger.New(t), testFld, workerFactory, testEqual)
	t.Cleanup(wm.StopAndWait)
	return wm, ctrl, workerFactory
}

func testFld(v string) slog.Attr {
	return slog.String("test_worker_id", v)
}

func testEqual(c1, c2 int) bool {
	return c1 == c2
}

func numUniqueConfigs(cfgs [][]WorkSource[string, int]) int {
	num := 0
	configs := make(map[string]int)
	for _, cfg := range cfgs {
		for _, c := range cfg {
			old, ok := configs[c.ID]
			if ok {
				if old != c.Configuration {
					configs[c.ID] = c.Configuration
					num++
				}
			} else {
				configs[c.ID] = c.Configuration
				num++
			}
		}
	}
	return num
}

func testConfigurations() [][]WorkSource[string, int] {
	project1 := "bla1/project1"
	project2 := "bla1/project2"
	project3 := "bla3/project3"
	return [][]WorkSource[string, int]{
		{
			// Empty
		},
		{
			{
				ID:            project1,
				Configuration: 1,
			},
		},
		{
			{
				ID:            project1,
				Configuration: 2, // update config
			},
			{ // add new worker for project2
				ID:            project2,
				Configuration: 3,
			},
		},
		{
			// remove project1 worker
			{ // add new worker for project3
				ID:            project3,
				Configuration: 5,
			},
			{
				ID:            project2,
				Configuration: 4, // update config
			},
		},
	}
}
