package pagestorage_test

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

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/pingcap/endless/pkg/matcher"
	"github.com/pingcap/endless/testcase/tiflash/pkg"

	"github.com/pingcap/errors"
	"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/workloadnode"
)

var fields = []string{"duration(ms)", "write_pages_nums", "write_storage(GB)", "write_speed(GB/s)", "read_pages_nums", "read_storage(GB)", "read_speed(GB/s)", "memory_lastest_used", "memory_avg_used", "memory_top_used", "snapshotslist_lastest_used", "snapshotslist_avg_used", "snapshotslist_top_used"}
var compareFields = []string{"write_speed(GB/s)", "read_speed(GB/s)"}

var _ = Describe("Perf test", func() {
	var (
		tiflash     resource.WorkloadNode
		info        testResultInfo
		clusterInfo pkg.ClusterInfo
		duration    string
		db          *sql.DB
	)
	Describe("Tiflash", func() {
		Context("run stress test", func() {
			BeforeEach(func() {
				tiflash = suiteTestCtx.Resource("tiflash").(resource.WorkloadNode)
				duration = suiteTestCtx.Param("duration", "5")
				clusterInfo.Branch = suiteTestCtx.Param("branch", "master")
				info.dsn = suiteTestCtx.Param("dsn", "slowlog:sNz1WeK7XfGg3vU@tcp(172.16.4.36)/slowlog")
				info.tableName = suiteTestCtx.Param("tableName", "pageworkload_perf")
				info.gapPercentage = Try(strconv.ParseFloat(suiteTestCtx.Param("standardGap", "0.05"), 32)).(float64)
				db = Try(sql.Open("mysql", info.dsn)).(*sql.DB)
				Try(pkg.PrintTiFlashVersionInformation(tiflash, &clusterInfo))
				log.Info("cluster", zap.String("version", clusterInfo.Version), zap.String("tiflash commit hash", clusterInfo.TiflashHash))
			})
			It("in pagestorage workload", func() {
				log.Info("run tiflash daily performance test tool")
				info.resultType = "daily"
				tiflashCmd := fmt.Sprintf("/tiflash/tiflash pageworkload -T%s", duration)
				info.beginTime = time.Now().Format("2006-01-02 15:04:05")
				start := time.Now().Unix()
				stdout, err := pkg.SyncRunCmdAndGetRes(tiflash, tiflashCmd, "stderr")
				log.Info("stdout", zap.Strings("stdout", stdout))
				end := time.Now().Unix()
				info.endTime = time.Now().Format("2006-01-02 15:04:05")
				info.duration = int(end - start)
				Expect(err).Should(BeNil())

				log.Info("parse stdout")
				// In this case, only the last four rows are valid data. So get index more than `len(stdout)-5`
				var temp []string
				for i, v := range stdout {
					if i > len(stdout)-5 {
						temp = append(temp, v)
					}
				}
				info.perfInfo = make(map[string]interface{})
				Expect(parseStdoutToMap(temp, info.perfInfo)).Should(BeNil())

				log.Info("compare daily result with baseline")
				Expect(compareWithBaseline(db, &info)).Should(BeNil())

				log.Info("skip - insert data into database")
				Expect(recordData(db, info, clusterInfo)).Should(BeNil())

				log.Info("judge case running result")
				Expect(info.status).Should(BeElementOf("true"))
			})
		})
	})
})

type testResultInfo struct {
	perfInfo      map[string]interface{}
	beginTime     string
	endTime       string
	duration      int
	tableName     string
	status        string  // case run result
	resultType    string  // data type, baseline or daily result
	gapPercentage float64 // if the gap exceeds `gapPercentage`, the case judgment fails
	dsn           string
}

func parseStdoutToMap(stdout []string, perfInfo map[string]interface{}) error {
	var resultNums []string
	for i := range stdout {
		// The fourth item of each line output is the performance index.
		// Example : [2022/07/05 12:18:03.725 +08:00] [INFO] [PSWorkload.cpp:28] ["root:result in 10187ms"] [thread_id=1]
		s := strings.Split(stdout[i], "[")[4]
		pattern := regexp.MustCompile(`\d+\.\d+|[0-9]+`)
		numberStrings := pattern.FindAllStringSubmatch(s, -1)
		for _, v := range numberStrings {
			resultNums = append(resultNums, v[0])
		}
	}
	if len(resultNums) != len(fields) {
		log.Error("stress tool number of data not match with fields", zap.Strings("resultNums", resultNums), zap.Strings("fields", fields))
		return errors.New("stress tool number of data not match with fields")
	}
	for i := range fields {
		perfInfo[fields[i]] = resultNums[i]
	}
	return nil
}

func compareWithBaseline(db *sql.DB, info *testResultInfo) error {
	info.status = "true"
	for i := range compareFields {
		s := fmt.Sprintf("select `%s` from %s where `result_type` = \"base\"", compareFields[i], info.tableName)
		log.Info("get baseline", zap.String("type", compareFields[i]), zap.String("sql", s))
		rows, err := db.Query(s)
		if err != nil {
			return err
		}
		var temp string
		if rows.Next() {
			if err := rows.Scan(&temp); err != nil {
				return err
			}
			baselineData, err := strconv.ParseFloat(temp, 32)
			if err != nil {
				return err
			}
			dailyData, err := strconv.ParseFloat(info.perfInfo[compareFields[i]].(string), 32)
			if err != nil {
				return err
			}
			diff := math.Abs((dailyData - baselineData) / baselineData)
			if dailyData > baselineData && diff > info.gapPercentage {
				info.status = "false"
				log.Info(fmt.Sprintf("result = false, dailyData drop more than %f%%, please update baseline or figure it out", diff*100), zap.Float64("baseline", baselineData), zap.Float64("daily data", dailyData))
			} else if dailyData < baselineData && diff < -info.gapPercentage {
				info.status = "false"
				log.Info(fmt.Sprintf("result = false, dailyData drop more than %f%%, please update baseline or figure it out", diff*100), zap.Float64("baseline", baselineData), zap.Float64("daily data", dailyData))
			} else {
				log.Info(fmt.Sprintf("result = true, diff percentage is lower than %f%%", diff*100), zap.Float64("baseline", baselineData), zap.Float64("daily data", dailyData))
			}
		} else {
			log.Error("no baseline")
			return errors.New("no baseline")
		}
	}
	return nil
}

func recordData(db *sql.DB, info testResultInfo, clusterInfo pkg.ClusterInfo) error {
	s := fmt.Sprintf("insert into %s(`branch`, `version`, `tiflash_hash`, `begin_at`, `end_at`, `id`, `status`,`result_type`,", info.tableName)
	for i := range fields {
		s += fmt.Sprintf("`%s`", fields[i])
		if i != len(fields)-1 {
			s += ", "
		} else {
			s += ")"
		}
	}
	s2 := fmt.Sprintf("value('%s', '%s', '%s', '%s', '%s', %s, '%s', '%s', ", clusterInfo.Branch, clusterInfo.Version, clusterInfo.TiflashHash, info.beginTime, info.endTime, "NULL", info.status, info.resultType)
	for i := range fields {
		s2 += fmt.Sprintf("'%s'", info.perfInfo[fields[i]])
		if i != len(fields)-1 {
			s2 += ", "
		} else {
			s2 += ")"
		}
	}
	log.Info("insert data", zap.String("sql", s+s2))
	if _, err := db.Exec(s + s2); err != nil {
		return err
	}
	return nil
}
