package testhelpers

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

	"buf.build/go/protovalidate"
	"github.com/golang-jwt/jwt/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/retry"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

const (
	KASUserAgent                 = "kas/v0.1-blabla/asdwd"
	AgentToken    api.AgentToken = "123123"
	AuthSecretKey                = "blablabla"

	// Copied from gitlab client package because we don't want to export them

	jwtRequestHeader  = "Gitlab-Kas-Api-Request"
	jwtGitLabAudience = "gitlab"

	ProjectID       int64 = 321
	UserID          int64 = 456
	ConfigProjectID int64 = 5
)

var (
	AgentkKey1 = api.AgentKey{
		ID:   123,
		Type: api.AgentTypeKubernetes,
	}
	AgentkKey2 = api.AgentKey{
		ID:   456,
		Type: api.AgentTypeKubernetes,
	}
	AgentwKey1 = api.AgentKey{
		ID:   1123,
		Type: api.AgentTypeWorkspace,
	}
	AgentwKey2 = api.AgentKey{
		ID:   1456,
		Type: api.AgentTypeWorkspace,
	}
)

// RespondWithJSON marshals response into JSON and writes it into w.
func RespondWithJSON(t *testing.T, w http.ResponseWriter, response any) {
	var data []byte
	var err error
	if m, ok := response.(proto.Message); ok {
		data, err = protojson.Marshal(m)
	} else {
		data, err = json.Marshal(response)
	}
	if !assert.NoError(t, err) {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Header()[httpz.ContentTypeHeader] = []string{"application/json"}
	_, err = w.Write(data)
	assert.NoError(t, err)
}

func AssertRequestMethod(t *testing.T, r *http.Request, method string) {
	assert.Equal(t, method, r.Method)
}

func AssertRequestAccept(t *testing.T, r *http.Request, accept string) {
	assert.Equal(t, accept, r.Header.Get(httpz.AcceptHeader))
}

func AssertRequestUserAgent(t *testing.T, r *http.Request, userAgent string) {
	assert.Equal(t, userAgent, r.Header.Get(httpz.UserAgentHeader))
}

func AssertRequestAcceptJSON(t *testing.T, r *http.Request) {
	AssertRequestAccept(t, r, "application/json")
}

func AssertRequestContentTypeJSON(t *testing.T, r *http.Request) bool {
	return assert.Equal(t, "application/json", r.Header.Get(httpz.ContentTypeHeader))
}

func AssertGetJSONRequest(t *testing.T, r *http.Request) {
	AssertRequestMethod(t, r, http.MethodGet)
	AssertRequestAcceptJSON(t, r)
}

func AssertAgentToken(t *testing.T, r *http.Request, agentToken api.AgentToken) {
	assert.EqualValues(t, agentToken, r.Header.Get(gitlab.AgentTokenHeader))
}

func AssertGetJSONRequestIsCorrect(t *testing.T, r *http.Request, traceID trace.TraceID) {
	AssertRequestAcceptJSON(t, r)
	AssertGetRequestIsCorrect(t, r, traceID)
}

func AssertGetRequestIsCorrect(t *testing.T, r *http.Request, traceID trace.TraceID) {
	AssertRequestMethod(t, r, http.MethodGet)
	AssertAgentToken(t, r, AgentToken)
	assert.Empty(t, r.Header[httpz.ContentTypeHeader])
	AssertCommonRequestParams(t, r, traceID)
	AssertJWTSignature(t, r)
}

func AssertCommonRequestParams(t *testing.T, r *http.Request, traceID trace.TraceID) {
	AssertRequestUserAgent(t, r, KASUserAgent)
	assert.Equal(t, traceID, trace.SpanContextFromContext(r.Context()).TraceID())
}

func AssertJWTSignature(t *testing.T, r *http.Request) {
	_, err := jwt.Parse(r.Header.Get(jwtRequestHeader), func(token *jwt.Token) (any, error) {
		return []byte(AuthSecretKey), nil
	}, jwt.WithAudience(jwtGitLabAudience), jwt.WithIssuer(api.JWTKAS), jwt.WithValidMethods([]string{"HS256"}))
	assert.NoError(t, err)
}

func ReadProtoJSONRequest(t *testing.T, r *http.Request, m proto.Message) bool {
	if !AssertRequestContentTypeJSON(t, r) {
		return false
	}
	req, err := io.ReadAll(r.Body)
	if !assert.NoError(t, err) {
		return false
	}
	err = protojson.Unmarshal(req, m)
	if !assert.NoError(t, err) {
		return false
	}
	err = protovalidate.Validate(m)

	return assert.NoError(t, err)
}

func CtxWithSpanContext(t *testing.T) (context.Context, trace.TraceID) {
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	return InjectSpanContext(t, ctx)
}

func InjectSpanContext(t *testing.T, ctx context.Context) (context.Context, trace.TraceID) {
	var traceID trace.TraceID
	var spanID trace.SpanID
	_, err := rand.Read(traceID[:])
	require.NoError(t, err)
	_, err = rand.Read(spanID[:])
	require.NoError(t, err)

	sc := trace.SpanContext{}.WithTraceID(traceID).WithSpanID(spanID)
	ctx = trace.ContextWithSpanContext(ctx, sc)
	return ctx, traceID
}

func AgentkInfoObj() *api.AgentkInfo {
	return &api.AgentkInfo{
		Key:       AgentkKey1,
		ProjectID: ProjectID,
		Name:      "agent1",
		GitalyInfo: &entity.GitalyInfo{
			Address: "127.0.0.1:123123",
			Token:   "abc",
			Features: map[string]string{
				"bla": "true",
			},
		},
		Repository: &gitalypb.Repository{
			StorageName:        "StorageName",
			RelativePath:       "RelativePath",
			GitObjectDirectory: "GitObjectDirectory",
			GlRepository:       "GlRepository",
			GlProjectPath:      "GlProjectPath",
		},
		DefaultBranch: "main",
	}
}

func AgentwInfoObj() *api.AgentwInfo {
	return &api.AgentwInfo{
		Key: AgentwKey1,
	}
}

func RecvMsg(value any) func(any) error {
	validateProto(value)
	return func(msg any) error {
		SetValue(msg, value)
		return nil
	}
}

// SetValue sets target to value with special handling for proto.Message.
// target must be a pointer. i.e. *blaProtoMsgType
// value must be of the same type as target.
func SetValue(target, value any) {
	if targetMsg, ok := target.(proto.Message); ok {
		validateProto(value)
		proto.Reset(targetMsg)                        // Emulate proto.Unmarshal() - it resets the message before assigning fields.
		proto.Merge(targetMsg, value.(proto.Message)) // proto messages cannot be just copied
	} else {
		reflect.ValueOf(target).Elem().Set(reflect.ValueOf(value).Elem())
	}
}

func NewPollConfig(interval time.Duration) retry.PollConfigFactory {
	return retry.NewPollConfigFactory(interval, retry.NewExponentialBackoffFactory(time.Minute, time.Minute, time.Minute, 2, 5.0))
}

func validateProto(value any) {
	targetMsg, ok := value.(proto.Message)
	if !ok {
		return
	}
	err := protovalidate.Validate(targetMsg)
	if err != nil {
		panic(fmt.Sprintf("validation failed for message %T: %v", targetMsg, err))
	}
	_, err = proto.Marshal(targetMsg)
	if err != nil {
		panic(fmt.Sprintf("proto.Marshal failed for message %T: %v", targetMsg, err))
	}
}
