package dtworkload_test

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

	"github.com/google/uuid"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/sdk/resource"
	_ "github.com/pingcap/test-infra/sdk/resource/impl/k8s" // register k8s impl
	"go.uber.org/zap"

	. "github.com/pingcap/endless/pkg/matcher"

	_ "github.com/pingcap/endless/pkg/workloadnode"
)

var _ = Describe("Perf test", func() {
	var (
		tiflash     resource.WorkloadNode
		testType    string
		info        testResultInfo
		clusterInfo clusterInfo
		dsn         string
		verifyRound string
	)
	Describe("Tiflash", func() {
		Context("in dt workload", func() {
			BeforeEach(func() {
				tiflash = suiteTestCtx.Resource("tiflash").(resource.WorkloadNode)
				dsn = suiteTestCtx.Param("dsn", "slowlog:sNz1WeK7XfGg3vU@tcp(172.16.4.36)/slowlog")
				clusterInfo.branch = suiteTestCtx.Param("branch", "master")
				log.Info("branch param", zap.String("branch", clusterInfo.branch))
				info.workload = suiteTestCtx.Param("workload", "tiflash_dt_perf")
				verifyRound = suiteTestCtx.Param("verifyRound", "100")
				info.standGap = Try(strconv.ParseFloat(suiteTestCtx.Param("standardGap", "0.05"), 32)).(float64)
				Try(getClusterVersionInformation(tiflash, &clusterInfo))
			})
			It("daily performance test", func() {
				testType = "daily_perf"
				info.resultType = "daily"
				log.Info("dsn param", zap.String("dsn", dsn))
				tiflashCmd := fmt.Sprintf("/tiflash/tiflash dtworkload --testing_type=%s --verify_round=%s --log_file=/tmp/dt.log", testType, verifyRound)

				// record time.
				log.Info("run tiflash daily performance test tool")
				info.beginTime = time.Now().Format("2006-01-02 15:04:05")
				start := time.Now().Unix()
				// run performance tool and record stdout
				exitCode, err := runCmdAndPutStdoutToStringArray(tiflash, tiflashCmd, &info.perfInfo)
				end := time.Now().Unix()
				info.endTime = time.Now().Format("2006-01-02 15:04:05")
				info.duration = int(end - start)

				// if exitCode != 0, means tool run failed
				if exitCode != 0 {
					info.status = "false"
				} else {
					info.status = "true"
				}
				Expect(err).Should(BeNil())
				Expect(exitCode).Should(BeZero())

				// record compare result
				Try(compareWithBaselineRecordResult(&info, &clusterInfo, dsn))
			})

			It("daily random test", func() {
				testType = "daily_random"
				tiflashCmd := fmt.Sprintf("/tiflash/tiflash dtworkload --testing_type=%s --log_file=/tmp/dt.log", testType)
				// random test tool
				Try(runCmdAndPutStdoutToStringArray(tiflash, tiflashCmd, &info.perfInfo))
			})

			It("get baseline performance test", func() {
				testType = "daily_perf"
				info.resultType = "base"
				log.Info("dsn param", zap.String("dsn", dsn))
				tiflashCmd := fmt.Sprintf("/tiflash/tiflash dtworkload --testing_type=%s --verify_round=%s --log_file=/tmp/dt.log", testType, verifyRound)

				// record time.
				log.Info("run tiflash daily performance test tool")
				info.beginTime = time.Now().Format("2006-01-02 15:04:05")
				start := time.Now().Unix()
				// run performance tool and record stdout
				exitCode, err := runCmdAndPutStdoutToStringArray(tiflash, tiflashCmd, &info.perfInfo)
				end := time.Now().Unix()
				info.endTime = time.Now().Format("2006-01-02 15:04:05")
				info.duration = int(end - start)

				// if exitCode != 0, means tool run failed
				if exitCode != 0 {
					info.status = "false"
				} else {
					info.status = "true"
				}
				Expect(err).Should(BeNil())
				Expect(exitCode).Should(BeZero())

				// record compare result
				Try(recordResult(info, &clusterInfo, dsn))
			})
		})
	})
})

type clusterInfo struct {
	branch      string
	version     string
	tidbHash    string
	pdHash      string
	tikvHash    string
	tiflashHash string
}

type testResultInfo struct {
	perfInfo     []string
	beginTime    string
	endTime      string
	duration     int
	status       string
	resultType   string
	workload     string
	standGap     float64
	initSeconds  string
	writeSeconds string
	readSeconds  string
	subWorkload  string
}

// use `compareWithBaseline` to get baseline and compare data, then insert data into database
func compareWithBaselineRecordResult(info *testResultInfo, clusterInfo *clusterInfo, dsn string) error {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}

	// perf test have 4 line stdout
	executionId := uuid.New().String()
	Expect(len(info.perfInfo)).Should(BeEquivalentTo(4))
	for i := range info.perfInfo {
		if i != 0 {
			singleItem := strings.Split(info.perfInfo[i], ",")
			info.subWorkload, info.initSeconds, info.writeSeconds, info.readSeconds = singleItem[2], singleItem[3], singleItem[4], singleItem[5]

			// get baseline and compare data
			log.Info(fmt.Sprintf("test type = %s", info.subWorkload))
			err := compareWithBaseline(singleItem, db, info)
			if err != nil {
				return err
			}

			// record compare result
			insterresult := fmt.Sprintf(`insert INTO tiflash_perf_res(branch,version,tidb_hash,pd_hash,tikv_hash,tiflash_hash,begin_at,end_at,duration,id,execution_id,workload,sub_workload,init_duration,write_speed,read_speed,status,result_type) value('%s','%s',NULL,NULL,NULL,'%s','%s','%s','%d',NULL,'%s','%s','%s','%s','%s','%s','%s','%s')`,
				clusterInfo.branch, clusterInfo.version, clusterInfo.tiflashHash, info.beginTime, info.endTime, info.duration, executionId, info.workload, info.subWorkload, info.initSeconds, info.writeSeconds, info.readSeconds, info.status, info.resultType)
			log.Info("sql", zap.String("insert perf data", insterresult))
			_, err = db.Exec(insterresult)
			if err != nil {
				return err
			}
		}
	}

	Expect(info.status).Should(BeEquivalentTo("true"))
	err = db.Close()
	return err
}

// run `/tiflash/tiflash version` to get tiflash's version and put it into `clusterInfo`
func getClusterVersionInformation(tiflash resource.WorkloadNode, clusterInfo *clusterInfo) error {
	tiflashVersionCmd := "/tiflash/tiflash version"
	var info []string
	exitCode, err := runCmdAndPutStdoutToStringArray(tiflash, tiflashVersionCmd, &info)
	if err != nil || exitCode != 0 {
		return err
	}

	// parse stdout, get tiflash version
	for _, s := range info {
		if strings.HasPrefix(s, "Release Version:") {
			clusterInfo.version = strings.Trim(s[16:], " ")
			log.Info("tiflash version:", zap.String("version", clusterInfo.version))
		}
		if strings.HasPrefix(s, "Git Commit Hash:") {
			clusterInfo.tiflashHash = strings.Trim(s[16:], " ")
			log.Info("tiflash commit hash:", zap.String("hash", clusterInfo.tiflashHash))
			break
		}
	}
	return nil
}

// run command and put the stdout to a string array
func runCmdAndPutStdoutToStringArray(tiflash resource.WorkloadNode, cmd string, info *[]string) (int, error) {
	log.Info("Start execute command on TiFlash nodes", zap.String("command", cmd))
	stdout, stderr, exitCode, err := tiflash.Exec(resource.WorkloadNodeExecOptions{Command: cmd})
	if err != nil || exitCode != 0 {
		log.Error("Execute command error on TiFlash nodes",
			zap.String("command", cmd),
			zap.Int("exit code", exitCode),
			zap.String("stderr", stderr),
			zap.String("stdout", stdout),
			zap.Error(err))
		return exitCode, err
	}
	*info = strings.Split(stdout, "\n")

	var formatStdout string
	for _, line := range *info {
		formatStdout += line + " "
	}
	log.Info("End execute command on TiFlash nodes", zap.String("command", cmd), zap.String("stdout", formatStdout))
	return exitCode, nil
}

// compare performance data with baseline
func compareWithBaseline(perfdata []string, db *sql.DB, info *testResultInfo) error {
	// perfdata format example:
	// [Date,Table Schema,Workload,Init Seconds,Write Speed(rows count),Read Speed(rows count)]
	// [20211223,random,uniform,0.26,29690.31,384396.75]
	// perfdata[2] indicate "subWorkload"
	subWorkload := perfdata[2]
	getBaselineSql := fmt.Sprintf("select init_duration,write_speed,read_speed from tiflash_perf_res where result_type = 'base' and sub_workload = '%s' and workload = '%s'", subWorkload, info.workload)
	log.Info("get baseline sql", zap.String("get baseline sql", getBaselineSql))
	rows, err := db.Query(getBaselineSql)
	if err != nil {
		return err
	}

	var baseInitDuration, baseWriteSpeed, baseReadSpeed string
	if rows.Next() {
		if err := rows.Scan(&baseInitDuration, &baseWriteSpeed, &baseReadSpeed); err != nil {
			return err
		}

		// perfdata format example:
		// [Date,Table Schema,Workload,Init Seconds,Write Speed(rows count),Read Speed(rows count)]
		// [20211223, random, uniform, 0.26, 29690.31, 384396.75]
		// perfdata[3] indicate "Init Seconds", perfdata[4] indicate "Write Speed", perfdata[5] indicate "Read Speed",
		_, writeSeconds, readSeconds := perfdata[3], perfdata[4], perfdata[5]

		// dataType := "Init Seconds"
		// initCompareResult, diff, err := computeGap(baseInitDuration, initSeconds, info, dataType)
		// if err != nil {
		// 	return err
		// }
		// if !initCompareResult {
		// 	info.status = "false"
		// 	log.Info("init speed performance drop", zap.String("gap", string(strconv.FormatFloat(diff, 'f', 2, 64))))
		// }
		dataType := "Write Speed(rows count)"
		writeCompareResult, diff, err := computeGap(baseWriteSpeed, writeSeconds, info, dataType)
		if err != nil {
			return err
		}
		if !writeCompareResult {
			info.status = "false"
			log.Info("write speed performance drop", zap.String("gap", string(strconv.FormatFloat(diff, 'f', 2, 64))))
		}
		dataType = "Read Speed(rows count)"
		readCompareResult, diff, err := computeGap(baseReadSpeed, readSeconds, info, dataType)
		if err != nil {
			return err
		}
		if !readCompareResult {
			info.status = "false"
			log.Info("read speed performance drop", zap.String("gap", string(strconv.FormatFloat(diff, 'f', 2, 64))))
		}
	} else {
		log.Info("no baseline")
	}
	return nil
}

// Compute the performance result diff. If diff is more than 5% return false
func computeGap(base string, temp string, info *testResultInfo, dataType string) (bool, float64, error) {
	basefloat, err := strconv.ParseFloat(base, 32)
	if err != nil {
		return false, 0, err
	}

	tempfloat, err := strconv.ParseFloat(temp, 32)
	if err != nil {
		return false, 0, err
	}

	var diff float64 = (tempfloat - basefloat) / basefloat
	log.Info(fmt.Sprintf("%s data and diff", dataType), zap.String("base", base), zap.String("this time", temp), zap.String("diff percentage", fmt.Sprintf("%s%%", strconv.FormatFloat(diff*100, 'f', 2, 32))), zap.String("standard gap", strconv.FormatFloat(info.standGap*100, 'f', 2, 32)))

	// info.standGap default = 5%
	if (diff > 0 && diff > info.standGap) || (diff < 0 && diff < -info.standGap) {
		return false, diff, nil
	}
	return true, diff, nil
}

// only record result as baseline
func recordResult(info testResultInfo, clusterInfo *clusterInfo, dsn string) error {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}

	executionId := uuid.New().String()
	for i := range info.perfInfo {
		if i != 0 {
			singleItem := strings.Split(info.perfInfo[i], ",")
			info.subWorkload, info.initSeconds, info.writeSeconds, info.readSeconds = singleItem[2], singleItem[3], singleItem[4], singleItem[5]

			// record compare result
			insterresult := fmt.Sprintf(`insert INTO baseline_data(branch,version,tidb_hash,pd_hash,tikv_hash,tiflash_hash,begin_at,end_at,duration,id,execution_id,workload,sub_workload,init_duration,write_speed,read_speed,status,result_type) value('%s','%s',NULL,NULL,NULL,NULL,'%s','%s','%d',NULL,'%s','%s','%s','%s','%s','%s','%s','%s')`,
				clusterInfo.branch, clusterInfo.version, info.beginTime, info.endTime, info.duration, executionId, info.workload, info.subWorkload, info.initSeconds, info.writeSeconds, info.readSeconds, info.status, info.resultType)
			log.Info("sql", zap.String("insert perf data", insterresult))
			_, err = db.Exec(insterresult)
			if err != nil {
				return err
			}
		}
	}
	err = db.Close()
	return err
}
