package operate

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	v1 "github.com/tidbcloud/infra-component/api/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestSelectComponent(t *testing.T) {
	testCases := []struct {
		name              string
		componentset      v1.ComponentSet
		desc              v1.Description
		wantErrs          bool
		wantComponentSpec *v1.ComponentSpec
	}{
		{
			name:              "empty component set",
			wantComponentSpec: nil,
		},
		{
			name: "cluster version too small",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								ClusterSelector: []v1.MatchingExpression{{Key: "version", Operator: v1.Greater, Value: "v5.1.1"}},
							},
						},
					},
				},
			},
			desc:              v1.Description{Cluster: map[string]interface{}{"version": "v5.0.0"}},
			wantComponentSpec: nil,
			wantErrs:          false,
		},
		{
			name: "cluster component selected",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								ClusterSelector: []v1.MatchingExpression{{Key: "version", Operator: v1.Greater, Value: "v5.1.1"}},
							},
						},
					},
				},
			},
			desc: v1.Description{Cluster: map[string]interface{}{"version": "v5.2.0"}},
			wantComponentSpec: &v1.ComponentSpec{
				Version: "v1.0.0",
				Selector: v1.ComponentSelector{
					ClusterSelector: []v1.MatchingExpression{{Key: "version", Operator: v1.Greater, Value: "v5.1.1"}},
				},
			},
			wantErrs: false,
		},
		{
			name: "cluster description matched, network description unmatched",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								ClusterSelector: []v1.MatchingExpression{{Key: "version", Operator: v1.Greater, Value: "v5.1.1"}},
								NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
							},
						},
					},
				},
			},
			desc:              v1.Description{Cluster: map[string]interface{}{"version": "v5.2.0"}},
			wantComponentSpec: nil,
			wantErrs:          true,
		},
		{
			name: "network component selected",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
							},
						},
					},
				},
			},
			desc: v1.Description{Network: map[string]interface{}{"labels": []string{"tags.tidbcloud.com", "pingcap.com"}}},
			wantComponentSpec: &v1.ComponentSpec{
				Version: "v1.0.0",
				Selector: v1.ComponentSelector{
					NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
				},
			},
			wantErrs: false,
		},
		{
			name: "network description matched, cluster description unmatched",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								ClusterSelector: []v1.MatchingExpression{{Key: "version", Operator: v1.Greater, Value: "v5.1.1"}},
								NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
							},
						},
					},
				},
			},
			desc:              v1.Description{Network: map[string]interface{}{"labels": []string{"tags.tidbcloud.com", "pingcap.com"}}},
			wantComponentSpec: nil,
			wantErrs:          true,
		},
		{
			name: "select multi-versioned components",
			componentset: v1.ComponentSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: v1.ComponentSetSpec{
					Versions: []v1.ComponentSpec{
						{
							Version: "v1.1.0",
							Selector: v1.ComponentSelector{
								NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
							},
						},
						{
							Version: "v1.0.0",
							Selector: v1.ComponentSelector{
								NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
							},
						},
					},
				},
			},
			desc: v1.Description{Network: map[string]interface{}{"labels": []string{"tags.tidbcloud.com", "pingcap.com"}}},
			wantComponentSpec: &v1.ComponentSpec{
				Version: "v1.1.0",
				Selector: v1.ComponentSelector{
					NetworkSelector: []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "tags.tidbcloud.com"}},
				},
			},
			wantErrs: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			component, errs := SelectComponent(tc.componentset, tc.desc)
			gotErrs := len(errs) > 0
			if gotErrs != tc.wantErrs {
				t.Fatalf("SelectComponent got errors = %t, want = %t, errs = %v", gotErrs, tc.wantErrs, errs)
			}
			var componentSpec *v1.ComponentSpec
			if component != nil {
				componentSpec = &component.Spec
			}
			if diff := cmp.Diff(componentSpec, tc.wantComponentSpec); diff != "" {
				t.Fatalf("SelectComponent diff(got, want) = %s", diff)
			}
		})
	}
}

func TestEvalExpressions(t *testing.T) {
	testCases := []struct {
		name      string
		exps      []v1.MatchingExpression
		desc      map[string]interface{}
		wantValue bool
		wantError bool
	}{
		{
			name:      "key not present",
			exps:      []v1.MatchingExpression{{Operator: v1.Equal, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"version": "v1.0.0"},
			wantValue: false,
			wantError: true,
		},
		{
			name:      "equal expression",
			exps:      []v1.MatchingExpression{{Key: "version", Operator: v1.Equal, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"version": "v1.0.0"},
			wantValue: true,
			wantError: false,
		},
		{
			name:      "not equal expression",
			exps:      []v1.MatchingExpression{{Key: "version", Operator: v1.NotEqual, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"version": "v1.0.0"},
			wantValue: false,
			wantError: false,
		},
		{
			name: "int compare expression",
			exps: []v1.MatchingExpression{
				{Key: "replicas", Operator: v1.Greater, Value: "1"},
				{Key: "replicas", Operator: v1.GreaterOrEqual, Value: "2"},
				{Key: "replicas", Operator: v1.Less, Value: "3"},
				{Key: "replicas", Operator: v1.LessOrEqual, Value: "2"},
			},
			desc:      map[string]interface{}{"replicas": 2},
			wantValue: true,
			wantError: false,
		},
		{
			name: "semver compare expression",
			exps: []v1.MatchingExpression{
				{Key: "version", Operator: v1.Greater, Value: "v1.0.0"},
				{Key: "version", Operator: v1.GreaterOrEqual, Value: "v2.0.0"},
				{Key: "version", Operator: v1.Less, Value: "v3.0.0"},
				{Key: "version", Operator: v1.LessOrEqual, Value: "v2.0.0"},
			},
			desc:      map[string]interface{}{"version": "v2.0.0"},
			wantValue: true,
			wantError: false,
		},
		{
			name: "string compare expression",
			exps: []v1.MatchingExpression{
				{Key: "kind", Operator: v1.Greater, Value: "apple"},
				{Key: "kind", Operator: v1.GreaterOrEqual, Value: "apple"},
				{Key: "kind", Operator: v1.Less, Value: "watermelon"},
				{Key: "kind", Operator: v1.LessOrEqual, Value: "watermelon"},
			},
			desc:      map[string]interface{}{"kind": "blueberry"},
			wantValue: true,
			wantError: false,
		},
		{
			name:      "include expression",
			exps:      []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"labels": []string{"v1.0.0", "v1.1.0"}},
			wantValue: true,
			wantError: false,
		},
		{
			name:      "not include expression",
			exps:      []v1.MatchingExpression{{Key: "labels", Operator: v1.NotInclude, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"labels": []string{"v1.0.0", "v1.1.0"}},
			wantValue: false,
			wantError: false,
		},
		{
			name:      "include expression type not matched",
			exps:      []v1.MatchingExpression{{Key: "labels", Operator: v1.Include, Value: "v1.0.0"}},
			desc:      map[string]interface{}{"labels": "v1.0.0"},
			wantValue: false,
			wantError: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			value, err := evalExpressions(tc.exps, tc.desc)
			gotErr := err != nil
			if gotErr != tc.wantError {
				t.Fatalf("evalExpression got error = %t, want error = %t", gotErr, tc.wantError)
			}
			if value != tc.wantValue {
				t.Fatalf("evalExpression got = %v, want = %v", value, tc.wantValue)
			}
		})
	}
}
