package config

import (
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
	"gopkg.in/yaml.v3"

	apiproxyv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-sidecar/apiproxy/v1"
	secretstorev1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-sidecar/secretstore/v1"
)

func TestExampleConfigIsValid(t *testing.T) {
	t.Parallel()
	data, err := os.ReadFile("../config.example.yaml")
	require.NoError(t, err)
	var conf RootConfig
	require.NoError(t, yaml.Unmarshal(data, &conf))
	conf.Normalize()
	require.NoError(t, conf.Validate())
}

func TestAPIProxyConfigValidation(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name      string
		mutate    func(*RootConfig)
		wantError string
	}{
		{name: "valid"},
		{name: "unknown secret source", mutate: func(conf *RootConfig) {
			secret := conf.Services.SecretStore.Runtimes["devops-prod"]
			secret.Source.Type = "missing"
			conf.Services.SecretStore.Runtimes["devops-prod"] = secret
		}, wantError: "unsupported source type"},
		{name: "staleness shorter than interval", mutate: func(conf *RootConfig) {
			secret := conf.Services.SecretStore.Runtimes["devops-prod"]
			secret.Refresh.MaxStaleness = time.Minute
			conf.Services.SecretStore.Runtimes["devops-prod"] = secret
		}, wantError: "must not be shorter"},
		{name: "routes without listener", mutate: func(conf *RootConfig) {
			conf.Services.APIProxy.Listeners = nil
		}, wantError: "listener must be configured"},
		{name: "nested route prefixes", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Prefix = "/devops/prod/v2"
			conf.Services.APIProxy.Routes["devops-v2"] = route
		}},
		{name: "duplicate route prefixes", mutate: func(conf *RootConfig) {
			conf.Services.APIProxy.Routes["second"] = conf.Services.APIProxy.Routes["devops-prod"]
		}, wantError: "duplicate route prefix"},
		{name: "non HTTPS upstream", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Upstream.BaseURL = "http://prod.devops.pingcap.com"
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "HTTPS"},
		{name: "invalid socket directory", mutate: func(conf *RootConfig) {
			listener := conf.Services.APIProxy.Listeners["default"]
			listener.Address = "/tmp/api-proxy.sock"
			conf.Services.APIProxy.Listeners["default"] = listener
		}, wantError: APIProxyRuntimeDir},
		{name: "forbidden header", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.AllowedRequestHeaders = []string{"Authorization"}
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "forbidden"},
		{name: "missing operation", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Middlewares[0].Config.Allow = nil
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "allowed operations"},
		{name: "middleware order", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Middlewares[0], route.Middlewares[1] = route.Middlewares[1], route.Middlewares[0]
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "must run before"},
		{name: "unsupported middleware", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Middlewares[0].Type = "custom"
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "unsupported middleware"},
		{name: "undeclared host parameter", mutate: func(conf *RootConfig) {
			route := conf.Services.APIProxy.Routes["devops-prod"]
			route.Upstream = APIProxyUpstreamConfig{
				Scheme:       "https",
				HostTemplate: "api.{region}.example.com",
				HostParameters: map[string]APIProxyHostParameterConfig{
					"region": {Source: APIProxyHostPathSegment, AllowedValues: []string{"us-west-2"}},
					"extra":  {Source: APIProxyHostPathSegment, AllowedValues: []string{"value"}},
				},
			}
			conf.Services.APIProxy.Routes["devops-prod"] = route
		}, wantError: "undeclared"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			conf := validProxyConfig()
			if test.mutate != nil {
				test.mutate(&conf)
			}
			err := conf.Validate()
			if test.wantError == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, test.wantError)
			}
		})
	}
}

func TestTemplatedUpstreamConfigIsValid(t *testing.T) {
	t.Parallel()
	conf := validProxyConfig()
	route := conf.Services.APIProxy.Routes["devops-prod"]
	route.Upstream = APIProxyUpstreamConfig{
		Scheme:       "https",
		HostTemplate: "api.{region}.{vendor}.example.com",
		HostParameters: map[string]APIProxyHostParameterConfig{
			"region": {Source: APIProxyHostPathSegment, AllowedValues: []string{"us-west-2"}},
			"vendor": {Source: APIProxyHostPathSegment, AllowedValues: []string{"aws"}},
		},
	}
	conf.Services.APIProxy.Routes["devops-prod"] = route
	require.NoError(t, conf.Validate())
}

func TestMultiSegmentOperationTemplateIsValid(t *testing.T) {
	t.Parallel()
	conf := validProxyConfig()
	route := conf.Services.APIProxy.Routes["devops-prod"]
	route.Middlewares[0].Config.Allow = []APIProxyOperationConfig{
		{Method: "POST", PathTemplate: "/model/{modelId...}/invoke"},
	}
	conf.Services.APIProxy.Routes["devops-prod"] = route
	require.NoError(t, conf.Validate())

	route.Middlewares[0].Config.Allow[0].PathTemplate = "/{owner...}/{repository...}/git-upload-pack"
	conf.Services.APIProxy.Routes["devops-prod"] = route
	require.ErrorContains(t, conf.Validate(), "at most one")
}

func TestConfigMapsToContractConcepts(t *testing.T) {
	t.Parallel()

	conf := validProxyConfig()
	secret := conf.Services.SecretStore.Runtimes["devops-prod"]
	route := conf.Services.APIProxy.Routes["devops-prod"]
	operationPolicy := route.Middlewares[0]
	credentialAuth := route.Middlewares[1]

	require.Equal(t, secretstorev1.SecretFormat_SECRET_FORMAT_JSON, secret.Source.AWSSecretsManager.ContractFormat())
	require.Equal(t, apiproxyv1.AuthType_AUTH_TYPE_HEADER, credentialAuth.Config.Candidates[0].ContractType())
	require.True(t, proto.Equal(&apiproxyv1.Operation{
		Method:      "GET",
		PathMatcher: &apiproxyv1.Operation_PathPrefix{PathPrefix: "/api/v1/"},
	}, operationPolicy.Config.Allow[0].ContractOperation()))
}

func validProxyConfig() RootConfig {
	conf := Default()
	conf.Services.SecretStore = SecretStoreServiceConfig{
		Runtimes: map[string]RuntimeConfig{
			"devops-prod": {
				Source: SecretSourceConfig{
					Type: SecretSourceAWSSecretsManager,
					AWSSecretsManager: &AWSSecretsManagerSecretSourceConfig{
						Region: "us-west-2", SecretID: "example/devops", Format: SecretFormatJSON,
					},
				},
				Refresh: SecretRefreshConfig{Interval: 5 * time.Minute, MaxStaleness: time.Hour},
			},
		},
	}
	conf.Services.APIProxy = APIProxyServiceConfig{
		Listeners: map[string]APIProxyListenerConfig{"default": {Network: APIProxyNetworkUnix, Address: "/run/agent-sidecar/api-proxy.sock", Mode: "0660"}},
		Routes: map[string]APIProxyRouteConfig{
			"devops-prod": {
				ListenerID: "default",
				Prefix:     "/devops/prod",
				Upstream:   APIProxyUpstreamConfig{BaseURL: "https://prod.devops.pingcap.com"},
				Middlewares: []APIProxyMiddlewareConfig{
					operationPolicyMiddleware(APIProxyOperationConfig{Method: "GET", PathPrefix: "/api/v1/"}),
					credentialAuthMiddleware(APIProxyAuthCandidateConfig{
						Type: APIProxyAuthHeader, HeaderName: "X-API-TOKEN", ValueFrom: APIProxySecretRefConfig{Secret: "devops-prod", Field: "apiToken"},
					}),
				},
			},
		},
	}
	return conf
}

func operationPolicyMiddleware(operations ...APIProxyOperationConfig) APIProxyMiddlewareConfig {
	return APIProxyMiddlewareConfig{
		Type:   APIProxyMiddlewareOperationPolicy,
		Config: APIProxyMiddlewareParamsConfig{Allow: operations},
	}
}

func credentialAuthMiddleware(candidates ...APIProxyAuthCandidateConfig) APIProxyMiddlewareConfig {
	return APIProxyMiddlewareConfig{
		Type:   APIProxyMiddlewareCredentialAuth,
		Config: APIProxyMiddlewareParamsConfig{Candidates: candidates},
	}
}
