package kas

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"testing"

	"github.com/getsentry/sentry-go"
	"github.com/stretchr/testify/assert"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	gapi "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modshared"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/server_api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/cache"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_cache"
	"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/testhelpers"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var (
	_ modshared.RPCAPIFactory      = (*serverRPCAPIFactory)(nil).New
	_ modserver.RPCAPI             = (*serverRPCAPI)(nil)
	_ modserver.AgentRPCAPI        = (*serverAgentRPCAPI)(nil)
	_ modserver.AgentRPCAPIFactory = (*serverAgentRPCAPIFactory)(nil).New
)

func TestGetAgentInfo_Errors(t *testing.T) {
	tests := []struct {
		httpStatus int
		code       codes.Code
		captureErr string
	}{
		{
			httpStatus: http.StatusForbidden,
			code:       codes.PermissionDenied,
		},
		{
			httpStatus: http.StatusUnauthorized,
			code:       codes.Unauthenticated,
		},
		{
			httpStatus: http.StatusNotFound,
			code:       codes.NotFound,
		},
		{
			httpStatus: http.StatusInternalServerError,
			captureErr: "HTTP status code: 500 for path /api/v4/internal/agents/agentk/agent_info",
			code:       codes.Unavailable,
		},
		{
			httpStatus: http.StatusBadGateway,
			captureErr: "HTTP status code: 502 for path /api/v4/internal/agents/agentk/agent_info",
			code:       codes.Unavailable,
		},
		{
			httpStatus: http.StatusServiceUnavailable,
			captureErr: "HTTP status code: 503 for path /api/v4/internal/agents/agentk/agent_info",
			code:       codes.Unavailable,
		},
	}
	for _, tc := range tests {
		t.Run(strconv.Itoa(tc.httpStatus), func(t *testing.T) {
			ctx, log, hub, rpcAPI, traceID, _ := setupAgentRPCAPI(t, tc.httpStatus)
			if tc.captureErr != "" {
				hub.EXPECT().
					CaptureEvent(gomock.Any()).
					Do(func(event *sentry.Event) *sentry.EventID {
						assert.Equal(t, traceID.String(), event.Tags[modserver.SentryFieldTraceID])
						assert.Empty(t, event.User.ID)
						assert.Equal(t, sentry.LevelError, event.Level)
						assert.Equal(t, "*gitlab.ClientError", event.Exception[0].Type)
						assert.Equal(t, "AgentInfo(): "+tc.captureErr, event.Exception[0].Value)
						return nil
					})
			}
			info, err := rpcAPI.AgentInfo(ctx, log)
			assert.Equal(t, tc.code, status.Code(err))
			assert.Nil(t, info)
		})
	}
}

func TestRPCHandleProcessingError_UserError(t *testing.T) {
	_, log, _, rpcAPI, _, _ := setupAgentRPCAPI(t, http.StatusInternalServerError)
	err := errz.NewUserError("boom")
	rpcAPI.HandleProcessingError(log, "Bla", err, logz.AgentKey(testhelpers.AgentkKey1))
}

func TestRPCHandleProcessingError_NonUserError_AgentKey(t *testing.T) {
	_, log, hub, rpcAPI, traceID, agentInfoResolver := setupAgentRPCAPI(t, http.StatusInternalServerError)
	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)
	rpcAPI.HandleProcessingError(log, "Bla", err, logz.AgentKey(testhelpers.AgentkKey1))
}

func TestRPCHandleProcessingError_NonUserError_AgentKey_WithExtraData(t *testing.T) {
	_, log, hub, rpcAPI, traceID, agentInfoResolver := setupAgentRPCAPI(t, http.StatusInternalServerError)
	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, api.AgentKeyFieldName: testhelpers.AgentkKey1}, event.Extra)
			return nil
		})

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

func TestRPCHandleProcessingError_NonUserError_NoAgentKey(t *testing.T) {
	_, log, hub, rpcAPI, traceID, _ := setupAgentRPCAPI(t, http.StatusInternalServerError)
	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.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
		})
	rpcAPI.HandleProcessingError(log, "Bla", err)
}

func setupAgentRPCAPI(t *testing.T, statusCode int) (context.Context, *slog.Logger, *MockSentryHub, *serverAgentRPCAPI, trace.TraceID, *mock_modserver.MockAgentInfoResolver) {
	log := testlogger.New(t)
	ctrl := gomock.NewController(t)
	hub := NewMockSentryHub(ctrl)
	agentInfoResolver := mock_modserver.NewMockAgentInfoResolver(ctrl)
	errCacher := mock_cache.NewMockErrCacher[api.AgentTokenWithType](ctrl)
	ctx, traceID := testhelpers.CtxWithSpanContext(t)
	gitLabClient := mock_gitlab.SetupClient(t, "", gapi.AgentkInfoAPIPath, func(w http.ResponseWriter, r *http.Request) {
		testhelpers.AssertGetJSONRequestIsCorrect(t, r, traceID)
		w.WriteHeader(statusCode)
	})
	sra := &serverRPCAPI{
		RPCAPIStub: modshared.RPCAPIStub{
			Logger:    log,
			StreamCtx: ctx,
		},
		sentryHubRoot:     sentry.NewHub(nil, sentry.NewScope()),
		service:           "svc",
		method:            "method",
		agentInfoResolver: agentInfoResolver,
	}
	sra.hub() // so that Once fires here and doesn't overwrite our mock later
	sra.sentryHub = hub

	rpcAPI := &serverAgentRPCAPI{
		RPCAPI: sra,
		TokenWithType: api.AgentTokenWithType{
			Token: testhelpers.AgentToken,
			Type:  api.AgentTypeKubernetes,
		},
		GitLabClient: gitLabClient,
		AgentInfoCache: cache.NewWithError[api.AgentTokenWithType, server_api.AgentInfo](0, 0, errCacher,
			noop.NewTracerProvider().Tracer(kasTracerName),
			func(err error) bool { return false }), // no cache!
	}
	return ctx, log, hub, rpcAPI, traceID, agentInfoResolver
}
