/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cmd

import (
	"context"
	"errors"

	"bou.ke/monkey"
	"github.com/apache/shardingsphere-on-cloud/pitr/cli/internal/pkg"
	mock_pkg "github.com/apache/shardingsphere-on-cloud/pitr/cli/internal/pkg/mocks"
	"github.com/apache/shardingsphere-on-cloud/pitr/cli/internal/pkg/model"
	"github.com/apache/shardingsphere-on-cloud/pitr/cli/internal/pkg/xerr"
	"github.com/apache/shardingsphere-on-cloud/pitr/cli/pkg/httputils"
	mock_httputils "github.com/apache/shardingsphere-on-cloud/pitr/cli/pkg/httputils/mocks"
	"github.com/apache/shardingsphere-on-cloud/pitr/cli/pkg/promptutil"
	"github.com/golang/mock/gomock"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Backup", func() {

	Context("do check", func() {
		var (
			as *mock_pkg.MockIAgentServer
			sn = &model.StorageNode{
				IP: "127.0.0.1",
			}

			task = &backuptask{}
		)
		BeforeEach(func() {
			ctrl = gomock.NewController(GinkgoT())
			as = mock_pkg.NewMockIAgentServer(ctrl)
			task = &backuptask{
				As:   as,
				Sn:   sn,
				Dn:   &model.DataNode{},
				DnCh: make(chan *model.DataNode, 2),

				Backup: &model.BackupInfo{},
			}
		})
		AfterEach(func() {
			ctrl.Finish()
		})

		It("mock agent server return err", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(nil, errors.New("mock agent timeout"))

			finished, err := task.checkProgress()
			Expect(err).To(HaveOccurred())
			Expect(finished).To(BeFalse())
			Expect(task.Dn.Status).To(Equal(model.SsBackupStatusCheckError))
		})

		It("mock agent server and return failed status", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusFailed}, nil)
			finished, err := task.checkProgress()
			Expect(err).ToNot(HaveOccurred())
			Expect(finished).To(BeTrue())
			Expect(task.Backup.Status).To(Equal(model.SsBackupStatusFailed))
		})

		It("mock agent server and return completed status", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusCompleted}, nil)

			finished, err := task.checkProgress()
			Expect(err).ToNot(HaveOccurred())
			Expect(finished).To(BeTrue())
			Expect(task.Backup.Status).To(Equal(model.SsBackupStatusCompleted))
		})

		It("mock agent server and return check err first time and then success", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(nil, errors.New("mock agent timeout"))
			finished, err := task.checkProgress()
			Expect(err).To(HaveOccurred())
			Expect(finished).To(BeFalse())
			Expect(task.Dn.Status).To(Equal(model.SsBackupStatusCheckError))

			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusCompleted}, nil)
			finished, err = task.checkProgress()
			Expect(err).ToNot(HaveOccurred())
			Expect(finished).To(BeTrue())
			Expect(task.Backup.Status).To(Equal(model.SsBackupStatusCompleted))
		})
	})

	Context("export data", func() {
		var (
			proxy *mock_pkg.MockIShardingSphereProxy
			ls    *mock_pkg.MockILocalStorage
		)
		BeforeEach(func() {
			ctrl = gomock.NewController(GinkgoT())
			proxy = mock_pkg.NewMockIShardingSphereProxy(ctrl)
			ls = mock_pkg.NewMockILocalStorage(ctrl)

		})
		AfterEach(func() {
			ctrl.Finish()
		})
		It("export data", func() {
			// mock proxy export metadata
			proxy.EXPECT().ExportMetaData().Return(&model.ClusterInfo{}, nil)
			// mock proxy export node storage data
			proxy.EXPECT().ExportStorageNodes().Return([]*model.StorageNode{}, nil)
			// mock ls generate filename
			ls.EXPECT().GenFilename(pkg.ExtnJSON).Return("mock.json")
			// mock ls write by json
			ls.EXPECT().WriteByJSON("mock.json", gomock.Any()).Return(nil)

			bk, err := exportData(proxy, ls)
			Expect(err).To(BeNil())
			Expect(bk.Info.CSN).To(Equal(""))
		})
	})

	Context("exec backup", func() {
		var (
			as *mock_pkg.MockIAgentServer
		)
		BeforeEach(func() {
			ctrl = gomock.NewController(GinkgoT())
			as = mock_pkg.NewMockIAgentServer(ctrl)
		})
		AfterEach(func() {
			ctrl.Finish()
		})
		bak := &model.LsBackup{
			DnList: nil,
			SsBackup: &model.SsBackup{
				Status:       "Running",
				StorageNodes: []*model.StorageNode{},
			},
		}

		It("exec backup empty storage nodes", func() {
			Expect(execBackup(bak)).To(BeNil())
		})
		It("exec backup 2 storage nodes", func() {
			bak.SsBackup.StorageNodes = []*model.StorageNode{
				{
					IP:       "127.0.0.1",
					Port:     80,
					Username: "",
					Password: "",
					Database: "",
					Remark:   "",
				},
				{
					IP:       "127.0.0.2",
					Port:     443,
					Username: "",
					Password: "",
					Database: "",
					Remark:   "",
				},
			}
			as.EXPECT().Backup(gomock.Any()).Return("", nil)
			dnCh := make(chan *model.DataNode, 10)

			Expect(_execBackup(as, bak.SsBackup.StorageNodes[0], dnCh)).To(BeNil())
			Expect(len(dnCh)).To(Equal(1))

			as.EXPECT().Backup(gomock.Any()).Return("", xerr.NewCliErr("backup failed"))

			Expect(_execBackup(as, bak.SsBackup.StorageNodes[1], dnCh)).ToNot(BeNil())
			close(dnCh)
			Expect(len(dnCh)).To(Equal(2))

		})
	})

	Context("check backup status", func() {
		var (
			as       *mock_pkg.MockIAgentServer
			lsbackup *model.LsBackup
		)
		BeforeEach(func() {
			lsbackup = &model.LsBackup{
				DnList: []*model.DataNode{
					{
						IP:   "127.0.0.1",
						Port: 3306,
					},
					{
						IP:   "127.0.0.2",
						Port: 3307,
					},
				},
				SsBackup: &model.SsBackup{
					Status: "Running",
					StorageNodes: []*model.StorageNode{
						{
							IP:   "127.0.0.1",
							Port: 3306,
						},
						{
							IP:   "127.0.0.2",
							Port: 3307,
						},
					},
				},
				Info: &model.BackupMetaInfo{},
			}

			ctrl = gomock.NewController(GinkgoT())
			as = mock_pkg.NewMockIAgentServer(ctrl)

			monkey.Patch(pkg.NewAgentServer, func(_ string) pkg.IAgentServer {
				return as
			})
		})
		AfterEach(func() {
			ctrl.Finish()
			monkey.UnpatchAll()
		})

		It("check error 1", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(nil, errors.New("timeout")).AnyTimes()
			Expect(checkBackupStatus(lsbackup)).To(Equal(model.SsBackupStatusFailed))
		})

		It("check error 2", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(nil, errors.New("timeout")).Times(1)
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusFailed}, nil).AnyTimes()
			Expect(checkBackupStatus(lsbackup)).To(Equal(model.SsBackupStatusFailed))
		})

		It("check error 3", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(nil, errors.New("timeout")).Times(2)
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusCompleted}, nil).AnyTimes()
			Expect(checkBackupStatus(lsbackup)).To(Equal(model.SsBackupStatusCompleted))
		})

		It("check failed", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusFailed}, nil).AnyTimes()
			Expect(checkBackupStatus(lsbackup)).To(Equal(model.SsBackupStatusFailed))
		})

		It("check success", func() {
			as.EXPECT().ShowDetail(gomock.Any()).Return(&model.BackupInfo{Status: model.SsBackupStatusCompleted}, nil).AnyTimes()
			Expect(checkBackupStatus(lsbackup)).To(Equal(model.SsBackupStatusCompleted))
		})
	})
})

var _ = Describe("test backup manually", func() {
	var (
		// implement with your own dev
		u  string
		p  string
		db string
		h  string
		pt uint16
	)
	Context("test manually", func() {
		It("unlock after lock", func() {
			if u == "" || p == "" || db == "" || h == "" || pt == 0 {
				Skip("need to set u, p, db, h, pt first")
			}
			proxy, _ := pkg.NewShardingSphereProxy(u, p, db, h, pt)
			Expect(proxy.LockForBackup()).To(BeNil())
			Expect(proxy.Unlock()).To(BeNil())
		})

		It("export data in dev", func() {
			if u == "" || p == "" || db == "" || h == "" || pt == 0 {
				Skip("need to set u, p, db, h, pt first")
			}
			proxy, _ := pkg.NewShardingSphereProxy(u, p, db, h, pt)
			ls, _ := pkg.NewLocalStorage("./")

			Expect(proxy.LockForBackup()).To(BeNil())
			defer func() {
				Expect(proxy.Unlock()).To(BeNil())
			}()

			bk, err := exportData(proxy, ls)

			Expect(err).To(BeNil())
			Expect(bk.Info).NotTo(BeNil())
		})

		It("test all", func() {
			if u == "" || p == "" || db == "" || h == "" || pt == 0 {
				Skip("need to set u, p, db, h, pt first")
			}
			proxy, _ := pkg.NewShardingSphereProxy(u, p, db, h, pt)
			ls, _ := pkg.NewLocalStorage(pkg.DefaultRootDir())
			bak, err := exportData(proxy, ls)
			Expect(err).To(BeNil())
			Expect(bak.Info).NotTo(BeNil())

			AgentPort = 18080
			BackupPath = "/home/omm/data"
			ThreadsNum = 1

			err = execBackup(bak)
			Expect(err).To(BeNil())
			Expect(bak.SsBackup.Status).To(Equal(model.SsBackupStatusRunning))

			err = ls.WriteByJSON(filename, bak)
			Expect(err).To(BeNil())

			Expect(checkBackupStatus(bak)).To(Equal(model.SsBackupStatusCompleted))

			err = ls.WriteByJSON(filename, bak)
			Expect(err).To(BeNil())
		})
	})
})

var _ = Describe("test backup mock", func() {
	Context("test by mock", func() {
		var (
			proxy *mock_pkg.MockIShardingSphereProxy
			ls    *mock_pkg.MockILocalStorage
			as    *mock_pkg.MockIAgentServer
		)
		BeforeEach(func() {
			ctrl = gomock.NewController(GinkgoT())
			proxy = mock_pkg.NewMockIShardingSphereProxy(ctrl)
			ls = mock_pkg.NewMockILocalStorage(ctrl)
			as = mock_pkg.NewMockIAgentServer(ctrl)

			monkey.Patch(pkg.NewShardingSphereProxy, func(u, p, db, h string, pt uint16) (pkg.IShardingSphereProxy, error) {
				return proxy, nil
			})
			monkey.Patch(pkg.NewLocalStorage, func(rootDir string) (pkg.ILocalStorage, error) {
				return ls, nil
			})
			monkey.Patch(pkg.NewAgentServer, func(addr string) pkg.IAgentServer {
				return as
			})
			monkey.Patch(promptutil.GetUserApproveInTerminal, func(_ string) error {
				return nil
			})
		})
		AfterEach(func() {
			monkey.UnpatchAll()
			ctrl.Finish()
		})

		It("test backup empty", func() {
			proxy.EXPECT().LockForBackup().Return(nil)
			proxy.EXPECT().ExportMetaData().Return(&model.ClusterInfo{}, nil)
			proxy.EXPECT().ExportStorageNodes().Return([]*model.StorageNode{}, nil)
			proxy.EXPECT().Unlock().Return(nil)
			ls.EXPECT().GenFilename(gomock.Any()).Return("filename")
			ls.EXPECT().WriteByJSON(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
			Expect(backup()).To(BeNil())
		})
	})

	Context("test check agent server status", func() {
		var mockCtrl *gomock.Controller
		var mockIreq *mock_httputils.MockIreq

		ls := &model.LsBackup{
			Info:   nil,
			DnList: nil,
			SsBackup: &model.SsBackup{
				StorageNodes: []*model.StorageNode{
					{
						IP:   "127.0.0.1",
						Port: 3306,
					},
					{
						IP:   "127.0.0.2",
						Port: 3307,
					},
				},
			},
		}
		BeforeEach(func() {
			mockCtrl = gomock.NewController(GinkgoT())
			mockIreq = mock_httputils.NewMockIreq(mockCtrl)
			mockIreq.EXPECT().Body(gomock.Any()).AnyTimes()

			monkey.Patch(httputils.NewRequest, func(ctx context.Context, method, url string) httputils.Ireq {
				return mockIreq
			})
		})

		AfterEach(func() {
			monkey.UnpatchAll()
			mockCtrl.Finish()
		})

		It("agent server is not running", func() {
			mockIreq.EXPECT().Send(gomock.Any()).Return(errors.New("error")).AnyTimes()
			mockIreq.EXPECT().Header(gomock.Any()).AnyTimes()
			Expect(checkAgentServerStatus(ls)).To(BeFalse())
		})

		It("agent server are running", func() {
			mockIreq.EXPECT().Send(gomock.Any()).Return(nil).AnyTimes()
			mockIreq.EXPECT().Header(gomock.Any()).AnyTimes()
			Expect(checkAgentServerStatus(ls)).To(BeTrue())
		})

		It("one agent server is not running", func() {
			mockIreq.EXPECT().Send(gomock.Any()).Return(errors.New("failed"))
			mockIreq.EXPECT().Send(gomock.Any()).Return(nil).AnyTimes()
			mockIreq.EXPECT().Header(gomock.Any()).AnyTimes()
			Expect(checkAgentServerStatus(ls)).To(BeFalse())
		})
	})

	Context("test delete backup data", func() {
		var (
			ls *mock_pkg.MockILocalStorage
		)

		bak := &model.LsBackup{
			Info: nil,
			DnList: []*model.DataNode{
				{
					IP:   "test.delete.backup",
					Port: 3306,
				},
			},
			SsBackup: &model.SsBackup{
				StorageNodes: []*model.StorageNode{
					{
						IP:   "test.delete.backup",
						Port: 3306,
					},
				},
			},
		}
		BeforeEach(func() {
			ctrl = gomock.NewController(GinkgoT())
			ls = mock_pkg.NewMockILocalStorage(ctrl)

			monkey.Patch(pkg.NewLocalStorage, func(rootDir string) (pkg.ILocalStorage, error) {
				return ls, nil
			})
		})
		AfterEach(func() {
			monkey.UnpatchAll()
			ctrl.Finish()
		})

		It("should delete failed", func() {
			ls.EXPECT().DeleteByName(gomock.Any()).Return(errors.New("failed"))
			deleteBackupFiles(ls, bak, deleteModeNormal)
		})

		It("should delete success", func() {
			ctrl := gomock.NewController(GinkgoT())
			as := mock_pkg.NewMockIAgentServer(ctrl)
			monkey.Patch(pkg.NewAgentServer, func(addr string) pkg.IAgentServer {
				return as
			})

			defer monkey.UnpatchAll()
			defer ctrl.Finish()
			as.EXPECT().DeleteBackup(gomock.Any()).Return(nil)
			ls.EXPECT().DeleteByName(gomock.Any()).Return(nil)
			deleteBackupFiles(ls, bak, deleteModeNormal)
		})
	})
})
