package server

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"testing"
	"time"

	"buf.build/go/protovalidate"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	gapi "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/event_tracker"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/matcher"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_event_tracker"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_gitlab"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_otel"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testhelpers"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/kascfg"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/types/known/durationpb"
)

var (
	_ modserver.Module  = &module{}
	_ modserver.Factory = &Factory{}
)

type TestEvent struct {
	UserID    int64
	ProjectID int64
}

func (e TestEvent) DeduplicateKey() string {
	return fmt.Sprintf("%d-%d", e.UserID, e.ProjectID)
}

func TestSendEvents(t *testing.T) {
	t.Parallel()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	events := make(map[string][]event_tracker.Event)
	events["eventName"] = []event_tracker.Event{
		TestEvent{
			UserID:    1,
			ProjectID: 2,
		},
		TestEvent{
			UserID:    4,
			ProjectID: 5,
		},
	}
	events["eventName2"] = []event_tracker.Event{
		TestEvent{
			UserID:    3,
			ProjectID: 6,
		},
	}

	ed := &event_tracker.EventData{Events: events}
	payload := setUpPayload()
	m, tracker, _ := setUpModule(t, func(w http.ResponseWriter, r *http.Request) {
		assertNoContentRequest(t, r, payload)
		w.WriteHeader(http.StatusNoContent)
	})
	assert.Zero(t, m.sentEvents.Load())

	gomock.InOrder(
		tracker.EXPECT().
			GetEventSizeLimit().
			Return(make(chan struct{})),
		tracker.EXPECT().
			CloneEventData().
			Return(ed),
		tracker.EXPECT().
			Subtract(ed),
		tracker.EXPECT().
			CloneEventData().
			DoAndReturn(func() *event_tracker.EventData {
				cancel()
				return &event_tracker.EventData{}
			}),
		tracker.EXPECT().
			CloneEventData().
			Return(&event_tracker.EventData{}),
	)

	require.NoError(t, m.Run(ctx))
	require.EqualValues(t, 3, m.sentEvents.Load())
}

func TestSendEventsFailureAndRetry(t *testing.T) {
	t.Parallel()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	events := make(map[string][]event_tracker.Event)
	events["eventName"] = []event_tracker.Event{
		TestEvent{
			UserID:    1,
			ProjectID: 2,
		},
		TestEvent{
			UserID:    4,
			ProjectID: 5,
		},
	}
	events["eventName2"] = []event_tracker.Event{
		TestEvent{
			UserID:    3,
			ProjectID: 6,
		},
	}
	ed := &event_tracker.EventData{Events: events}
	payload := setUpPayload()
	var call int
	m, tracker, mockAPI := setUpModule(t, func(w http.ResponseWriter, r *http.Request) {
		call++
		switch call {
		case 1:
			assertNoContentRequest(t, r, payload)
			w.WriteHeader(http.StatusInternalServerError)
		case 2:
			assertNoContentRequest(t, r, payload)
			w.WriteHeader(http.StatusNoContent)
		default:
			assert.Fail(t, "unexpected call", call)
		}
	})
	assert.Zero(t, m.sentEvents.Load())

	gomock.InOrder(
		tracker.EXPECT().
			GetEventSizeLimit().
			Return(make(chan struct{})),
		tracker.EXPECT().
			CloneEventData().
			Return(ed),
		tracker.EXPECT().
			Subtract(ed),
		mockAPI.EXPECT().
			HandleProcessingError(gomock.Any(), gomock.Any(), "Failed to send event data", matcher.ErrorEq("HTTP status code: 500 for path /api/v4/internal/kubernetes/agent_events")),
		tracker.EXPECT().
			CloneEventData().
			Return(ed),
		tracker.EXPECT().
			Subtract(ed),
		tracker.EXPECT().
			CloneEventData().
			DoAndReturn(func() *event_tracker.EventData {
				cancel()
				return &event_tracker.EventData{}
			}),
		tracker.EXPECT().
			CloneEventData().
			Return(&event_tracker.EventData{}),
	)

	require.NoError(t, m.Run(ctx))
	assert.EqualValues(t, 3, m.sentEvents.Load())

}

func setUpModule(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*module, *mock_event_tracker.MockEventTrackerInterface, *mock_modserver.MockAPI) {
	ctrl := gomock.NewController(t)
	tracker := mock_event_tracker.NewMockEventTrackerInterface(ctrl)
	mockAPI := mock_modserver.NewMockAPI(ctrl)
	f := Factory{
		EventTracker: tracker,
	}
	config := &kascfg.ConfigurationFile{}
	ApplyDefaults(config)
	config.Observability.EventReportingPeriod = durationpb.New(100 * time.Millisecond)
	meter := mock_otel.NewMockMeter(ctrl)
	meter.EXPECT().
		Int64ObservableCounter(sentEventsCounterName, gomock.Any()).
		Return(nil, nil)
	meter.EXPECT().
		Int64ObservableUpDownCounter(accumulatedEventsCounterName, gomock.Any()).
		Return(nil, nil)
	v, err := protovalidate.New()
	require.NoError(t, err)
	m, err := f.New(&modserver.Config{
		Log:          testlogger.New(t),
		API:          mockAPI,
		Config:       config,
		GitLabClient: mock_gitlab.SetupClient(t, "", gapi.EventAPIPath, handler),
		EventTracker: tracker,
		Meter:        meter,
		Validator:    v,
	})
	require.NoError(t, err)
	return m.(*module), tracker, mockAPI
}

func assertNoContentRequest(t *testing.T, r *http.Request, expectedPayload any) {
	testhelpers.AssertRequestMethod(t, r, http.MethodPost)
	assert.Empty(t, r.Header[httpz.AcceptHeader])
	testhelpers.AssertRequestContentTypeJSON(t, r)
	testhelpers.AssertRequestUserAgent(t, r, testhelpers.KASUserAgent)
	testhelpers.AssertJWTSignature(t, r)
	expectedBin, err := json.Marshal(expectedPayload)
	if !assert.NoError(t, err) {
		return
	}
	var expected any
	err = json.Unmarshal(expectedBin, &expected)
	if !assert.NoError(t, err) {
		return
	}
	actualBin, err := io.ReadAll(r.Body)
	if !assert.NoError(t, err) {
		return
	}
	var actual any
	err = json.Unmarshal(actualBin, &actual)
	if !assert.NoError(t, err) {
		return
	}
	assert.Equal(t, expected, actual)
}

func setUpPayload() (payload map[string]any) {
	payload = map[string]any{}
	payloadEvents := make(map[string][]gapi.Event)
	payloadEvents["eventName"] = []gapi.Event{
		TestEvent{
			UserID:    1,
			ProjectID: 2,
		},
		TestEvent{
			UserID:    4,
			ProjectID: 5,
		},
	}
	payloadEvents["eventName2"] = []gapi.Event{
		TestEvent{
			UserID:    3,
			ProjectID: 6,
		},
	}
	payload["events"] = payloadEvents
	return payload
}
