package agent

import (
	"context"
	"errors"
	"testing"
	"time"

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

var (
	_ modagent.Module[int] = (*leaderModuleWrapper[int])(nil)
)

func TestLMW_DefaultAndValidateConfiguration_IsDelegated(t *testing.T) {
	w, _, m := setupLMW(t)
	c := &agentcfg.AgentConfiguration{}
	m.EXPECT().
		DefaultAndValidateConfiguration(c).
		Return(errors.New("boom"))
	err := w.DefaultAndValidateConfiguration(c)
	assert.EqualError(t, err, "boom")
}

func TestLMW_Name_IsDelegated(t *testing.T) {
	w, _, m := setupLMW(t)
	m.EXPECT().
		Name().
		Return("name1")
	assert.Equal(t, "name1", w.Name())
}

func TestLMW_Run_StartWithConfig(t *testing.T) {
	// GIVEN
	w, r, m := setupLMW(t)
	c1 := &agentcfg.AgentConfiguration{}
	cfg := make(chan *agentcfg.AgentConfiguration, 1)
	complete := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	var stop ModuleStopFunc

	// setup mock expectations
	gomock.InOrder(
		r.EXPECT().
			RunWhenLeader(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, startFn ModuleStartFunc, stopFn ModuleStopFunc) (CancelRunWhenLeaderFunc, error) {
				// we cannot block the caller, because it wouldn't be able to receive the start function.
				stop = stopFn
				go startFn()
				return func() {}, nil
			}),
		m.EXPECT().
			Run(gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctxm context.Context, cfgm <-chan *agentcfg.AgentConfiguration) error {
				c1m := <-cfgm
				assert.Same(t, c1, c1m)

				stop()
				close(cfg)
				<-ctxm.Done()
				close(complete)
				return nil
			}),
	)

	// WHEN
	cfg <- c1
	err := w.Run(ctx, cfg)

	// THEN
	select {
	case <-ctx.Done():
		require.FailNow(t, ctx.Err().Error())
	case <-complete:
	}
	require.NoError(t, err)
	assert.False(t, w.isRunning())
}

func TestLMW_Run_ReceiveNewConfigs(t *testing.T) {
	// GIVEN
	w, r, m := setupLMW(t)
	c1 := &agentcfg.AgentConfiguration{}
	c2 := &agentcfg.AgentConfiguration{}
	c3 := &agentcfg.AgentConfiguration{}
	cfg := make(chan *agentcfg.AgentConfiguration, 1)
	complete := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	var stop ModuleStopFunc

	// setup mock expectations
	gomock.InOrder(
		r.EXPECT().
			RunWhenLeader(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, startFn ModuleStartFunc, stopFn ModuleStopFunc) (CancelRunWhenLeaderFunc, error) {
				// we cannot block the caller, because it wouldn't be able to receive the start function.
				stop = stopFn
				go startFn()
				return func() {}, nil
			}),
		m.EXPECT().
			Run(gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctxm context.Context, cfgm <-chan *agentcfg.AgentConfiguration) error {
				c1m := <-cfgm
				assert.Same(t, c1, c1m)

				cfg <- c2
				c2m := <-cfgm
				assert.Same(t, c2, c2m)

				cfg <- c3
				c3m := <-cfgm
				assert.Same(t, c3, c3m)

				stop()
				close(cfg)
				<-ctxm.Done()
				close(complete)
				return nil
			}),
	)

	// WHEN
	cfg <- c1
	err := w.Run(ctx, cfg)

	// THEN
	select {
	case <-ctx.Done():
		require.FailNow(t, ctx.Err().Error())
	case <-complete:
	}
	require.NoError(t, err)
	assert.False(t, w.isRunning())
}

func TestLMW_Run_EarlyReturnNoError_Panics(t *testing.T) {
	// GIVEN
	w, r, m := setupLMW(t)
	c1 := &agentcfg.AgentConfiguration{}
	cfg := make(chan *agentcfg.AgentConfiguration, 1)

	complete := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// setup mock expectations
	gomock.InOrder(
		r.EXPECT().
			RunWhenLeader(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, startFn ModuleStartFunc, stopFn ModuleStopFunc) (CancelRunWhenLeaderFunc, error) {
				// we cannot block the caller, because it wouldn't be able to receive the start function.
				go startFn()
				return func() {}, nil
			}),
		m.EXPECT().
			Run(gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, cfgm <-chan *agentcfg.AgentConfiguration) error {
				c1m := <-cfgm
				assert.Same(t, c1, c1m)
				close(complete)
				return nil
			}),
		m.EXPECT().Name().Return("test-module"),
	)

	// WHEN
	cfg <- c1

	// THEN
	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() {
		w.Run(ctx, cfg)
	})
}

func TestLMW_Run_EarlyReturnError(t *testing.T) {
	// GIVEN
	w, r, m := setupLMW(t)
	c1 := &agentcfg.AgentConfiguration{}
	cfg := make(chan *agentcfg.AgentConfiguration, 1)

	complete := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// setup mock expectations
	gomock.InOrder(
		r.EXPECT().
			RunWhenLeader(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, startFn ModuleStartFunc, stopFn ModuleStopFunc) (CancelRunWhenLeaderFunc, error) {
				// we cannot block the caller, because it wouldn't be able to receive the start function.
				go startFn()
				return func() {}, nil
			}),
		m.EXPECT().
			Run(gomock.Any(), gomock.Any()).
			DoAndReturn(func(ctx context.Context, cfgm <-chan *agentcfg.AgentConfiguration) error {
				close(complete)
				return errors.New("boom")
			}),
	)

	// WHEN
	cfg <- c1
	err := w.Run(ctx, cfg)

	// THEN
	select {
	case <-ctx.Done():
		require.FailNow(t, ctx.Err().Error())
	case <-complete:
	}
	assert.EqualError(t, err, "boom")
	assert.False(t, w.isRunning())
}

func setupLMW(t *testing.T) (*leaderModuleWrapper[*agentcfg.AgentConfiguration], *MockRunner, *mock_modagent.MockModule[*agentcfg.AgentConfiguration]) {
	ctrl := gomock.NewController(t)
	m := mock_modagent.NewMockModule[*agentcfg.AgentConfiguration](ctrl)
	r := NewMockRunner(ctrl)
	w := newLeaderModuleWrapper(m, r)
	return w, r, m
}
