// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validate

import (
	"testing"

	envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
	envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	envoy_aggregate_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/envoyextensions/extensioncommon"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/types/known/anypb"
)

func TestErrors(t *testing.T) {
	cases := map[string]struct {
		validate          func() *Validate
		endpointValidator EndpointValidator
		err               string
	}{
		"success": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"db-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"db-sni": {
							required: true,
							cluster:  true,
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
				r.endpoints = 1
			},
		},
		"no clusters for listener or route": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"db-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"db-sni": {
							required: false,
							cluster:  true,
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
				r.endpoints = 1
			},
			err: "No clusters found on route or listener",
		},
		"no healthy endpoints": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"db-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"db-sni": {
							required: true,
							cluster:  true,
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
			},
			err: "No healthy endpoints for cluster \"db-sni\" for upstream \"db\"",
		},
		"success: aggregate cluster with one target with endpoints": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"db-sni":        {},
						"db-fail-1-sni": {},
						"db-fail-2-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"db-sni": {
							required:         true,
							cluster:          true,
							aggregateCluster: true,
							aggregateClusterChildren: []string{
								"db-fail-1-sni",
								"db-fail-2-sni",
							},
						},
						"db-fail-1-sni": {
							required:      true,
							cluster:       true,
							parentCluster: "db-sni",
							// This doesn't usually get set here, but this tests that at least one child cluster has
							// healthy endpoints case.
							endpoints: 1,
						},
						"db-fail-2-sni": {
							required:      true,
							cluster:       true,
							parentCluster: "db-sni",
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
			},
		},
		"aggregate cluster no healthy endpoints": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"db-sni":        {},
						"db-fail-1-sni": {},
						"db-fail-2-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"db-sni": {
							required:         true,
							cluster:          true,
							aggregateCluster: true,
							aggregateClusterChildren: []string{
								"db-fail-1-sni",
								"db-fail-2-sni",
							},
						},
						"db-fail-1-sni": {
							required:      true,
							cluster:       true,
							parentCluster: "db-sni",
						},
						"db-fail-2-sni": {
							required:      true,
							cluster:       true,
							parentCluster: "db-sni",
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
				r.endpoints = 0
			},
			err: "No healthy endpoints for aggregate cluster \"db-sni\" for upstream \"db\"",
		},
		"success: passthrough cluster doesn't error even though there are zero endpoints": {
			validate: func() *Validate {
				return &Validate{
					envoyID: "db",
					snis: map[string]struct{}{
						"passthrough~db-sni": {},
					},
					listener: true,
					usesRDS:  true,
					route:    true,
					resources: map[string]*resource{
						"passthrough~db-sni": {
							required: true,
							cluster:  true,
						},
					},
				}
			},
			endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
				r.loadAssignment = true
			},
		},
	}

	for n, tc := range cases {
		t.Run(n, func(t *testing.T) {
			v := tc.validate()
			messages := v.GetMessages(true, tc.endpointValidator, nil)

			var outputErrors string
			for _, msgError := range messages.Errors() {
				outputErrors += msgError.Message
				for _, action := range msgError.PossibleActions {
					outputErrors += action
				}
			}
			if tc.err == "" {
				require.True(t, messages.Success())
			} else {
				require.Contains(t, outputErrors, tc.err)
			}

		})
	}

}

func TestIsAggregateCluster(t *testing.T) {
	aggregateClusterConfig, err := anypb.New(&envoy_aggregate_cluster_v3.ClusterConfig{
		Clusters: []string{"c1", "c2"},
	})
	require.NoError(t, err)

	cases := map[string]struct {
		input                    *envoy_cluster_v3.Cluster
		expectedAggregateCluster *envoy_aggregate_cluster_v3.ClusterConfig
		expectedOk               bool
	}{
		"non-aggregate cluster": {
			input: &envoy_cluster_v3.Cluster{
				Name:                 "foo",
				ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_LOGICAL_DNS},
			},
			expectedOk: false,
		},
		"valid aggregate cluster": {
			input: &envoy_cluster_v3.Cluster{
				Name: "foo",
				ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
					ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{
						Name:        "foo",
						TypedConfig: aggregateClusterConfig,
					},
				},
			},
			expectedOk:               true,
			expectedAggregateCluster: &envoy_aggregate_cluster_v3.ClusterConfig{Clusters: []string{"c1", "c2"}},
		},
	}
	for n, tc := range cases {
		t.Run(n, func(t *testing.T) {
			ac, ok := isAggregateCluster(tc.input)
			require.Equal(t, tc.expectedOk, ok)

			if tc.expectedOk {
				require.Equal(t, tc.expectedAggregateCluster.Clusters, ac.Clusters)
			}

		})
	}
}

func TestMakeValidate(t *testing.T) {

	cases := map[string]struct {
		extensionName string
		arguments     map[string]interface{}
		expected      *Validate
		snis          map[string]struct{}
		ok            bool
	}{
		"with no arguments": {
			arguments: nil,
			ok:        false,
		},
		"with an invalid name": {
			arguments: map[string]interface{}{
				"envoyID": "id",
			},
			snis: map[string]struct{}{
				"sni1": {},
				"sni2": {},
			},
			extensionName: "bad",
			ok:            false,
		},
		"empty envoy ID": {
			arguments: map[string]interface{}{"envoyID": ""},
			snis: map[string]struct{}{
				"sni1": {},
				"sni2": {},
			},
			ok: false,
		},
		"missing snis": {
			arguments: map[string]interface{}{
				"envoyID": "id",
			},
			expected: &Validate{
				envoyID:   "id",
				resources: map[string]*resource{},
				snis: map[string]struct{}{
					"sni1": {},
					"sni2": {},
				},
			},
			ok: false,
		},
		"valid everything": {
			arguments: map[string]interface{}{
				"envoyID": "id",
			},
			snis: map[string]struct{}{
				"sni1": {},
				"sni2": {},
			},
			expected: &Validate{
				envoyID:   "id",
				resources: map[string]*resource{},
				snis: map[string]struct{}{
					"sni1": {},
					"sni2": {},
				},
			},
			ok: true,
		},
	}

	for n, tc := range cases {
		t.Run(n, func(t *testing.T) {

			extensionName := api.BuiltinValidateExtension
			if tc.extensionName != "" {
				extensionName = tc.extensionName
			}

			svc := api.CompoundServiceName{Name: "svc"}
			ext := extensioncommon.RuntimeConfig{
				ServiceName: svc,
				EnvoyExtension: api.EnvoyExtension{
					Name:      extensionName,
					Arguments: tc.arguments,
				},
				Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
					svc: {
						SNIs: tc.snis,
					},
				},
			}

			patcher, err := MakeValidate(ext)

			if tc.ok {
				require.NoError(t, err)
				require.Equal(t, tc.expected, patcher)
			} else {
				require.Error(t, err)
			}
		})
	}
}
