package agent

import (
	"context"
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"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/internal/tool/testing/testlogger"
	"go.uber.org/mock/gomock"
)

func TestFullReconciliationExecution(t *testing.T) {
	logger := testlogger.New(t)
	mockAPI := newMockAPI(t)

	// the following section is being run as a flaky test as it relies on real timers and hence
	// the test results can be flaky. Running the test via runFlakyTest allows the wrapped section
	// to be attempted at most 3 times and the first successful attempt is accepted as test success
	runFlakyTest(t, func(t require.TestingT) {
		// the test will be configured to run for a target number of full reconciliations
		// and the number of intermittent partial reconciliations will be compared to an expected value
		targetFullReconciliationCount := uint32(3)

		// 3 full reconciliations are expected to occur at 0ms, 70ms, 140ms.
		// Between the first and the last full reconciliation, partial reconciliations are expected to occur
		// at 30ms, 60ms, 90ms and 120ms i.e. 4 times
		expectedPartialReconciliationCount := uint32(4)

		fullReconciliationCallCounter := uint32(0)

		ctx, cancel := context.WithCancel(context.Background())
		mock := &mockReconciler{
			partialInterval: 30 * time.Millisecond,
			fullInterval:    70 * time.Millisecond,
		}

		w := &worker{
			log:                                  logger,
			api:                                  mockAPI,
			currentPartialReconciliationInterval: 30 * time.Millisecond,
			currentFullReconciliationInterval:    70 * time.Millisecond,
			reconcilerFactory: func(ctx context.Context, namespace string) (remoteDevReconciler, error) {
				fullReconciliationCallCounter++

				if fullReconciliationCallCounter == targetFullReconciliationCount {
					cancel()
				}

				return mock, nil
			},
		}

		err := w.Run(ctx)
		require.NoError(t, err)

		// mock reconciler will be invoked for every full reconciliation
		// so full reconciliation call counter must be subtracted to get partial reconciliations
		partialReconciliationCallCounter := mock.timesCalled - fullReconciliationCallCounter

		require.Equal(t, expectedPartialReconciliationCount, partialReconciliationCallCounter, "partial reconciliation call count: %d", partialReconciliationCallCounter)
	})
}

func TestWorker_UpdatedWorkerSettings(t *testing.T) {
	tests := []struct {
		description                                    string
		startingFullReconciliationInterval             time.Duration
		expectedFinishingFullReconciliationInterval    time.Duration
		startingPartialReconciliationInterval          time.Duration
		expectedFinishingPartialReconciliationInterval time.Duration
		mockReconciler                                 *mockReconciler
	}{
		{
			description:                                    "full & partial reconciliation intervals updated from api",
			startingFullReconciliationInterval:             10 * time.Millisecond,
			startingPartialReconciliationInterval:          5 * time.Millisecond,
			expectedFinishingFullReconciliationInterval:    9 * time.Millisecond,
			expectedFinishingPartialReconciliationInterval: 9 * time.Millisecond,
			mockReconciler: &mockReconciler{
				fullInterval:    9 * time.Millisecond,
				partialInterval: 9 * time.Millisecond,
			},
		},
		{
			description:                                    "when error occurred, we do not update intervals and keep existing values",
			startingFullReconciliationInterval:             10 * time.Millisecond,
			startingPartialReconciliationInterval:          5 * time.Millisecond,
			expectedFinishingFullReconciliationInterval:    10 * time.Millisecond,
			expectedFinishingPartialReconciliationInterval: 5 * time.Millisecond,
			mockReconciler: &mockReconciler{
				fullInterval:    99 * time.Millisecond,
				partialInterval: 99 * time.Millisecond,
				runError:        errors.New("an error"),
			},
		},
	}

	for _, tt := range tests {
		logger := testlogger.New(t)
		mockAPI := newMockAPI(t)

		t.Run(tt.description, func(t *testing.T) {
			ctx, cancel := context.WithCancel(context.Background())

			w := &worker{
				log:                                  logger,
				api:                                  mockAPI,
				currentFullReconciliationInterval:    tt.startingFullReconciliationInterval,
				currentPartialReconciliationInterval: tt.startingPartialReconciliationInterval,
				reconcilerFactory: func(ctx context.Context, namespace string) (remoteDevReconciler, error) {
					cancel()
					return tt.mockReconciler, nil
				},
			}

			err := w.Run(ctx)

			require.NoError(t, err)
			require.Equal(t, tt.expectedFinishingFullReconciliationInterval, w.currentFullReconciliationInterval)
			require.Equal(t, tt.expectedFinishingPartialReconciliationInterval, w.currentPartialReconciliationInterval)
		})
	}
}

func newMockAPI(t *testing.T) *mock_modagent.MockAPI {
	mockAPI := mock_modagent.NewMockAPI(gomock.NewController(t))
	mockAPI.EXPECT().GetAgentKey(gomock.Any()).AnyTimes()
	mockAPI.EXPECT().HandleProcessingError(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
	mockAPI.EXPECT().
		MakeGitLabRequest(gomock.Any(), "/prerequisites", gomock.Any()).AnyTimes().
		DoAndReturn(func(_ context.Context, _ string, _ ...modagent.GitLabRequestOption) (*modagent.GitLabResponse, error) {
			return &modagent.GitLabResponse{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(strings.NewReader(`{"shared_namespace": ""}`)),
			}, nil
		})

	return mockAPI
}
