// Copyright 2018 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,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	"sort"
	"testing"
	"time"

	"github.com/pingcap/kvproto/pkg/kvrpcpb"
	"github.com/pingcap/log"
	"github.com/pingcap/tidb/config"
	"github.com/pingcap/tidb/ddl"
	"github.com/pingcap/tidb/domain"
	"github.com/pingcap/tidb/infoschema"
	"github.com/pingcap/tidb/kv"
	"github.com/pingcap/tidb/meta"
	"github.com/pingcap/tidb/parser/model"
	"github.com/pingcap/tidb/parser/mysql"
	"github.com/pingcap/tidb/planner/core"
	"github.com/pingcap/tidb/session"
	"github.com/pingcap/tidb/sessionctx"
	"github.com/pingcap/tidb/sessionctx/binloginfo"
	"github.com/pingcap/tidb/sessionctx/stmtctx"
	"github.com/pingcap/tidb/store/helper"
	"github.com/pingcap/tidb/store/mockstore"
	"github.com/pingcap/tidb/tablecodec"
	"github.com/pingcap/tidb/testkit"
	"github.com/pingcap/tidb/testkit/external"
	"github.com/pingcap/tidb/types"
	"github.com/pingcap/tidb/util/codec"
	"github.com/pingcap/tidb/util/deadlockhistory"
	"github.com/pingcap/tidb/util/rowcodec"
	"github.com/stretchr/testify/require"
	"github.com/tikv/client-go/v2/tikv"
	"go.uber.org/zap"
)

type basicHTTPHandlerTestSuite struct {
	*testServerClient
	server  *Server
	store   kv.Storage
	domain  *domain.Domain
	tidbdrv *TiDBDriver
	sh      *StatsHandler
}

func createBasicHTTPHandlerTestSuite() *basicHTTPHandlerTestSuite {
	ts := &basicHTTPHandlerTestSuite{}
	ts.testServerClient = newTestServerClient()
	return ts
}

func TestRegionIndexRange(t *testing.T) {
	sTableID := int64(3)
	sIndex := int64(11)
	eTableID := int64(9)
	recordID := int64(133)
	indexValues := []types.Datum{
		types.NewIntDatum(100),
		types.NewBytesDatum([]byte("foobar")),
		types.NewFloat64Datum(-100.25),
	}
	expectIndexValues := make([]string, 0, len(indexValues))
	for _, v := range indexValues {
		str, err := v.ToString()
		if err != nil {
			str = fmt.Sprintf("%d-%v", v.Kind(), v.GetValue())
		}
		expectIndexValues = append(expectIndexValues, str)
	}
	encodedValue, err := codec.EncodeKey(&stmtctx.StatementContext{TimeZone: time.Local}, nil, indexValues...)
	require.NoError(t, err)

	startKey := tablecodec.EncodeIndexSeekKey(sTableID, sIndex, encodedValue)
	recordPrefix := tablecodec.GenTableRecordPrefix(eTableID)
	endKey := tablecodec.EncodeRecordKey(recordPrefix, kv.IntHandle(recordID))

	region := &tikv.KeyLocation{
		Region:   tikv.RegionVerID{},
		StartKey: startKey,
		EndKey:   endKey,
	}
	r, err := helper.NewRegionFrameRange(region)
	require.NoError(t, err)
	require.Equal(t, sIndex, r.First.IndexID)
	require.False(t, r.First.IsRecord)
	require.Equal(t, int64(0), r.First.RecordID)
	require.Equal(t, expectIndexValues, r.First.IndexValues)
	require.Equal(t, recordID, r.Last.RecordID)
	require.Nil(t, r.Last.IndexValues)

	testCases := []struct {
		tableID int64
		indexID int64
		isCover bool
	}{
		{2, 0, false},
		{3, 0, true},
		{9, 0, true},
		{10, 0, false},
		{2, 10, false},
		{3, 10, false},
		{3, 11, true},
		{3, 20, true},
		{9, 10, true},
		{10, 1, false},
	}
	for _, c := range testCases {
		var f *helper.FrameItem
		if c.indexID == 0 {
			f = r.GetRecordFrame(c.tableID, "", "", false)
		} else {
			f = r.GetIndexFrame(c.tableID, c.indexID, "", "", "")
		}
		if c.isCover {
			require.NotNil(t, f)
		} else {
			require.Nil(t, f)
		}
	}
}

func TestRegionCommonHandleRange(t *testing.T) {
	sTableID := int64(3)
	indexValues := []types.Datum{
		types.NewIntDatum(100),
		types.NewBytesDatum([]byte("foobar")),
		types.NewFloat64Datum(-100.25),
	}
	expectIndexValues := make([]string, 0, len(indexValues))
	for _, v := range indexValues {
		str, err := v.ToString()
		if err != nil {
			str = fmt.Sprintf("%d-%v", v.Kind(), v.GetValue())
		}
		expectIndexValues = append(expectIndexValues, str)
	}
	encodedValue, err := codec.EncodeKey(&stmtctx.StatementContext{TimeZone: time.Local}, nil, indexValues...)
	require.NoError(t, err)

	startKey := tablecodec.EncodeRowKey(sTableID, encodedValue)

	region := &tikv.KeyLocation{
		Region:   tikv.RegionVerID{},
		StartKey: startKey,
		EndKey:   nil,
	}
	r, err := helper.NewRegionFrameRange(region)
	require.NoError(t, err)
	require.True(t, r.First.IsRecord)
	require.Equal(t, int64(0), r.First.RecordID)
	require.Equal(t, expectIndexValues, r.First.IndexValues)
	require.Equal(t, "PRIMARY", r.First.IndexName)
	require.Equal(t, int64(0), r.Last.RecordID)
	require.Nil(t, r.Last.IndexValues)
}

func TestRegionIndexRangeWithEndNoLimit(t *testing.T) {
	sTableID := int64(15)
	startKey := tablecodec.GenTableRecordPrefix(sTableID)
	endKey := []byte("z_aaaaafdfd")
	region := &tikv.KeyLocation{
		Region:   tikv.RegionVerID{},
		StartKey: startKey,
		EndKey:   endKey,
	}
	r, err := helper.NewRegionFrameRange(region)
	require.NoError(t, err)
	require.True(t, r.First.IsRecord)
	require.True(t, r.Last.IsRecord)
	require.NotNil(t, r.GetRecordFrame(300, "", "", false))
	require.NotNil(t, r.GetIndexFrame(200, 100, "", "", ""))
}

func TestRegionIndexRangeWithStartNoLimit(t *testing.T) {
	eTableID := int64(9)
	startKey := []byte("m_aaaaafdfd")
	endKey := tablecodec.GenTableRecordPrefix(eTableID)
	region := &tikv.KeyLocation{
		Region:   tikv.RegionVerID{},
		StartKey: startKey,
		EndKey:   endKey,
	}
	r, err := helper.NewRegionFrameRange(region)
	require.NoError(t, err)
	require.False(t, r.First.IsRecord)
	require.True(t, r.Last.IsRecord)
	require.NotNil(t, r.GetRecordFrame(3, "", "", false))
	require.NotNil(t, r.GetIndexFrame(8, 1, "", "", ""))
}

func TestRegionsAPI(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	ts.prepareData(t)
	resp, err := ts.fetchStatus("/tables/tidb/t/regions")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	decoder := json.NewDecoder(resp.Body)

	var data TableRegions
	err = decoder.Decode(&data)
	require.NoError(t, err)
	require.True(t, len(data.RecordRegions) > 0)

	// list region
	for _, region := range data.RecordRegions {
		require.True(t, ts.regionContainsTable(t, region.ID, data.TableID))
	}
}

func TestRegionsAPIForClusterIndex(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	ts.prepareData(t)
	resp, err := ts.fetchStatus("/tables/tidb/t/regions")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	decoder := json.NewDecoder(resp.Body)
	var data TableRegions
	err = decoder.Decode(&data)
	require.NoError(t, err)
	require.True(t, len(data.RecordRegions) > 0)
	// list region
	for _, region := range data.RecordRegions {
		resp, err := ts.fetchStatus(fmt.Sprintf("/regions/%d", region.ID))
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)
		decoder := json.NewDecoder(resp.Body)
		var data RegionDetail
		err = decoder.Decode(&data)
		require.NoError(t, err)
		frameCnt := 0
		for _, f := range data.Frames {
			if f.DBName == "tidb" && f.TableName == "t" {
				frameCnt++
			}
		}
		// frameCnt = clustered primary key + secondary index(idx) = 2.
		require.Equal(t, 2, frameCnt)
		require.NoError(t, resp.Body.Close())
	}
}

func TestRangesAPI(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	ts.prepareData(t)
	resp, err := ts.fetchStatus("/tables/tidb/t/ranges")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	decoder := json.NewDecoder(resp.Body)

	var data TableRanges
	err = decoder.Decode(&data)
	require.NoError(t, err)
	require.Equal(t, "t", data.TableName)
	require.Equal(t, 2, len(data.Indices))
	_, ok := data.Indices["PRIMARY"]
	require.True(t, ok)
	_, ok = data.Indices["idx"]
	require.True(t, ok)
}

func (ts *basicHTTPHandlerTestSuite) regionContainsTable(t *testing.T, regionID uint64, tableID int64) bool {
	resp, err := ts.fetchStatus(fmt.Sprintf("/regions/%d", regionID))
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	decoder := json.NewDecoder(resp.Body)
	var data RegionDetail
	err = decoder.Decode(&data)
	require.NoError(t, err)
	for _, index := range data.Frames {
		if index.TableID == tableID {
			return true
		}
	}
	return false
}

func TestListTableRegions(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	ts.prepareData(t)
	// Test list table regions with error
	resp, err := ts.fetchStatus("/tables/fdsfds/aaa/regions")
	require.NoError(t, err)
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/tables/tidb/pt/regions")
	require.NoError(t, err)

	var data []*TableRegions
	dec := json.NewDecoder(resp.Body)
	err = dec.Decode(&data)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	region := data[1]
	resp, err = ts.fetchStatus(fmt.Sprintf("/regions/%d", region.TableID))
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
}

func TestListTableRanges(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	ts.prepareData(t)
	// Test list table regions with error
	resp, err := ts.fetchStatus("/tables/fdsfds/aaa/ranges")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)

	resp, err = ts.fetchStatus("/tables/tidb/pt/ranges")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()

	var data []*TableRanges
	dec := json.NewDecoder(resp.Body)
	err = dec.Decode(&data)
	require.NoError(t, err)
	require.Equal(t, 3, len(data))
	for i, partition := range data {
		require.Equal(t, fmt.Sprintf("p%d", i), partition.TableName)
	}
}

func TestGetRegionByIDWithError(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/regions/xxx")
	require.NoError(t, err)
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	defer func() { require.NoError(t, resp.Body.Close()) }()
}

func TestBinlogRecover(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	binloginfo.EnableSkipBinlogFlag()
	require.Equal(t, true, binloginfo.IsBinlogSkipped())
	resp, err := ts.fetchStatus("/binlog/recover")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())

	// Invalid operation will use the default operation.
	binloginfo.EnableSkipBinlogFlag()
	require.Equal(t, true, binloginfo.IsBinlogSkipped())
	resp, err = ts.fetchStatus("/binlog/recover?op=abc")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())

	binloginfo.EnableSkipBinlogFlag()
	require.Equal(t, true, binloginfo.IsBinlogSkipped())
	resp, err = ts.fetchStatus("/binlog/recover?op=abc&seconds=1")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())

	binloginfo.EnableSkipBinlogFlag()
	require.Equal(t, true, binloginfo.IsBinlogSkipped())
	binloginfo.AddOneSkippedCommitter()
	resp, err = ts.fetchStatus("/binlog/recover?op=abc&seconds=1")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())
	binloginfo.RemoveOneSkippedCommitter()

	binloginfo.AddOneSkippedCommitter()
	require.Equal(t, int32(1), binloginfo.SkippedCommitterCount())
	resp, err = ts.fetchStatus("/binlog/recover?op=reset")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, int32(0), binloginfo.SkippedCommitterCount())

	binloginfo.EnableSkipBinlogFlag()
	resp, err = ts.fetchStatus("/binlog/recover?op=nowait")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())

	// Only the first should work.
	binloginfo.EnableSkipBinlogFlag()
	resp, err = ts.fetchStatus("/binlog/recover?op=nowait&op=reset")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, false, binloginfo.IsBinlogSkipped())

	resp, err = ts.fetchStatus("/binlog/recover?op=status")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusOK, resp.StatusCode)
}

func (ts *basicHTTPHandlerTestSuite) startServer(t *testing.T) {
	var err error
	ts.store, err = mockstore.NewMockStore()
	require.NoError(t, err)
	ts.domain, err = session.BootstrapSession(ts.store)
	require.NoError(t, err)
	ts.tidbdrv = NewTiDBDriver(ts.store)

	cfg := newTestConfig()
	cfg.Store = "tikv"
	cfg.Port = 0
	cfg.Status.StatusPort = 0
	cfg.Status.ReportStatus = true

	server, err := NewServer(cfg, ts.tidbdrv)
	require.NoError(t, err)
	ts.port = getPortFromTCPAddr(server.listener.Addr())
	ts.statusPort = getPortFromTCPAddr(server.statusListener.Addr())
	ts.server = server
	go func() {
		err := server.Run()
		require.NoError(t, err)
	}()
	ts.waitUntilServerOnline()

	do, err := session.GetDomain(ts.store)
	require.NoError(t, err)
	ts.sh = &StatsHandler{do}
}

func getPortFromTCPAddr(addr net.Addr) uint {
	return uint(addr.(*net.TCPAddr).Port)
}

func (ts *basicHTTPHandlerTestSuite) stopServer(t *testing.T) {
	if ts.server != nil {
		ts.server.Close()
	}
	if ts.domain != nil {
		ts.domain.Close()
	}
	if ts.store != nil {
		require.NoError(t, ts.store.Close())
	}
}

func (ts *basicHTTPHandlerTestSuite) prepareData(t *testing.T) {
	db, err := sql.Open("mysql", ts.getDSN())
	require.NoError(t, err)
	defer func() {
		err := db.Close()
		require.NoError(t, err)
	}()
	dbt := testkit.NewDBTestKit(t, db)

	dbt.MustExec("create database tidb;")
	dbt.MustExec("use tidb;")
	dbt.MustExec("create table tidb.test (a int auto_increment primary key, b varchar(20));")
	dbt.MustExec("insert tidb.test values (1, 1);")
	txn1, err := dbt.GetDB().Begin()
	require.NoError(t, err)
	_, err = txn1.Exec("update tidb.test set b = b + 1 where a = 1;")
	require.NoError(t, err)
	_, err = txn1.Exec("insert tidb.test values (2, 2);")
	require.NoError(t, err)
	_, err = txn1.Exec("insert tidb.test (a) values (3);")
	require.NoError(t, err)
	_, err = txn1.Exec("insert tidb.test values (4, '');")
	require.NoError(t, err)
	err = txn1.Commit()
	require.NoError(t, err)
	dbt.MustExec("alter table tidb.test add index idx1 (a, b);")
	dbt.MustExec("alter table tidb.test drop index idx1;")
	dbt.MustExec("alter table tidb.test add index idx1 (a, b);")
	dbt.MustExec("alter table tidb.test add unique index idx2 (a, b);")

	dbt.MustExec(`create table tidb.pt (a int primary key, b varchar(20), key idx(a, b))
partition by range (a)
(partition p0 values less than (256),
 partition p1 values less than (512),
 partition p2 values less than (1024))`)

	txn2, err := dbt.GetDB().Begin()
	require.NoError(t, err)
	_, err = txn2.Exec("insert into tidb.pt values (42, '123')")
	require.NoError(t, err)
	_, err = txn2.Exec("insert into tidb.pt values (256, 'b')")
	require.NoError(t, err)
	_, err = txn2.Exec("insert into tidb.pt values (666, 'def')")
	require.NoError(t, err)
	err = txn2.Commit()
	require.NoError(t, err)
	dbt.MustExec("drop table if exists t")
	dbt.MustExec("create table t (a double, b varchar(20), c int, primary key(a,b) clustered, key idx(c))")
	dbt.MustExec("insert into t values(1.1,'111',1),(2.2,'222',2)")
}

func decodeKeyMvcc(closer io.ReadCloser, t *testing.T, valid bool) {
	decoder := json.NewDecoder(closer)
	var data []helper.MvccKV
	err := decoder.Decode(&data)
	require.NoError(t, err)
	if valid {
		require.NotNil(t, data[0].Value.Info)
		require.Greater(t, len(data[0].Value.Info.Writes), 0)
	} else {
		require.Nil(t, data[0].Value.Info.Lock)
		require.Nil(t, data[0].Value.Info.Writes)
		require.Nil(t, data[0].Value.Info.Values)
	}
}

func TestGetTableMVCC(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)

	resp, err := ts.fetchStatus("/mvcc/key/tidb/test/1")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)
	var data helper.MvccKV
	err = decoder.Decode(&data)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.NotNil(t, data.Value)
	info := data.Value.Info
	require.NotNil(t, info)
	require.Greater(t, len(info.Writes), 0)

	// TODO: Unistore will not return Op_Lock.
	// Use this workaround to support two backend, we can remove this hack after deprecated mocktikv.
	var startTs uint64
	for _, w := range info.Writes {
		if w.Type == kvrpcpb.Op_Lock {
			continue
		}
		startTs = w.StartTs
		break
	}

	resp, err = ts.fetchStatus(fmt.Sprintf("/mvcc/txn/%d/tidb/test", startTs))
	require.NoError(t, err)
	var p2 helper.MvccKV
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&p2)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	for i, expect := range info.Values {
		v2 := p2.Value.Info.Values[i].Value
		require.Equal(t, expect.Value, v2)
	}

	hexKey := p2.Key
	resp, err = ts.fetchStatus("/mvcc/hex/" + hexKey)
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	var data2 helper.MvccKV
	err = decoder.Decode(&data2)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, data, data2)

	resp, err = ts.fetchStatus("/mvcc/key/tidb/test/1?decode=true")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	var data3 map[string]interface{}
	err = decoder.Decode(&data3)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.NotNil(t, data3["key"])
	require.NotNil(t, data3["info"])
	require.NotNil(t, data3["data"])
	require.Nil(t, data3["decode_error"])

	resp, err = ts.fetchStatus("/mvcc/key/tidb/pt(p0)/42?decode=true")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	var data4 map[string]interface{}
	err = decoder.Decode(&data4)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.NotNil(t, data4["key"])
	require.NotNil(t, data4["info"])
	require.NotNil(t, data4["data"])
	require.Nil(t, data4["decode_error"])
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/key/tidb/t/42")
	require.NoError(t, err)
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	require.NoError(t, resp.Body.Close())
	resp, err = ts.fetchStatus("/mvcc/key/tidb/t?a=1.1")
	require.NoError(t, err)
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	require.NoError(t, resp.Body.Close())
	resp, err = ts.fetchStatus("/mvcc/key/tidb/t?a=1.1&b=111&decode=1")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	var data5 map[string]interface{}
	err = decoder.Decode(&data5)
	require.NoError(t, err)
	require.NotNil(t, data4["key"])
	require.NotNil(t, data4["info"])
	require.NotNil(t, data4["data"])
	require.Nil(t, data4["decode_error"])
	require.NoError(t, resp.Body.Close())
}

func TestGetMVCCNotFound(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/mvcc/key/tidb/test/1234")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)
	var data helper.MvccKV
	err = decoder.Decode(&data)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Nil(t, data.Value.Info.Lock)
	require.Nil(t, data.Value.Info.Writes)
	require.Nil(t, data.Value.Info.Values)
}

func TestDecodeColumnValue(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)

	// column is a structure used for test
	type column struct {
		id int64
		tp *types.FieldType
	}
	// Backfill columns.
	c1 := &column{id: 1, tp: types.NewFieldType(mysql.TypeLonglong)}
	c2 := &column{id: 2, tp: types.NewFieldType(mysql.TypeVarchar)}
	c3 := &column{id: 3, tp: types.NewFieldType(mysql.TypeNewDecimal)}
	c4 := &column{id: 4, tp: types.NewFieldType(mysql.TypeTimestamp)}
	cols := []*column{c1, c2, c3, c4}
	row := make([]types.Datum, len(cols))
	row[0] = types.NewIntDatum(100)
	row[1] = types.NewBytesDatum([]byte("abc"))
	row[2] = types.NewDecimalDatum(types.NewDecFromInt(1))
	row[3] = types.NewTimeDatum(types.NewTime(types.FromGoTime(time.Now()), mysql.TypeTimestamp, 6))

	// Encode the row.
	colIDs := make([]int64, 0, 3)
	for _, col := range cols {
		colIDs = append(colIDs, col.id)
	}
	rd := rowcodec.Encoder{Enable: true}
	sc := &stmtctx.StatementContext{TimeZone: time.UTC}
	bs, err := tablecodec.EncodeRow(sc, row, colIDs, nil, nil, &rd)
	require.NoError(t, err)
	require.NotNil(t, bs)
	bin := base64.StdEncoding.EncodeToString(bs)

	unitTest := func(col *column) {
		path := fmt.Sprintf("/tables/%d/%v/%d/%d?rowBin=%s", col.id, col.tp.GetType(), col.tp.GetFlag(), col.tp.GetFlen(), bin)
		resp, err := ts.fetchStatus(path)
		require.NoErrorf(t, err, "url: %v", ts.statusURL(path))
		decoder := json.NewDecoder(resp.Body)
		var data interface{}
		err = decoder.Decode(&data)
		require.NoErrorf(t, err, "url: %v\ndata: %v", ts.statusURL(path), data)
		require.NoError(t, resp.Body.Close())
		colVal, err := types.DatumsToString([]types.Datum{row[col.id-1]}, false)
		require.NoError(t, err)
		require.Equalf(t, colVal, data, "url: %v", ts.statusURL(path))
	}

	for _, col := range cols {
		unitTest(col)
	}

	// Test bin has `+`.
	// 2018-03-08 16:01:00.315313
	bin = "CAIIyAEIBAIGYWJjCAYGAQCBCAgJsZ+TgISg1M8Z"
	row[3] = types.NewTimeDatum(types.NewTime(types.FromGoTime(time.Date(2018, 3, 8, 16, 1, 0, 315313000, time.UTC)), mysql.TypeTimestamp, 6))
	unitTest(cols[3])

	// Test bin has `/`.
	// 2018-03-08 02:44:46.409199
	bin = "CAIIyAEIBAIGYWJjCAYGAQCBCAgJ7/yY8LKF1M8Z"
	row[3] = types.NewTimeDatum(types.NewTime(types.FromGoTime(time.Date(2018, 3, 8, 2, 44, 46, 409199000, time.UTC)), mysql.TypeTimestamp, 6))
	unitTest(cols[3])
}

func TestGetIndexMVCC(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)

	// tests for normal index key
	resp, err := ts.fetchStatus("/mvcc/index/tidb/test/idx1/1?a=1&b=2")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx2/1?a=1&b=2")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	// tests for index key which includes null
	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx1/3?a=3&b")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx2/3?a=3&b")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	// tests for index key which includes empty string
	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx1/4?a=4&b=")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx2/3?a=4&b=")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/t/idx?a=1.1&b=111&c=1")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())

	// tests for wrong key
	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx1/5?a=5&b=1")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, false)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx2/5?a=5&b=1")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, false)
	require.NoError(t, resp.Body.Close())

	// tests for missing column value
	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx1/1?a=1")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)
	var data1 helper.MvccKV
	err = decoder.Decode(&data1)
	require.Error(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/test/idx2/1?a=1")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	var data2 helper.MvccKV
	err = decoder.Decode(&data2)
	require.Error(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/mvcc/index/tidb/pt(p2)/idx/666?a=666&b=def")
	require.NoError(t, err)
	decodeKeyMvcc(resp.Body, t, true)
	require.NoError(t, resp.Body.Close())
}

func TestGetSettings(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/settings")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)
	var settings *config.Config
	err = decoder.Decode(&settings)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	var configBytes []byte
	configBytes, err = json.Marshal(config.GetGlobalConfig())
	require.NoError(t, err)
	var settingBytes []byte
	settingBytes, err = json.Marshal(settings)
	require.NoError(t, err)
	require.Equal(t, configBytes, settingBytes)
}

func TestGetSchema(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/schema")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)
	var dbs []*model.DBInfo
	err = decoder.Decode(&dbs)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	expects := []string{"information_schema", "metrics_schema", "mysql", "performance_schema", "test", "tidb"}
	names := make([]string, len(dbs))
	for i, v := range dbs {
		names[i] = v.Name.L
	}
	sort.Strings(names)
	require.Equal(t, expects, names)
	store := testkit.CreateMockStore(t)

	tk := testkit.NewTestKit(t, store)
	userTbl := external.GetTableByName(t, tk, "mysql", "user")
	resp, err = ts.fetchStatus(fmt.Sprintf("/schema?table_id=%d", userTbl.Meta().ID))
	require.NoError(t, err)
	var ti *model.TableInfo
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "user", ti.Name.L)

	resp, err = ts.fetchStatus("/schema?table_id=a")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/schema?table_id=1")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/schema?table_id=-1")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/schema/tidb")
	require.NoError(t, err)
	var lt []*model.TableInfo
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&lt)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Greater(t, len(lt), 2)

	resp, err = ts.fetchStatus("/schema/abc")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/schema/tidb/test")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "test", ti.Name.L)

	resp, err = ts.fetchStatus("/schema/tidb/abc")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus(fmt.Sprintf("/db-table/%d", userTbl.Meta().ID))
	require.NoError(t, err)
	var dbtbl *dbTableInfo
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&dbtbl)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "user", dbtbl.TableInfo.Name.L)
	require.Equal(t, "mysql", dbtbl.DBInfo.Name.L)
	se, err := session.CreateSession(ts.store)
	require.NoError(t, err)
	require.Equal(t, domain.GetDomain(se.(sessionctx.Context)).InfoSchema().SchemaMetaVersion(), dbtbl.SchemaVersion)

	db, err := sql.Open("mysql", ts.getDSN())
	require.NoError(t, err)
	defer func() {
		err := db.Close()
		require.NoError(t, err)
	}()
	dbt := testkit.NewDBTestKit(t, db)

	dbt.MustExec("create database if not exists test;")
	dbt.MustExec("use test;")
	dbt.MustExec(` create table t1 (id int KEY)
		partition by range (id) (
		PARTITION p0 VALUES LESS THAN (3),
		PARTITION p1 VALUES LESS THAN (5),
		PARTITION p2 VALUES LESS THAN (7),
		PARTITION p3 VALUES LESS THAN (9))`)

	resp, err = ts.fetchStatus("/schema/test/t1")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "t1", ti.Name.L)

	resp, err = ts.fetchStatus(fmt.Sprintf("/db-table/%v", ti.GetPartitionInfo().Definitions[0].ID))
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&dbtbl)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "t1", dbtbl.TableInfo.Name.L)
	require.Equal(t, "test", dbtbl.DBInfo.Name.L)
	require.Equal(t, ti, dbtbl.TableInfo)

	resp, err = ts.fetchStatus(fmt.Sprintf("/schema?table_id=%v", ti.GetPartitionInfo().Definitions[0].ID))
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "t1", ti.Name.L)
	require.Equal(t, ti, ti)
}

func TestAllHistory(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	ts.prepareData(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/ddl/history/?limit=3")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	resp, err = ts.fetchStatus("/ddl/history/?limit=-1")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/ddl/history")
	require.NoError(t, err)
	decoder := json.NewDecoder(resp.Body)

	var jobs []*model.Job
	s, _ := session.CreateSession(ts.server.newTikvHandlerTool().Store.(kv.Storage))
	defer s.Close()
	store := domain.GetDomain(s.(sessionctx.Context)).Store()
	txn, _ := store.Begin()
	txnMeta := meta.NewMeta(txn)
	data, err := ddl.GetAllHistoryDDLJobs(txnMeta)
	require.NoError(t, err)
	err = decoder.Decode(&jobs)

	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, len(data), len(jobs))
	for i := range data {
		// For the jobs that have arguments(job.Args) for GC delete range,
		// the RawArgs should be the same after filtering the spaces.
		data[i].RawArgs = filterSpaces(data[i].RawArgs)
		jobs[i].RawArgs = filterSpaces(jobs[i].RawArgs)
		require.Equal(t, data[i], jobs[i], i)
	}

	// Cover the start_job_id parameter.
	resp, err = ts.fetchStatus("/ddl/history?start_job_id=41")
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.fetchStatus("/ddl/history?start_job_id=41&limit=3")
	require.NoError(t, err)
	decoder = json.NewDecoder(resp.Body)
	err = decoder.Decode(&jobs)
	require.NoError(t, err)

	// The result is in descending order
	lastID := int64(42)
	for _, job := range jobs {
		require.Less(t, job.ID, lastID)
		lastID = job.ID
	}
	require.NoError(t, resp.Body.Close())
}

func filterSpaces(bs []byte) []byte {
	if len(bs) == 0 {
		return nil
	}
	tmp := bs[:0]
	for _, b := range bs {
		// 0xa is the line feed character.
		// 0xd is the carriage return character.
		// 0x20 is the space character.
		if b != 0xa && b != 0xd && b != 0x20 {
			tmp = append(tmp, b)
		}
	}
	return tmp
}

func dummyRecord() *deadlockhistory.DeadlockRecord {
	return &deadlockhistory.DeadlockRecord{}
}

func TestPprof(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	retryTime := 100
	for retry := 0; retry < retryTime; retry++ {
		resp, err := ts.fetchStatus("/debug/pprof/heap")
		if err == nil {
			_, err = io.ReadAll(resp.Body)
			require.NoError(t, err)
			err = resp.Body.Close()
			require.NoError(t, err)
			return
		}
		time.Sleep(time.Millisecond * 10)
	}
	log.Fatal("failed to get profile for %d retries in every 10 ms", zap.Int("retryTime", retryTime))
}

func TestHotRegionInfo(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/regions/hot")
	require.NoError(t, err)
	defer func() { require.NoError(t, resp.Body.Close()) }()
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestDebugZip(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()
	ts.startServer(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/debug/zip?seconds=1")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	b, err := httputil.DumpResponse(resp, true)
	require.NoError(t, err)
	require.Greater(t, len(b), 0)
	require.NoError(t, resp.Body.Close())
}

func TestCheckCN(t *testing.T) {
	s := &Server{cfg: &config.Config{Security: config.Security{ClusterVerifyCN: []string{"a ", "b", "c"}}}}
	tlsConfig := &tls.Config{}
	s.setCNChecker(tlsConfig)
	require.NotNil(t, tlsConfig.VerifyPeerCertificate)
	err := tlsConfig.VerifyPeerCertificate(nil, [][]*x509.Certificate{{{Subject: pkix.Name{CommonName: "a"}}}})
	require.NoError(t, err)
	err = tlsConfig.VerifyPeerCertificate(nil, [][]*x509.Certificate{{{Subject: pkix.Name{CommonName: "b"}}}})
	require.NoError(t, err)
	err = tlsConfig.VerifyPeerCertificate(nil, [][]*x509.Certificate{{{Subject: pkix.Name{CommonName: "d"}}}})
	require.Error(t, err)
}

func TestDDLHookHandler(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()

	ts.startServer(t)
	defer ts.stopServer(t)
	resp, err := ts.fetchStatus("/test/ddl/hook")
	require.NoError(t, err)
	require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	require.NoError(t, resp.Body.Close())

	resp, err = ts.postStatus("/test/ddl/hook", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(`ddl_hook=ctc_hook`)))
	require.NoError(t, err)
	require.NotNil(t, resp)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "\"success!\"", string(body))
	require.Equal(t, http.StatusOK, resp.StatusCode)

	resp, err = ts.postStatus("/test/ddl/hook", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(`ddl_hook=default_hook`)))
	require.NoError(t, err)
	require.NotNil(t, resp)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())
	require.Equal(t, "\"success!\"", string(body))
	require.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestWriteDBTablesData(t *testing.T) {
	// No table in a schema.
	info := infoschema.MockInfoSchema([]*model.TableInfo{})
	rc := httptest.NewRecorder()
	tbs := info.SchemaTables(model.NewCIStr("test"))
	require.Equal(t, 0, len(tbs))
	writeDBTablesData(rc, tbs)
	var ti []*model.TableInfo
	decoder := json.NewDecoder(rc.Body)
	err := decoder.Decode(&ti)
	require.NoError(t, err)
	require.Equal(t, 0, len(ti))

	// One table in a schema.
	info = infoschema.MockInfoSchema([]*model.TableInfo{core.MockSignedTable()})
	rc = httptest.NewRecorder()
	tbs = info.SchemaTables(model.NewCIStr("test"))
	require.Equal(t, 1, len(tbs))
	writeDBTablesData(rc, tbs)
	decoder = json.NewDecoder(rc.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.Equal(t, 1, len(ti))
	require.Equal(t, ti[0].ID, tbs[0].Meta().ID)
	require.Equal(t, ti[0].Name.String(), tbs[0].Meta().Name.String())

	// Two tables in a schema.
	info = infoschema.MockInfoSchema([]*model.TableInfo{core.MockSignedTable(), core.MockUnsignedTable()})
	rc = httptest.NewRecorder()
	tbs = info.SchemaTables(model.NewCIStr("test"))
	require.Equal(t, 2, len(tbs))
	writeDBTablesData(rc, tbs)
	decoder = json.NewDecoder(rc.Body)
	err = decoder.Decode(&ti)
	require.NoError(t, err)
	require.Equal(t, 2, len(ti))
	require.Equal(t, ti[0].ID, tbs[0].Meta().ID)
	require.Equal(t, ti[1].ID, tbs[1].Meta().ID)
	require.Equal(t, ti[0].Name.String(), tbs[0].Meta().Name.String())
	require.Equal(t, ti[1].Name.String(), tbs[1].Meta().Name.String())
}

func TestSetLabels(t *testing.T) {
	ts := createBasicHTTPHandlerTestSuite()

	ts.startServer(t)
	defer ts.stopServer(t)

	testUpdateLabels := func(labels, expected map[string]string) {
		buffer := bytes.NewBuffer([]byte{})
		require.Nil(t, json.NewEncoder(buffer).Encode(labels))
		resp, err := ts.postStatus("/labels", "application/json", buffer)
		require.NoError(t, err)
		require.NotNil(t, resp)
		defer func() {
			require.NoError(t, resp.Body.Close())
		}()
		require.Equal(t, http.StatusOK, resp.StatusCode)
		newLabels := config.GetGlobalConfig().Labels
		require.Equal(t, newLabels, expected)
	}

	labels := map[string]string{
		"zone": "us-west-1",
		"test": "123",
	}
	testUpdateLabels(labels, labels)

	updated := map[string]string{
		"zone": "bj-1",
	}
	labels["zone"] = "bj-1"
	testUpdateLabels(updated, labels)

	// reset the global variable
	config.GetGlobalConfig().Labels = map[string]string{}
}
