package agent

import (
	"context"
	"testing"
	"time"

	"github.com/ash2k/stager"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_cmd_agent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"go.uber.org/mock/gomock"
	"golang.org/x/sync/errgroup"
	"k8s.io/apimachinery/pkg/util/wait"
)

func TestConfigurationIsApplied(t *testing.T) {
	var cfg1 int64 = 1
	var cfg2 int64 = 2
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ctrl := gomock.NewController(t)
	watcher := mock_cmd_agent.NewMockConfigurationWatcher[int64](ctrl)
	m := mock_modagent.NewMockModule[int32](ctrl)
	ctx1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	ctx2, cancel2 := context.WithCancel(context.Background())
	defer cancel2()
	m.EXPECT().
		Run(gomock.Any(), gomock.Any()).
		Do(func(ctx context.Context, cfg <-chan int32) error {
			c := <-cfg
			cancel1()
			assert.EqualValues(t, cfg1, c)
			c = <-cfg
			cancel2()
			assert.EqualValues(t, cfg2, c)
			<-ctx.Done()
			return nil
		})
	gomock.InOrder(
		watcher.EXPECT().
			Watch(gomock.Any(), gomock.Any()).
			Do(func(ctx context.Context, callback func(context.Context, int64)) {
				callback(ctx, cfg1)
				<-ctx1.Done()
				callback(ctx, cfg2)
				<-ctx2.Done()
				cancel()
			}),
		m.EXPECT().
			DefaultAndValidateConfiguration(int32(cfg1)),
		m.EXPECT().
			DefaultAndValidateConfiguration(int32(cfg2)),
	)
	a := ModuleRunner[int64, int32]{
		Log:     testlogger.New(t),
		Watcher: watcher,
		Data2Config: func(data int64) (int32, []any) {
			return int32(data), nil
		},
	}
	stages := a.RegisterModules(m)
	g, ctx := errgroup.WithContext(ctx)
	g.Go(func() error {
		return stager.RunStages(ctx, stages...)
	})
	g.Go(func() error {
		return a.RunConfigurationRefresh(ctx)
	})
	err := g.Wait()
	require.NoError(t, err)
}

func TestConfigurationIsSquashed(t *testing.T) {
	var cfg1 int64 = 1
	var cfg2 int64 = 2
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ctrl := gomock.NewController(t)
	watcher := mock_cmd_agent.NewMockConfigurationWatcher[int64](ctrl)
	m := mock_modagent.NewMockModule[int32](ctrl)
	ctx1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	m.EXPECT().
		Run(gomock.Any(), gomock.Any()).
		Do(func(ctx context.Context, cfg <-chan int32) error {
			<-ctx1.Done()
			c := <-cfg
			cancel()
			assert.EqualValues(t, cfg2, c)
			<-ctx.Done()
			return nil
		})
	gomock.InOrder(
		watcher.EXPECT().
			Watch(gomock.Any(), gomock.Any()).
			Do(func(ctx context.Context, callback func(context.Context, int64)) {
				callback(ctx, cfg1)
				callback(ctx, cfg2)
				cancel1()
				<-ctx.Done()
			}),
		m.EXPECT().
			DefaultAndValidateConfiguration(int32(cfg1)),
		m.EXPECT().
			DefaultAndValidateConfiguration(int32(cfg2)),
	)
	a := ModuleRunner[int64, int32]{
		Log:     testlogger.New(t),
		Watcher: watcher,
		Data2Config: func(data int64) (int32, []any) {
			return int32(data), nil
		},
	}
	stages := a.RegisterModules(m)
	g, ctx := errgroup.WithContext(ctx)
	g.Go(func() error {
		return stager.RunStages(ctx, stages...)
	})
	g.Go(func() error {
		return a.RunConfigurationRefresh(ctx)
	})
	err := g.Wait()
	require.NoError(t, err)
}

func TestModule_ModuleTerminatesWithoutStop(t *testing.T) {
	// NOTE: we cannot use the module runner to test this behavior, because the panic happens in a goroutine that we cannot recover from or assert.
	ctrl := gomock.NewController(t)
	m := mock_modagent.NewMockModule[int32](ctrl)
	m.EXPECT().Run(gomock.Any(), gomock.Any())
	m.EXPECT().Name().Return("test-module")

	a := moduleHolder[int32]{
		module:      m,
		cfg2pipe:    make(chan int32),
		pipe2module: make(chan int32),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	assert.PanicsWithError(t, "the module 'test-module' terminated from its Run method without a stop. This is a programming error in that module. Please open an issue in https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/issues", func() {
		a.runModule(ctx)
	})
}

func TestModule_ModuleTerminatesDueToConfigChannelClosed(t *testing.T) {
	ctrl := gomock.NewController(t)
	m := mock_modagent.NewMockModule[int32](ctrl)
	m.EXPECT().
		Run(gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctxm context.Context, cfgm <-chan int32) error {
			<-cfgm // wait for config channel, not for context
			return nil
		})
	m.EXPECT().Name().Return("test-module").AnyTimes()
	a := moduleHolder[int32]{
		module:      m,
		cfg2pipe:    make(chan int32),
		pipe2module: make(chan int32),
	}

	ctxPipe, cancelPipe := context.WithCancel(context.Background())
	ctxModule, cancelModule := context.WithCancel(context.Background())

	var wg wait.Group

	wg.Start(func() {
		assert.NoError(t, a.runPipe(ctxPipe))
	})
	wg.Start(func() {
		assert.NoError(t, a.runModule(ctxModule))
	})
	cancelPipe()
	time.Sleep(50 * time.Millisecond)
	cancelModule()
	wg.Wait()
}
