package kas

import (
	"context"
	"errors"
	"log/slog"
	"testing"

	"github.com/getsentry/sentry-go"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/fieldz"
	"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_modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_tunnel_rpc"
	"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/internal/tunnel/rpc"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/proto"
)

var (
	_ modserver.API = (*serverAPI)(nil)
)

func TestHandleProcessingError_UserError(t *testing.T) {
	ctx, log, _, apiObj, _, _ := setupAPI(t)
	err := errz.NewUserError("boom")
	apiObj.HandleProcessingError(ctx, log, "Bla", err, fieldz.AgentKey(testhelpers.AgentkKey1))
}

func TestHandleProcessingError_NonUserError_AgentKey(t *testing.T) {
	ctx, log, hub, apiObj, traceID, agentInfoResolver := setupAPI(t)
	err := errors.New("boom")
	hub.EXPECT().
		CaptureEvent(gomock.Any()).
		Do(func(event *sentry.Event) *sentry.EventID {
			assert.Equal(t, traceID.String(), event.Tags[modserver.SentryFieldTraceID])
			assert.Equal(t, testhelpers.AgentkKey1.String(), event.User.ID)
			assert.Equal(t, sentry.LevelError, event.Level)
			assert.Equal(t, "*errors.errorString", event.Exception[0].Type)
			assert.Equal(t, "Bla: boom", event.Exception[0].Value)
			return nil
		})

	agentInfoResolver.EXPECT().Get(gomock.Any(), testhelpers.AgentkKey1)
	apiObj.HandleProcessingError(ctx, log, "Bla", err, fieldz.AgentKey(testhelpers.AgentkKey1))
}

func TestHandleProcessingError_NonUserError_AgentKey_WithExtraData(t *testing.T) {
	ctx, log, hub, apiObj, traceID, agentInfoResolver := setupAPI(t)
	err := errors.New("boom")
	hub.EXPECT().
		CaptureEvent(gomock.Any()).
		Do(func(event *sentry.Event) *sentry.EventID {
			assert.Equal(t, traceID.String(), event.Tags[modserver.SentryFieldTraceID])
			assert.Equal(t, testhelpers.AgentkKey1.String(), event.User.ID)
			assert.Equal(t, sentry.LevelError, event.Level)
			assert.Equal(t, "*errors.errorString", event.Exception[0].Type)
			assert.Equal(t, "Bla: boom", event.Exception[0].Value)
			assert.Equal(t, map[string]any{"foo": "bar", "any": 2, fieldz.AgentKeyFieldName: testhelpers.AgentkKey1}, event.Extra)
			return nil
		})

	agentInfoResolver.EXPECT().Get(gomock.Any(), testhelpers.AgentkKey1).Return(map[string]any{"foo": "bar", "any": 2}, nil)
	apiObj.HandleProcessingError(ctx, log, "Bla", err, fieldz.AgentKey(testhelpers.AgentkKey1))
}

func TestHandleProcessingError_NonUserError_NoAgentKey_NoTraceID(t *testing.T) {
	_, log, hub, apiObj, _, _ := setupAPI(t)
	err := errors.New("boom")
	hub.EXPECT().
		CaptureEvent(gomock.Any()).
		Do(func(event *sentry.Event) *sentry.EventID {
			assert.NotContains(t, event.Tags, modserver.SentryFieldTraceID)
			assert.Empty(t, event.User.ID)
			assert.Equal(t, sentry.LevelError, event.Level)
			assert.Equal(t, "*errors.errorString", event.Exception[0].Type)
			assert.Equal(t, "Bla: boom", event.Exception[0].Value)
			return nil
		})
	apiObj.HandleProcessingError(context.Background(), log, "Bla", err)
}

func setupAPI(t *testing.T) (context.Context, *slog.Logger, *MockSentryHub, *serverAPI, trace.TraceID, *mock_modserver.MockAgentInfoResolver) {
	log := testlogger.New(t)
	ctrl := gomock.NewController(t)
	hub := NewMockSentryHub(ctrl)
	agentInfoResolver := mock_modserver.NewMockAgentInfoResolver(ctrl)
	ctx, traceID := testhelpers.CtxWithSpanContext(t)
	apiObj := newServerAPI(log, hub, nil, agentInfoResolver)
	return ctx, log, hub, apiObj, traceID, agentInfoResolver
}

func TestRemoveRandomPort(t *testing.T) {
	tests := []struct {
		input    string
		expected string
	}{
		{
			input:    "",
			expected: "",
		},
		{
			input:    "bla",
			expected: "bla",
		},
		{
			input:    "read tcp 10.222.67.20:40272->10.216.1.45:11443: read: connection reset by peer",
			expected: "read tcp 10.222.67.20:x->10.216.1.45:11443: read: connection reset by peer",
		},
		{
			input:    "some error with ip and port 10.222.67.20:40272: bla",
			expected: "some error with ip and port 10.222.67.20:40272: bla",
		},
	}
	for _, tc := range tests {
		t.Run(tc.input, func(t *testing.T) {
			actual := removeRandomPort(tc.input)
			assert.Equal(t, tc.expected, actual)
		})
	}
}

func TestRedisMarshalAndUnmarshal(t *testing.T) {
	mIn := &rpc.GatewayResponse_Header{
		Meta: []*rpc.MetadataKV{
			mock_tunnel_rpc.NewMetadataKV("key", "1", "2"),
		},
	}
	msg, err := redisProtoMarshal(mIn)
	require.NoError(t, err)
	mOut, err := redisProtoUnmarshal(string(msg))
	require.NoError(t, err)

	matcher.AssertProtoEqual(t, mIn, mOut)
}

func TestRedisUnmarshalErr(t *testing.T) {
	_, err := redisProtoUnmarshal("")
	assert.ErrorIs(t, err, proto.Error)
}
