// Copyright 2019 PingCAP, Inc.
//
// Licensed 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package domain

import (
	"sync"
	"sync/atomic"
	"time"

	. "github.com/pingcap/check"
	"github.com/pingcap/parser/ast"
	"github.com/pingcap/parser/charset"
	"github.com/pingcap/parser/model"
	"github.com/pingcap/parser/mysql"
	"github.com/pingcap/tidb/sessionctx/variable"
	"github.com/pingcap/tidb/store/mockstore"
	"github.com/pingcap/tidb/types"
	"github.com/pingcap/tidb/util/chunk"
	"github.com/pingcap/tidb/util/stmtsummary"
	"github.com/pingcap/tidb/util/testleak"
)

var _ = SerialSuites(&testGVCSuite{})

type testGVCSuite struct{}

func (gvcSuite *testGVCSuite) TestSimple(c *C) {
	defer testleak.AfterTest(c)()
	testleak.BeforeTest()

	store, err := mockstore.NewMockTikvStore()
	c.Assert(err, IsNil)
	defer store.Close()
	ddlLease := 50 * time.Millisecond
	dom := NewDomain(store, ddlLease, 0, mockFactory)
	err = dom.Init(ddlLease, sysMockFactory)
	c.Assert(err, IsNil)
	defer dom.Close()

	// Get empty global vars cache.
	gvc := dom.GetGlobalVarsCache()
	succ, rows, fields := gvc.Get()
	c.Assert(succ, IsFalse)
	c.Assert(rows, IsNil)
	c.Assert(fields, IsNil)
	// Get a variable from global vars cache.
	rf := getResultField("c", 1, 0)
	rf1 := getResultField("c1", 2, 1)
	ft := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}
	ft1 := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}
	ck := chunk.NewChunkWithCapacity([]*types.FieldType{ft, ft1}, 1024)
	ck.AppendString(0, "variable1")
	ck.AppendString(1, "value1")
	row := ck.GetRow(0)
	gvc.Update([]chunk.Row{row}, []*ast.ResultField{rf, rf1})
	succ, rows, fields = gvc.Get()
	c.Assert(succ, IsTrue)
	c.Assert(rows[0], Equals, row)
	c.Assert(fields, DeepEquals, []*ast.ResultField{rf, rf1})
	// Disable the cache.
	gvc.Disable()
	succ, rows, fields = gvc.Get()
	c.Assert(succ, IsFalse)
	c.Assert(rows[0], Equals, row)
	c.Assert(fields, DeepEquals, []*ast.ResultField{rf, rf1})
}

func getResultField(colName string, id, offset int) *ast.ResultField {
	return &ast.ResultField{
		Column: &model.ColumnInfo{
			Name:   model.NewCIStr(colName),
			ID:     int64(id),
			Offset: offset,
			FieldType: types.FieldType{
				Tp:      mysql.TypeString,
				Charset: charset.CharsetUTF8,
				Collate: charset.CollationUTF8,
			},
		},
		TableAsName: model.NewCIStr("tbl"),
		DBName:      model.NewCIStr("test"),
	}
}

func (gvcSuite *testGVCSuite) TestConcurrentOneFlight(c *C) {
	defer testleak.AfterTest(c)()
	testleak.BeforeTest()
	gvc := &GlobalVariableCache{}
	succ, rows, fields := gvc.Get()
	c.Assert(succ, IsFalse)
	c.Assert(rows, IsNil)
	c.Assert(fields, IsNil)

	// Get a variable from global vars cache.
	rf := getResultField("c", 1, 0)
	rf1 := getResultField("c1", 2, 1)
	ft := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}
	ft1 := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}
	ckLow := chunk.NewChunkWithCapacity([]*types.FieldType{ft, ft1}, 1024)
	val := "fromStorage"
	val1 := "fromStorage1"
	ckLow.AppendString(0, val)
	ckLow.AppendString(1, val1)

	// Let cache become invalid, and try concurrent load
	counter := int32(0)
	waitToStart := new(sync.WaitGroup)
	waitToStart.Add(1)
	gvc.lastModify = time.Now().Add(time.Duration(-10) * time.Second)
	loadFunc := func() ([]chunk.Row, []*ast.ResultField, error) {
		time.Sleep(100 * time.Millisecond)
		atomic.AddInt32(&counter, 1)
		return []chunk.Row{ckLow.GetRow(0)}, []*ast.ResultField{rf, rf1}, nil
	}
	wg := new(sync.WaitGroup)
	worker := 100
	resArray := make([]loadResult, worker)
	for i := 0; i < worker; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			waitToStart.Wait()
			resRow, resField, _ := gvc.LoadGlobalVariables(loadFunc)
			resArray[idx].rows = resRow
			resArray[idx].fields = resField
		}(i)
	}
	waitToStart.Done()
	wg.Wait()
	succ, rows, fields = gvc.Get()
	c.Assert(counter, Equals, int32(1))
	c.Assert(resArray[0].rows[0].GetString(0), Equals, val)
	c.Assert(resArray[0].rows[0].GetString(1), Equals, val1)
	for i := 0; i < worker; i++ {
		c.Assert(resArray[0].rows[0], Equals, resArray[i].rows[0])
		c.Assert(resArray[i].rows[0].GetString(0), Equals, val)
		c.Assert(resArray[i].rows[0].GetString(1), Equals, val1)
	}
	// Validate cache
	c.Assert(succ, IsTrue)
	c.Assert(rows[0], Equals, resArray[0].rows[0])
	c.Assert(fields, DeepEquals, []*ast.ResultField{rf, rf1})
}

func (gvcSuite *testGVCSuite) TestCheckEnableStmtSummary(c *C) {
	defer testleak.AfterTest(c)()
	testleak.BeforeTest()

	store, err := mockstore.NewMockTikvStore()
	c.Assert(err, IsNil)
	defer store.Close()
	ddlLease := 50 * time.Millisecond
	dom := NewDomain(store, ddlLease, 0, mockFactory)
	err = dom.Init(ddlLease, sysMockFactory)
	c.Assert(err, IsNil)
	defer dom.Close()

	gvc := dom.GetGlobalVarsCache()

	rf := getResultField("c", 1, 0)
	rf1 := getResultField("c1", 2, 1)
	ft := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}
	ft1 := &types.FieldType{
		Tp:      mysql.TypeString,
		Charset: charset.CharsetBin,
		Collate: charset.CollationBin,
	}

	stmtsummary.StmtSummaryByDigestMap.SetEnabled("0", false)
	ck := chunk.NewChunkWithCapacity([]*types.FieldType{ft, ft1}, 1024)
	ck.AppendString(0, variable.TiDBEnableStmtSummary)
	ck.AppendString(1, "1")
	row := ck.GetRow(0)
	gvc.Update([]chunk.Row{row}, []*ast.ResultField{rf, rf1})
	c.Assert(stmtsummary.StmtSummaryByDigestMap.Enabled(), Equals, true)

	ck = chunk.NewChunkWithCapacity([]*types.FieldType{ft, ft1}, 1024)
	ck.AppendString(0, variable.TiDBEnableStmtSummary)
	ck.AppendString(1, "0")
	row = ck.GetRow(0)
	gvc.Update([]chunk.Row{row}, []*ast.ResultField{rf, rf1})
	c.Assert(stmtsummary.StmtSummaryByDigestMap.Enabled(), Equals, false)
}
