package tiup_deploy

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	_ "github.com/pingcap/endless/pkg/client/tidb"
	. "github.com/pingcap/endless/pkg/matcher"
	"github.com/pingcap/endless/pkg/workloadnode/tiup"
	. "github.com/pingcap/endless/testcase/deploy/plugin/deploypkg"
	util "github.com/pingcap/endless/testcase/upgrade/utils"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/sdk/resource"
	"go.uber.org/zap"
)

var tiupTopo = `
global:
  user: "root"
  ssh_port: 22
  deploy_dir: "/tiup/deploy"
  data_dir: "/tiup/data"
  arch: "%s"
pd_servers:
  - host: pd-peer
tikv_servers:
  - host: tikv-peer
tidb_servers:
  - host: tidb-peer
`

var pluginConfig = `
[plugin]
dir = "/root/plugins/bin/"
load = "audit-1,whitelist-1"
`

var _ = Describe("ClusterWithPlugin", func() {
	var (
		tc     Cluster
		db     *sql.DB
		ctx    context.Context
		cancel context.CancelFunc
	)
	// do not put in beforeEach, It's not necessary to deploy a new cluster for every case
	tc = deployCluster()
	Context("using tiup deploy", func() {
		It("should started and connected", func() {
			CheckClusterStatus(tc)
		})

		Context("audit table_access log", func() {
			BeforeEach(func() {
				db, _ = tc.GetDB("test")
				Try(db.Exec("create user if not exists test1, test2, qa1, qa;"))
				Try(db.Exec("grant all on *.* to test1, test2, qa1, qa;"))
				// clean the audit access table
				Try(db.Exec("delete from mysql.tidb_audit_table_access;"))
				Try(db.Exec("admin plugins enable audit;"))
				ctx, cancel = context.WithTimeout(suiteTestCtx, 10*time.Minute)
			})
			AfterEach(func() {
				Try(db.Exec("drop user if exists test1, test2, qa1, qa;"))
				db.Close()
				cancel()
			})

			It("admin plugin can works", func() {
				log.Info("start admin plugin")
				AdminPlugin(db)
			})

			It("should record audit log for matched users", func() {
				log.Info("start check audit log for matched users")
				CheckAuditLogForMatcherUser(ctx, tc, db)
			})

			It("should not record audit log for unmatched users", func() {
				log.Info("start check audit log for unmatched users")
				CheckAuditLogForNoMatcherUser(ctx, tc, db)
			})

			It("redact audit log works", func() {
				log.Info("start check audit log redact")
				CheckRedactAuditLog(ctx, tc, db)
			})

			It("should not record audit log after disable audit plugin", func() {
				log.Info("start check disable audit log")
				CheckAuditLogDisable(ctx, tc, db)
			})
		})

		// 6.1.0 开始支持 general log event， 但是 tiup 部署的集群要测试，需要在 tcms 上独立创建该 case
		// 按照目前 tiup 的实现，暂时不能将这个 case 拆出来，现不在 tiup 环境上跑该 case
		// Context("audit general log", func() {
		//     BeforeEach(func() {
		//         db, _ = tc.GetDB("test")
		//         // clean the audit access table
		//         Try(db.Exec("insert into mysql.tidb_audit_table_access(`user`,access_type, db, tbl) values ('root', '', '.*', '.*');"))
		//         Try(db.Exec("admin plugins enable audit;"))
		//         ctx, cancel = context.WithTimeout(suiteTestCtx, 10*time.Minute)
		//     })
		//     AfterEach(func() {
		//         Try(db.Exec("set global tidb_audit_log_general_events = 'STARTING,COMPLETED,ERROR'"))
		//         Try(db.Exec("delete from mysql.tidb_audit_table_access;"))
		//         db.Close()
		//         cancel()
		//     })
		//     It("default values is correct", func() {
		//         var res string
		//         err := db.QueryRow("select @@tidb_audit_log_general_events;").Scan(&res)
		//         Expect(err).Should(BeNil())
		//         Expect(res).Should(Equal("STARTING,COMPLETED,ERROR"))
		//     })
		//     It("ERROR works with execute sql success", func() {
		//         CheckGeneralAuditLog(ctx, tc, "ERROR")
		//     })
		//     It("ERROR works with execute sql failed", func() {
		//         CheckGeneralAuditLogFail(ctx, tc)
		//     })
		//     It("STARTING works", func() {
		//         CheckGeneralAuditLog(ctx, tc, "STARTING")
		//     })
		//     It("COMPLETED works", func() {
		//         CheckGeneralAuditLog(ctx, tc, "COMPLETED")
		//     })
		// })

		Context("whitelist plugin", func() {
			BeforeEach(func() {
				db, _ = tc.GetDB("test")
				Try(db.Exec("admin plugins enable whitelist;"))
				Try(db.Exec("create user if not exists u1;"))
				Try(db.Exec("grant all on *.* to u1;"))
			})

			AfterEach(func() {
				Try(db.Exec("delete from mysql.whitelist;"))
				Try(db.Exec("flush tidb plugins whitelist;"))
			})

			When("disable whitelist plugin", func() {
				It("the ip not in the whitelist also can connect", func() {
					// set an impossible IP in the env.
					Try(db.Exec("admin plugins disable whitelist;"))
					time.Sleep(1 * time.Second)
					Try(db.Exec("insert into mysql.whitelist(name, list) values ('dev', '192.168.100.100/32')"))
					Try(db.Exec("flush tidb plugins whitelist;"))
					ShowPluginInfo(db, "whitelist", "Ready-disable")
					_, err := GetDBWithUser(tc, "test", "u1")
					Expect(err).Should(BeNil())
				})
			})

			When("enable whitelist plugin", func() {
				It("the ip in the whitelist can connect", func() {
					var user, host string
					if err := db.QueryRow("select user();").Scan(&user); err != nil {
						host = strings.Split(user, "@")[1]
					}
					Try(db.Exec("insert into mysql.whitelist(name, list) values ('dev', ?);", host))
					Try(db.Exec("flush tidb plugins whitelist;"))
					time.Sleep(1 * time.Second)
					_, err := GetDBWithUser(tc, "test", "u1")
					Expect(err).Should(Succeed())
				})

				It("the ip not in the whitelist can not connect", func() {
					// set an impossible IP in the env.
					Try(db.Exec("insert into mysql.whitelist(name, list) values ('dev', '192.168.100.100/32')"))
					Try(db.Exec("flush tidb plugins whitelist;"))
					time.Sleep(1 * time.Second)
					_, err := GetDBWithUser(tc, "test", "u1")
					log.Info("con", zap.Error(err))
					Expect(err.Error()).Should(Equal("driver: bad connection"))
				})
			})
		})
		It("should gather the cluster info to clinic", func() {
			util.Clinic_Upload_Config(tc.(*tiup.TiUPCluster), "tiup_plugin_cluster")
		})
	})
})

func deployCluster() (cluster *tiup.TiUPCluster) {
	tiupNode := suiteTestCtx.Resource("tiup").(resource.WorkloadNode)
	tidbNode := suiteTestCtx.Resource("tidb").(resource.WorkloadNode)
	version := suiteTestCtx.Param("tiup_target_version")
	plugin_url := suiteTestCtx.Param("plugin_url")
	patch_binary := suiteTestCtx.Param("patch_binary")
	arch := suiteTestCtx.Param("arch", "amd64")
	tiup_mirror := suiteTestCtx.Param("tiup_mirror", "staging")

	defer GinkgoRecover()
	// version = "v6.2.0"
	// plugin_url = "http://fileserver.pingcap.net/download/builds/pingcap/enterprise-plugin/optimization/v6.2.0/901bcdd2470a31542330122a6080781d6df2f366/centos7/enterprise-plugin-linux-amd64-enterprise.tar.gz"
	// tiup_mirror = "product"

	var cmd string
	if tiup_mirror == "staging" {
		cmd = "tiup mirror set http://staging.tiup-server.pingcap.net"
	} else {
		cmd = "tiup mirror set http://172.16.5.134:8987"
	}

	// deploy a cluster without plugin
	topology := fmt.Sprintf(tiupTopo, arch)
	Try(tiupNode.Exec(resource.WorkloadNodeExecOptions{Command: cmd}))
	cluster = tiup.NewTiUPCluster(tiupNode, "tiup-peer", topology, "plugin").WithCMDTimeout(10 * time.Minute)
	Must(cluster.Deploy(version))

	if err := cluster.Start(); err != nil {
		log.Error("start tidb with plugin failed", zap.String("plugin", err.Error()))
		Expect(err).Should(Succeed())
	}

	// for hotfix, we need to patch first
	// patch_binary = "tidb:http://fileserver.pingcap.net/download/builds/hotfix/tidb/v6.1.0-20220729/a0ee324e64273516c1af8a69acfef338857f6fda/centos7/tidb-patch-linux-amd64.tar.gz"
	if patch_binary != "" {
		for _, component := range strings.Split(patch_binary, ";") {
			comp := strings.Split(component, ":")
			url := strings.Join(comp[1:], ":")
			if err := cluster.Patch(url, 30*time.Minute, "-R", comp[0]); err != nil {
				log.Error("patch failed", zap.String("component", comp[1]), zap.String("url", url), zap.Error(err))
				Expect(err).Should(Succeed())
			}
		}
	}

	// update config to use plugin
	DownloadPlugin(tidbNode, plugin_url)
	opts := resource.TiDBClusterUpdateConfigOptions{
		Target:  resource.TiDB,
		Config:  pluginConfig,
		Timeout: 5 * time.Minute,
	}
	if err := cluster.UpdateConfig(opts); err != nil {
		log.Error("update cluster with plugin failed", zap.Error(err))
		Expect(err).Should(Succeed())
	}
	return
}
