package configchange

import (
	"context"
	"fmt"
	"math/rand"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/infra-provider/pkg/scheme"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

func Test_compareConfBetweenCMAndCR(t *testing.T) {
	type args struct {
		comp           cloudv1.ComponentType
		ConfigRevision *cloudv1.ConfigRevision
		configInCM     string
	}
	tests := []struct {
		name      string
		args      args
		wantEqual bool
		wantErr   bool
	}{
		{
			name: "pd only have diff keys 'replication.a' should return equal==true",
			args: args{
				comp: cloudv1.ComponentPD,
				ConfigRevision: &cloudv1.ConfigRevision{
					Spec: cloudv1.ConfigRevisionSpec{
						PD: &cloudv1.Configuration{
							Config: cloudv1.NewTomlConfig(map[string]interface{}{
								"replication": map[string]interface{}{
									"a": "value-a",
								},
							}),
						},
					},
				},
				configInCM: `
[replication]
a = "value-changed"
`,
			},
			wantEqual: true,
			wantErr:   false,
		}, {
			name: "pd only have diff keys 'log.level' should return equal==false",
			args: args{
				comp: cloudv1.ComponentPD,
				ConfigRevision: &cloudv1.ConfigRevision{
					Spec: cloudv1.ConfigRevisionSpec{
						PD: &cloudv1.Configuration{
							Config: cloudv1.NewTomlConfig(map[string]interface{}{
								"log": map[string]interface{}{
									"level": "value-a",
								},
							}),
						},
					},
				},
				configInCM: `
[log]
level = "value-changed"
`,
			},
			wantEqual: false,
			wantErr:   false,
		},
		{
			name: "tikv have diff keys 'replication.a' should return equal==false",
			args: args{
				comp: cloudv1.ComponentTiKV,
				ConfigRevision: &cloudv1.ConfigRevision{
					Spec: cloudv1.ConfigRevisionSpec{
						TiKV: &cloudv1.Configuration{
							Config: cloudv1.NewTomlConfig(map[string]interface{}{
								"replication": map[string]interface{}{
									"a": "value-a",
								},
							}),
						},
					},
				},
				configInCM: `
[replication]
a = "value-changed"
`,
			},
			wantEqual: false,
			wantErr:   false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configKeyInCM := "config-file"
			cm := &corev1.ConfigMap{
				Data: map[string]string{
					configKeyInCM: tt.args.configInCM,
				},
			}
			gotEqual, err := compareConfBetweenCMAndCR(tt.args.ConfigRevision, tt.args.comp, cm, configKeyInCM)
			if (err != nil) != tt.wantErr {
				t.Errorf("compareConfBetweenCMAndCR() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if gotEqual != tt.wantEqual {
				t.Errorf("compareConfBetweenCMAndCR() gotEqual = %v, want %v", gotEqual, tt.wantEqual)
			}
		})
	}
}

func TestConfigChangeReconciler_deleteCCIfExpired(t *testing.T) {
	ccTemplate := cloudv1.ConfigChange{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cc",
			Namespace: "cc-ns",
			Annotations: map[string]string{
				cloudv1.AnnoCCOverwrittenTTL: "30m",
			},
		},
		Spec: cloudv1.ConfigChangeSpec{},
		Status: cloudv1.ConfigChangeStatus{
			Phase: cloudv1.ConfigChangePhaseCompleted,
			EndTime: &metav1.Time{
				Time: time.Now().Add(-DefaultTTL),
			},
		},
	}

	type args struct {
		ccMutator func(cc *cloudv1.ConfigChange)
	}
	tests := []struct {
		name        string
		args        args
		wantErr     bool
		ccDeleted   bool
		ccAssertion assert.ValueAssertionFunc
	}{
		{
			name: "cc is not done",
			args: args{
				ccMutator: func(cc *cloudv1.ConfigChange) {
					cc.Status.Phase = cloudv1.ConfigChangePhaseRunning
				},
			},
			wantErr:   false,
			ccDeleted: false,
		},
		{
			name: "endTime is nil",
			args: args{
				ccMutator: func(cc *cloudv1.ConfigChange) {
					cc.Status.EndTime = nil
				},
			},
			wantErr:   false,
			ccDeleted: false,
			ccAssertion: func(t assert.TestingT, i interface{}, i2 ...interface{}) bool {
				cc := i.(*cloudv1.ConfigChange)
				return cc.Status.EndTime != nil
			},
		},
		{
			name: "use default TTL",
			args: args{
				ccMutator: func(cc *cloudv1.ConfigChange) {
					delete(cc.Annotations, cloudv1.AnnoCCOverwrittenTTL)
				},
			},
			wantErr:   false,
			ccDeleted: true,
		},
		{
			name: "use annotation TTL",
			args: args{
				ccMutator: func(cc *cloudv1.ConfigChange) {
					cc.Annotations[cloudv1.AnnoCCOverwrittenTTL] = (DefaultTTL * 2).String() // cc shouldn't be deleted
				},
			},
			wantErr:   false,
			ccDeleted: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cc := ccTemplate.DeepCopy()
			cc.Name = fmt.Sprintf("%s-%d", cc.Name, rand.Int63())
			if tt.args.ccMutator != nil {
				tt.args.ccMutator(cc)
			}

			cli := newFakeClient([]client.Object{cc})
			ctx := context.Background()

			r := &ConfigChangeReconciler{
				Client: cli,
			}
			if err := r.deleteCCIfExpired(ctx, cc); (err != nil) != tt.wantErr {
				t.Errorf("deleteCCIfExpired() error = %v, wantErr %v", err, tt.wantErr)
			}
			if tt.ccAssertion != nil {
				assert.Truef(t, tt.ccAssertion(t, cc), "cc assertion failed")
			}
			ccDeleted := false
			if err := cli.Get(ctx, client.ObjectKeyFromObject(cc), &cloudv1.ConfigChange{}); err != nil && apierrors.IsNotFound(err) {
				ccDeleted = true
			}
			assert.Equalf(t, tt.ccDeleted, ccDeleted, "cc deleted, expected=%t, actual=%d", tt.ccDeleted, ccDeleted)
		})
	}
}

func newFakeClient(initObjs []client.Object) client.Client {
	cli := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithObjects(initObjs...).
		WithStatusSubresource(initObjs...).
		Build()
	return cli
}
