package server

import (
	"context"
	"net/url"
	"os"
	"testing"

	"buf.build/go/protovalidate"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/go-cmp/cmp/cmpopts"
	"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"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab"
	gapi "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/configuration_project/rpc"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/server_api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/grpctool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/ioz"
	"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/testlogger"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/encoding/protojson"
)

var (
	_ modserver.Factory              = &Factory{}
	_ rpc.ConfigurationProjectServer = &server{}
	_ gitaly.FetchVisitor            = (*configVisitor)(nil)
)

func TestConfigVisitor(t *testing.T) {
	tests := []struct {
		name string
		path string
		file *rpc.AgentConfigFile
	}{
		{
			name: "invalid one-segment path",
			path: "asdfdas",
		},
		{
			name: "valid path",
			path: ".gitlab/agents/my-agent/config.yaml",
			file: &rpc.AgentConfigFile{
				Name:      ".gitlab/agents/my-agent/config.yaml",
				AgentName: "my-agent",
			},
		},
		{
			name: "invalid with only agent config directory",
			path: ".gitlab/agents/my-agent",
		},
		{
			name: "invalid with only agent config directory and trailing slash",
			path: ".gitlab/agents/my-agent/",
		},
		{
			name: "invalid agent name in otherwise valid plath",
			path: ".gitlab/agents/-my-agent-with-invalid-name/config.yaml",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			v := configVisitor{}
			download, _, err := v.Entry(&gitalypb.TreeEntry{
				Path: []byte(tc.path),
			})
			require.NoError(t, err)
			assert.False(t, download)
			var expected []*rpc.AgentConfigFile
			if tc.file != nil {
				expected = []*rpc.AgentConfigFile{tc.file}
			}
			matcher.AssertProtoEqual(t, v.resp, expected, cmpopts.EquateEmpty())
		})
	}
}

func TestAsClient(t *testing.T) {
	kasAddress := os.Getenv("KAS_ADDRESS")
	gitLabAddress := os.Getenv("GITLAB_ADDRESS")
	kasSecretFile := os.Getenv("KAS_SECRET_FILE")
	agentTokenFile := os.Getenv("AGENT_TOKEN_FILE")
	if kasAddress == "" || kasSecretFile == "" || gitLabAddress == "" || agentTokenFile == "" {
		t.SkipNow()
	}
	conn := constructKASConnection(t, kasAddress, kasSecretFile)
	defer conn.Close()
	gitLabC := constructGitLabClient(t, gitLabAddress, kasSecretFile)
	agentToken, err := os.ReadFile(agentTokenFile)
	require.NoError(t, err)

	agentTokenWithType := api.AgentTokenWithType{
		Token: api.AgentToken(agentToken),
		Type:  api.AgentTypeKubernetes,
	}
	a, err := gapi.GetAgentInfo(context.TODO(), gitLabC, agentTokenWithType)
	require.NoError(t, err)

	agentInfo, ok := a.(*server_api.AgentkInfo)
	require.True(t, ok)

	kasC := rpc.NewConfigurationProjectClient(conn)
	configFiles, err := kasC.ListAgentConfigFiles(context.Background(), &rpc.ListAgentConfigFilesRequest{
		Repository: &entity.GitalyRepository{
			StorageName:                   agentInfo.Repository.StorageName,
			RelativePath:                  agentInfo.Repository.RelativePath,
			GitObjectDirectory:            agentInfo.Repository.GitObjectDirectory,
			GitAlternateObjectDirectories: agentInfo.Repository.GitAlternateObjectDirectories,
			GlRepository:                  agentInfo.Repository.GlRepository,
			GlProjectPath:                 agentInfo.Repository.GlProjectPath,
		},
		GitalyInfo: agentInfo.GitalyInfo,
	})
	require.NoError(t, err)
	data, err := protojson.MarshalOptions{
		Multiline: true,
	}.Marshal(configFiles)
	require.NoError(t, err)
	t.Logf("configFiles:\n%s", data)
}

func constructKASConnection(t *testing.T, kasAddress, kasSecretFile string) *grpc.ClientConn {
	v, err := protovalidate.New()
	require.NoError(t, err)
	jwtSecret, err := ioz.LoadSHA256Base64Secret(testlogger.New(t), kasSecretFile)
	require.NoError(t, err)
	u, err := url.Parse(kasAddress)
	require.NoError(t, err)
	opts := []grpc.DialOption{
		grpc.WithChainStreamInterceptor(
			grpctool.StreamClientValidatingInterceptor(v),
		),
		grpc.WithChainUnaryInterceptor(
			grpctool.UnaryClientValidatingInterceptor(v),
		),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithPerRPCCredentials(&grpctool.JWTCredentials{
			SigningMethod: jwt.SigningMethodHS256,
			SigningKey:    jwtSecret,
			Audience:      api.JWTKAS,
			Issuer:        api.JWTKAS,
			Insecure:      true,
		}),
	}
	var addressToDial string
	switch u.Scheme {
	case "grpc":
		addressToDial = "dns:" + u.Host
	default:
		t.Fatalf("unsupported scheme in GitLab Kubernetes Agent Server address: %q", u.Scheme)
	}
	conn, err := grpc.NewClient(addressToDial, opts...)
	require.NoError(t, err)
	return conn
}

func constructGitLabClient(t *testing.T, gitLabAddress, gitLabSecretFile string) *gitlab.Client {
	gitLabURL, err := url.Parse(gitLabAddress)
	require.NoError(t, err)
	secret, err := ioz.LoadSHA256Base64Secret(testlogger.New(t), gitLabSecretFile)
	require.NoError(t, err)
	// Secret for JWT signing
	return gitlab.NewClient(
		gitLabURL,
		secret,
	)
}
