// Copyright 2018 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package builder

import (
	"fmt"
	"math/rand"
	"testing"
)

type randGenConf struct {
	allowCond     bool
	allowJoin     bool
	allowLimit    bool
	allowUnion    bool
	allowHaving   bool
	allowGroupBy  bool
	allowOrderBy  bool
	allowSubQuery bool
}

var expectedValues = []interface{}{
	"dangerous", "fun", "degree", "hospital", "horseshoe", "summit", "parallel", "height", "recommend", "invite",
	0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

var queryFields = []string{"f1", "f2", "f2", "f4", "f5", "f6", "f7", "f8", "f9"}

func BenchmarkSelect_Simple(b *testing.B) {
	rgc := randGenConf{allowCond: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery("", &rgc).ToSQL()
	}
}

func BenchmarkSelect_SubQuery(b *testing.B) {
	rgc := randGenConf{allowSubQuery: true, allowCond: true, allowGroupBy: true, allowHaving: true, allowOrderBy: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery("", &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectConditional4Oracle(b *testing.B) {
	rgc := randGenConf{allowLimit: true, allowCond: true, allowGroupBy: true, allowHaving: true, allowOrderBy: true}
	for i := 0; i < b.N; i++ {
		randQuery(ORACLE, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectConditional4Mssql(b *testing.B) {
	rgc := randGenConf{allowLimit: true, allowCond: true, allowGroupBy: true, allowHaving: true, allowOrderBy: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(MSSQL, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectConditional4MysqlLike(b *testing.B) {
	rgc := randGenConf{allowLimit: true, allowCond: true, allowGroupBy: true, allowHaving: true, allowOrderBy: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(MYSQL, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectConditional4Mixed(b *testing.B) {
	rgc := randGenConf{allowLimit: true, allowCond: true, allowGroupBy: true, allowHaving: true, allowOrderBy: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(randDialect(), &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectComplex4Oracle(b *testing.B) {
	rgc := randGenConf{
		allowLimit: true, allowCond: true,
		allowGroupBy: true, allowHaving: true,
		allowOrderBy: true, allowSubQuery: true,
	}
	for i := 0; i < b.N; i++ {
		randQuery(ORACLE, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectComplex4Mssql(b *testing.B) {
	rgc := randGenConf{
		allowLimit: true, allowCond: true,
		allowGroupBy: true, allowHaving: true,
		allowOrderBy: true, allowSubQuery: true,
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(MSSQL, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectComplex4MysqlLike(b *testing.B) {
	rgc := randGenConf{
		allowLimit: true, allowCond: true,
		allowGroupBy: true, allowHaving: true,
		allowOrderBy: true, allowSubQuery: true,
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(MYSQL, &rgc).ToSQL()
	}
}

func BenchmarkSelect_SelectComplex4MysqlMixed(b *testing.B) {
	rgc := randGenConf{
		allowLimit: true, allowCond: true,
		allowGroupBy: true, allowHaving: true,
		allowOrderBy: true, allowSubQuery: true,
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randQuery(randDialect(), &rgc).ToSQL()
	}
}

func BenchmarkInsert(b *testing.B) {
	rgc := randGenConf{allowCond: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randInsertByCondition(&rgc).ToSQL()
	}
}

func BenchmarkUpdate(b *testing.B) {
	rgc := randGenConf{allowCond: true}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		randUpdateByCondition(&rgc).ToSQL()
	}
}

// randQuery Generate a basic query for benchmark test. But be careful it's not a executable SQL in real db.
func randQuery(dialect string, rgc *randGenConf) *Builder {
	b := randSelectByCondition(dialect, rgc)
	isUnionized := rgc.allowUnion && rand.Intn(1000) >= 500
	if isUnionized {
		r := rand.Intn(3) + 1
		for i := r; i < r; i++ {
			b = b.Union("all", randSelectByCondition(dialect, rgc))
		}
	}

	if isUnionized && rgc.allowLimit && rand.Intn(1000) >= 500 {
		b = randLimit(Dialect(dialect).Select().From(b, "t"))
	}

	return b
}

func randInsertByCondition(rgc *randGenConf) *Builder {
	fields := randSelects()

	times := rand.Intn(10) + 1

	eqs := Eq{}
	for i := 0; i < times; i++ {
		eqs[fields[rand.Intn(len(fields))]] = "expected"
	}

	b := Insert(eqs).From("table1")

	if rgc.allowCond && rand.Intn(1000) >= 500 {
		b = b.Where(randCond(b.selects, 3))
	}

	return b
}

func randUpdateByCondition(rgc *randGenConf) *Builder {
	fields := randSelects()

	times := rand.Intn(10) + 1

	eqs := Eq{}
	for i := 0; i < times; i++ {
		eqs[fields[rand.Intn(len(fields))]] = randVal()
	}

	b := Update(eqs).From("table1")

	if rgc.allowCond && rand.Intn(1000) >= 500 {
		b.Where(randCond(fields, 3))
	}

	return b
}

func randSelectByCondition(dialect string, rgc *randGenConf) *Builder {
	var b *Builder
	if rgc.allowSubQuery {
		cpRgc := *rgc
		cpRgc.allowSubQuery = false
		b = Dialect(dialect).Select(randSelects()...).From(randQuery(dialect, &cpRgc), randTableName(0))
	} else {
		b = Dialect(dialect).Select(randSelects()...).From(randTableName(0))
	}
	if rgc.allowJoin {
		b = randJoin(b, 3)
	}
	if rgc.allowCond && rand.Intn(1000) >= 500 {
		b = b.Where(randCond(b.selects, 3))
	}
	if rgc.allowLimit && rand.Intn(1000) >= 500 {
		b = randLimit(b)
	}
	if rgc.allowOrderBy && rand.Intn(1000) >= 500 {
		b = randOrderBy(b)
	}
	if rgc.allowHaving && rand.Intn(1000) >= 500 {
		b = randHaving(b)
	}
	if rgc.allowGroupBy && rand.Intn(1000) >= 500 {
		b = randGroupBy(b)
	}

	return b
}

func randDialect() string {
	dialects := []string{MYSQL, ORACLE, MSSQL, SQLITE, POSTGRES}

	return dialects[rand.Intn(len(dialects))]
}

func randSelects() []string {
	if rand.Intn(1000) > 900 {
		return []string{"*"}
	}

	rdx := rand.Intn(len(queryFields) / 2)
	return queryFields[rdx:]
}

func randTableName(offset int) string {
	return fmt.Sprintf("table%v", rand.Intn(10)+offset)
}

func randJoin(b *Builder, lessThan int) *Builder {
	if lessThan <= 0 {
		return b
	}

	times := rand.Intn(lessThan)

	for i := 0; i < times; i++ {
		tableName := randTableName(i * 10)
		b = b.Join("", tableName, fmt.Sprintf("%v.id = %v.id", b.TableName(), tableName))
	}

	return b
}

func randCond(selects []string, lessThan int) Cond {
	if len(selects) <= 0 {
		return nil
	}

	cond := NewCond()

	times := rand.Intn(lessThan)
	for i := 0; i < times; i++ {
		cond = cond.And(Eq{selects[rand.Intn(len(selects))]: randVal()})
	}

	return cond
}

func randLimit(b *Builder) *Builder {
	r := rand.Intn(1000) + 1
	if r > 500 {
		return b.Limit(r, 1000)
	} else {
		return b.Limit(r)
	}
}

func randOrderBy(b *Builder) *Builder {
	return b.OrderBy(fmt.Sprintf("%v ASC", b.selects[rand.Intn(len(b.selects))]))
}

func randHaving(b *Builder) *Builder {
	return b.OrderBy(fmt.Sprintf("%v = %v", b.selects[rand.Intn(len(b.selects))], randVal()))
}

func randGroupBy(b *Builder) *Builder {
	return b.GroupBy(fmt.Sprintf("%v = %v", b.selects[rand.Intn(len(b.selects))], randVal()))
}

func randVal() interface{} {
	return expectedValues[rand.Intn(len(expectedValues))]
}
