package backup

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/tidbcloud/config-service/sdk/api"
	"github.com/tidbcloud/config-service/sdk/mock"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	. "github.com/onsi/gomega"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/test"
	"github.com/tidbcloud/infra-api/pkg/consts"
)

func TestCreateBackup(t *testing.T) {
	g := NewGomegaWithT(t)

	mockCli := &test.MockClient{
		MockGet: test.NewMockGetFn(nil),
		MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
			mr, _ := obj.(*cloudv1.Backup)
			status := mr.Status.Phase
			g.Expect(status).Should(Equal(cloudv1.BackupPrepare))

			return nil
		}),
		MockCreate: test.MockCreateFn(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
			return nil
		}),
		MockSubresourceUpdate: test.NewMockSubresourceUpdateFn(nil),
		MockSubresourcePatch:  test.NewMockSubresourcePatchFn(nil),
	}

	mockClusterCli := &test.MockClient{
		MockGet: test.NewMockGetFn(nil),
		MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
			tidbBackup, _ := obj.(*v1alpha1.Backup)

			g.Expect(tidbBackup.Spec.S3.Region).Should(Equal("us-west-2"))
			g.Expect(tidbBackup.Spec.S3.Bucket).Should(Equal("test-bucket"))
			g.Expect(tidbBackup.Spec.S3.Prefix).Should(Equal("backup1"))
			g.Expect(tidbBackup.ObjectMeta.Namespace).Should(Equal("tidb1"))

			if tidbBackup.Spec.Mode == v1alpha1.BackupModeLog {
				g.Expect(tidbBackup.Spec.CommitTs).ShouldNot(Equal(""))
			}

			return nil
		},
	}
	cb := fakeClusterBackup(mockCli, mockClusterCli)
	backup := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "backup1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.BackupSpec{
			ClusterName: "tidb1",
		},
	}
	f := backup.GetFinalizers()
	backup.SetFinalizers(append(f, BackupFinalizer))

	err := cb.Create(context.TODO(), backup)
	g.Expect(err).Should(Succeed())

	storage := "gs://test"
	backupOfCustomBucket := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "backup2",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.BackupSpec{
			ClusterName: "tidb1",
			Storage:     &storage,
		},
	}

	err = cb.Create(context.TODO(), backupOfCustomBucket)
	g.Expect(err).Should(Succeed())

	logBackupMode := cloudv1.BackupModeLog
	startTS := "123"
	logBackup := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "backup3",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.BackupSpec{
			ClusterName: "tidb1",
			Mode:        &logBackupMode,
			CommitTS:    &startTS,
		},
	}
	err = cb.Create(context.TODO(), logBackup)
	g.Expect(err).Should(Succeed())

	// TODO: should we add more assertions here?
}

func TestUpdateLogBackup(t *testing.T) {
	g := NewGomegaWithT(t)
	var (
		actualLogCheckpointTs  = "ts1"
		actualLogTruncateUntil = "ts2"
		actualBackupPath       = "path1"

		expectedLogTruncateUntil = "ts4"
		expectedLogStop          = true
	)

	mockCli := &test.MockClient{
		MockSubresourceUpdate: test.MockSubresourceUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
			backup, ok := obj.(*cloudv1.Backup)
			g.Expect(ok).Should(BeTrue())
			g.Expect(backup.Status.BackupPath).Should(Equal(actualBackupPath))
			g.Expect(backup.Status.LogCheckpointTs).Should(Equal(actualLogCheckpointTs))
			g.Expect(backup.Status.LogTruncateUntil).Should(Equal(actualLogTruncateUntil))
			return nil
		}),
	}

	mockClusterCli := &test.MockClient{
		MockGet: test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
			tidbBackup, ok := obj.(*v1alpha1.Backup)
			g.Expect(ok).Should(BeTrue())
			tidbBackup.Name = key.Name
			tidbBackup.Namespace = key.Namespace
			tidbBackup.Status = v1alpha1.BackupStatus{
				BackupPath:              actualBackupPath,
				LogCheckpointTs:         actualLogCheckpointTs,
				LogSuccessTruncateUntil: actualLogTruncateUntil,
			}
			return nil
		}),
		MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
			tidbBackup, ok := obj.(*v1alpha1.Backup)
			g.Expect(ok).Should(BeTrue())
			g.Expect(tidbBackup.Spec.LogTruncateUntil).Should(Equal(expectedLogTruncateUntil))
			g.Expect(tidbBackup.Spec.LogStop).Should(Equal(expectedLogStop))
			return nil
		}),
	}

	cb := fakeClusterBackup(mockCli, mockClusterCli)
	logBackupMode := cloudv1.BackupModeLog
	logBackup := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "backup3",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.BackupSpec{
			ClusterName:      "tidb1",
			Mode:             &logBackupMode,
			LogTruncateUntil: &expectedLogTruncateUntil,
			LogStop:          &expectedLogStop,
		},
	}

	g.Expect(cb.Update(context.TODO(), logBackup)).Should(Succeed())
}

func TestBackupUseNodeGroup(t *testing.T) {
	snapshotMode := cloudv1.BackupModeSnapshot
	logMode := cloudv1.BackupModeLog
	invalidMode := cloudv1.BackupMode("invalid")
	testcases := []struct {
		name           string
		clusterBackup  *clusterBackup
		backup         *cloudv1.Backup
		expectedResult bool
	}{
		{
			name:           "cluster and network of cluster backup is nil",
			clusterBackup:  &clusterBackup{},
			expectedResult: false,
		},
		{
			name: "cluster of cluster backup is nil",
			clusterBackup: &clusterBackup{
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &snapshotMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: true,
		},
		{
			name: "network of cluster backup is nil",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
			},
			expectedResult: false,
		},
		{
			name: "snapshot backup without resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &snapshotMode,
				},
			},
			expectedResult: false,
		},
		{
			name: "snapshot backup with small resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &snapshotMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("100Mi"),
						},
					},
				},
			},
			expectedResult: false,
		},
		{
			name: "snapshot backup with big resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &snapshotMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: true,
		},
		{
			name: "log backup start with small resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &logMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("100Mi"),
						},
					},
				},
			},
			expectedResult: false,
		},
		{
			name: "log backup start with big resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &logMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: true,
		},
		{
			name: "log backup stop with big resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode:    &logMode,
					LogStop: pointer.Bool(true),
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: true,
		},
		{
			name: "log backup truncate with big resource",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode:             &logMode,
					LogTruncateUntil: pointer.String("123"),
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: true,
		},
		{
			name: "backup without mode",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: false,
		},
		{
			name: "backup with other mode",
			clusterBackup: &clusterBackup{
				cluster: &cloudv1.Cluster{},
				network: &cloudv1.Network{},
			},
			backup: &cloudv1.Backup{
				Spec: cloudv1.BackupSpec{
					Mode: &invalidMode,
					ResourceRequirements: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			expectedResult: false,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			g.Expect(tc.clusterBackup.needUseNodeGroup(tc.backup)).Should(BeEquivalentTo(tc.expectedResult))
		})
	}
}

func fakeClusterBackup(infraCli, clusterCli *test.MockClient) *clusterBackup {
	status := &cloudv1.AwsClusterStatus{
		Kubernetes: &cloudv1.KubernetesStatus{
			Namespace: "test",
		},
	}
	data, _ := json.Marshal(status)

	return &clusterBackup{
		cli:        infraCli,
		clusterCli: clusterCli,
		network: &cloudv1.Network{
			Spec: cloudv1.NetworkSpec{
				Region: "us-west-2",
			},
			Status: cloudv1.NetworkStatus{
				FromProvider: runtime.RawExtension{
					Object: &cloudv1.GardenerNetworkStatus{
						ShootName: "shoot",
					},
				},
			},
		},
		cluster: &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Labels: map[string]string{
					cloudv1.ProviderTypeLabel: consts.ProviderTypeAWS,
				},
			},
			Status: cloudv1.ClusterStatus{
				ObjectStore: &cloudv1.ObjectStoreStatus{
					S3: &cloudv1.Path{
						Path: "test-bucket",
					},
				},
				FromProvider: runtime.RawExtension{
					Raw: data,
				},
			},
		},
		provider:  &cloudv1.Provider{},
		k8sStatus: status.Kubernetes,
		configSvc: &mock.MockClient{
			BackupConfigOutput: &api.DescribeBRConfigOutput{
				Version: "v4.0.9",
				Config:  map[string]interface{}{"concurrency": float64(2)},
				Image: api.Image{
					BaseImage: "gcr.io/pingcap-public/dbaas/br",
					Tag:       pointer.String("v4.0.9-hotfix"),
				},
			},
		},
	}
}
