// Copyright 2022 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 cache_test

import (
	"context"
	"fmt"
	"math"
	"math/rand/v2"
	"slices"
	"sort"
	"sync"
	"testing"
	"time"

	"github.com/pingcap/kvproto/pkg/metapb"
	"github.com/pingcap/tidb/pkg/infoschema"
	"github.com/pingcap/tidb/pkg/kv"
	"github.com/pingcap/tidb/pkg/parser/model"
	"github.com/pingcap/tidb/pkg/parser/mysql"
	"github.com/pingcap/tidb/pkg/store/helper"
	"github.com/pingcap/tidb/pkg/tablecodec"
	"github.com/pingcap/tidb/pkg/testkit"
	"github.com/pingcap/tidb/pkg/ttl/cache"
	"github.com/pingcap/tidb/pkg/types"
	"github.com/pingcap/tidb/pkg/util/codec"
	"github.com/pingcap/tidb/pkg/util/logutil"
	"github.com/stretchr/testify/require"
	"github.com/tikv/client-go/v2/tikv"
	pd "github.com/tikv/pd/client"
	"go.uber.org/zap"
)

func newMockRegion(regionID uint64, startKey []byte, endKey []byte) *pd.Region {
	leader := &metapb.Peer{
		Id:      regionID,
		StoreId: 1,
		Role:    metapb.PeerRole_Voter,
	}

	return &pd.Region{
		Meta: &metapb.Region{
			Id:       regionID,
			StartKey: startKey,
			EndKey:   endKey,
			Peers:    []*metapb.Peer{leader},
		},
		Leader: leader,
	}
}

type mockPDClient struct {
	t  *testing.T
	mu sync.Mutex
	pd.Client

	nextRegionID  uint64
	regions       []*pd.Region
	regionsSorted bool
}

func (c *mockPDClient) ScanRegions(_ context.Context, key, endKey []byte, limit int, _ ...pd.GetRegionOption) ([]*pd.Region, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if len(c.regions) == 0 {
		return []*pd.Region{newMockRegion(1, []byte{}, []byte{0xFF, 0xFF})}, nil
	}

	if !c.regionsSorted {
		sort.Slice(c.regions, func(i, j int) bool {
			return kv.Key(c.regions[i].Meta.StartKey).Cmp(c.regions[j].Meta.StartKey) < 0
		})
		c.regionsSorted = true
	}

	regions := []*pd.Region{newMockRegion(1, []byte{}, c.regions[0].Meta.StartKey)}
	regions = append(regions, c.regions...)
	regions = append(regions, newMockRegion(2, c.regions[len(c.regions)-1].Meta.EndKey, []byte{0xFF, 0xFF, 0xFF}))

	result := make([]*pd.Region, 0)
	for _, r := range regions {
		if kv.Key(r.Meta.StartKey).Cmp(endKey) >= 0 {
			continue
		}

		if kv.Key(r.Meta.EndKey).Cmp(key) <= 0 {
			continue
		}

		if len(result) >= limit {
			break
		}

		result = append(result, r)
	}
	return result, nil
}

func (c *mockPDClient) GetRegionByID(ctx context.Context, regionID uint64, opts ...pd.GetRegionOption) (*pd.Region, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	for _, r := range c.regions {
		if r.Meta.Id == regionID {
			return r, nil
		}
	}
	return nil, fmt.Errorf("region %d not found", regionID)
}

func (c *mockPDClient) GetRegion(ctx context.Context, key []byte, _ ...pd.GetRegionOption) (*pd.Region, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	for _, r := range c.regions {
		if kv.Key(r.Meta.StartKey).Cmp(key) <= 0 && kv.Key(r.Meta.EndKey).Cmp(key) > 0 {
			return r, nil
		}
	}

	return nil, fmt.Errorf("region not found for key %s", key)
}

func (c *mockPDClient) BatchScanRegions(ctx context.Context, ranges []pd.KeyRange, limit int, opts ...pd.GetRegionOption) ([]*pd.Region, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	logutil.BgLogger().Info("BatchScanRegions", zap.Any("ranges", ranges))

	var reg []*pd.Region
	for _, r := range c.regions {
		for _, kr := range ranges {
			if kv.Key(r.Meta.StartKey).Cmp(kr.EndKey) < 0 && kv.Key(r.Meta.EndKey).Cmp(kr.StartKey) > 0 {
				reg = append(reg, r)
				break
			}
		}
	}

	return reg, nil
}

func (c *mockPDClient) GetStore(_ context.Context, storeID uint64) (*metapb.Store, error) {
	return &metapb.Store{
		Id:      storeID,
		Address: fmt.Sprintf("127.0.0.%d", storeID),
	}, nil
}

func (c *mockPDClient) GetClusterID(_ context.Context) uint64 {
	return 1
}

func (c *mockPDClient) addRegion(key, endKey []byte) {
	c.mu.Lock()
	defer c.mu.Unlock()

	require.True(c.t, kv.Key(endKey).Cmp(key) > 0)
	if len(c.regions) > 0 {
		lastRegion := c.regions[len(c.regions)-1]
		require.True(c.t, kv.Key(endKey).Cmp(lastRegion.Meta.EndKey) >= 0)
	}

	regionID := c.nextRegionID
	c.nextRegionID++
	leader := &metapb.Peer{
		Id:      regionID,
		StoreId: 1,
		Role:    metapb.PeerRole_Voter,
	}

	c.regions = append(c.regions, &pd.Region{
		Meta: &metapb.Region{
			Id:       regionID,
			StartKey: key,
			EndKey:   endKey,
			Peers:    []*metapb.Peer{leader},
			RegionEpoch: &metapb.RegionEpoch{
				ConfVer: 1,
				Version: 1,
			},
		},
		Leader: leader,
	})

	c.regionsSorted = false
}

// randomlyMergeRegions merges two region, and returned whether it has merged
func (c *mockPDClient) randomlyMergeRegions() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	if len(c.regions) < 2 {
		return len(c.regions)
	}

	// Randomly merge two regions
	if !c.regionsSorted {
		sort.Slice(c.regions, func(i, j int) bool {
			return kv.Key(c.regions[i].Meta.StartKey).Cmp(c.regions[j].Meta.StartKey) < 0
		})
		c.regionsSorted = true
	}
	r1Idx := rand.IntN(len(c.regions) - 1)
	r2Idx := r1Idx + 1

	newRegion := &pd.Region{
		Meta: &metapb.Region{
			Id:       c.regions[r1Idx].Meta.Id,
			StartKey: c.regions[r1Idx].Meta.StartKey,
			EndKey:   c.regions[r2Idx].Meta.EndKey,
			Peers:    c.regions[r1Idx].Meta.Peers,
			RegionEpoch: &metapb.RegionEpoch{
				ConfVer: c.regions[r1Idx].Meta.RegionEpoch.ConfVer,
				Version: max(c.regions[r1Idx].Meta.RegionEpoch.Version,
					c.regions[r2Idx].Meta.RegionEpoch.Version) + 1,
			},
		},
		Leader: c.regions[r1Idx].Leader,
	}
	c.regions = slices.Delete(c.regions, r1Idx, r1Idx+2)
	c.regions = append(c.regions, newRegion)
	c.regionsSorted = false
	return len(c.regions)
}

type mockTiKVStore struct {
	t *testing.T
	helper.Storage
	pdClient *mockPDClient
	cache    *tikv.RegionCache
}

func newMockTiKVStore(t *testing.T) *mockTiKVStore {
	pdClient := &mockPDClient{t: t, nextRegionID: 1000}
	s := &mockTiKVStore{
		t:        t,
		pdClient: pdClient,
		cache:    tikv.NewRegionCache(pdClient),
	}
	s.refreshCache()
	t.Cleanup(func() {
		s.cache.Close()
	})
	return s
}

func (s *mockTiKVStore) addRegionBeginWithTablePrefix(tableID int64, handle kv.Handle) *mockTiKVStore {
	start := tablecodec.GenTablePrefix(tableID)
	end := tablecodec.EncodeRowKeyWithHandle(tableID, handle)
	s.pdClient.addRegion(start, end)
	s.refreshCache()
	return s
}

func (s *mockTiKVStore) addRegionEndWithTablePrefix(handle kv.Handle, tableID int64) *mockTiKVStore {
	start := tablecodec.EncodeRowKeyWithHandle(tableID, handle)
	end := tablecodec.GenTablePrefix(tableID + 1)
	s.pdClient.addRegion(start, end)
	s.refreshCache()
	return s
}

func (s *mockTiKVStore) addRegionWithTablePrefix(tableID int64, start kv.Handle, end kv.Handle) *mockTiKVStore {
	startKey := tablecodec.EncodeRowKeyWithHandle(tableID, start)
	endKey := tablecodec.EncodeRowKeyWithHandle(tableID, end)
	s.pdClient.addRegion(startKey, endKey)
	s.refreshCache()
	return s
}

func (s *mockTiKVStore) addRegion(start, end []byte) *mockTiKVStore {
	s.pdClient.addRegion(start, end)
	s.refreshCache()
	return s
}

func (s *mockTiKVStore) refreshCache() {
	_, err := s.cache.LoadRegionsInKeyRange(
		tikv.NewBackofferWithVars(context.Background(), 1000, nil),
		[]byte{},
		[]byte{0xFF},
	)
	require.NoError(s.t, err)
}

func (s *mockTiKVStore) batchAddIntHandleRegions(tblID int64, regionCnt, regionSize int,
	offset int64) (end kv.IntHandle) {
	for i := 0; i < regionCnt; i++ {
		start := kv.IntHandle(offset + int64(i*regionSize))
		end = kv.IntHandle(start.IntValue() + int64(regionSize))
		s.addRegionWithTablePrefix(tblID, start, end)
	}
	return
}

// clearRegions is **not** thread-safe, it is only used in test cases to reset the region cache.
// A more suggested way is to create a new mockTiKVStore instance for each test case.
func (s *mockTiKVStore) clearRegions() {
	s.pdClient.regions = nil
	s.cache.Close()
	s.cache = tikv.NewRegionCache(s.pdClient)
	s.refreshCache()
}

func (s *mockTiKVStore) GetRegionCache() *tikv.RegionCache {
	return s.cache
}

func (s *mockTiKVStore) randomlyMergeRegions() int {
	ret := s.pdClient.randomlyMergeRegions()
	s.refreshCache()
	return ret
}

func bytesHandle(t *testing.T, data []byte) kv.Handle {
	return commonHandle(t, types.NewBytesDatum(data))
}

func commonHandle(t *testing.T, d ...types.Datum) kv.Handle {
	encoded, err := codec.EncodeKey(time.UTC, nil, d...)
	require.NoError(t, err)
	h, err := kv.NewCommonHandle(encoded)
	require.NoError(t, err)
	return h
}

func createTTLTable(t *testing.T, tk *testkit.TestKit, name string, option string) *cache.PhysicalTable {
	if option == "" {
		return createTTLTableWithSQL(t, tk, name,
			fmt.Sprintf("create table test.%s(t timestamp) TTL = `t` + interval 1 day", name))
	}

	return createTTLTableWithSQL(t, tk, name,
		fmt.Sprintf("create table test.%s(id %s primary key, t timestamp) TTL = `t` + interval 1 day",
			name, option))
}

func create2PKTTLTable(t *testing.T, tk *testkit.TestKit, name string, option string) *cache.PhysicalTable {
	return createTTLTableWithSQL(t, tk, name,
		fmt.Sprintf(
			"create table test.%s(id %s, id2 int, t timestamp, primary key(id, id2)) TTL = `t` + interval 1 day",
			name, option))
}

func createTTLTableWithSQL(t *testing.T, tk *testkit.TestKit, name string, sql string) *cache.PhysicalTable {
	tk.MustExec(sql)
	is, ok := tk.Session().GetDomainInfoSchema().(infoschema.InfoSchema)
	require.True(t, ok)
	tbl, err := is.TableByName(context.Background(), model.NewCIStr("test"), model.NewCIStr(name))
	require.NoError(t, err)
	ttlTbl, err := cache.NewPhysicalTable(model.NewCIStr("test"), tbl.Meta(), model.NewCIStr(""))
	require.NoError(t, err)
	return ttlTbl
}

func checkRange(t *testing.T, r cache.ScanRange, start, end types.Datum, msgAndArgs ...any) {
	if start.IsNull() {
		require.Nil(t, r.Start, msgAndArgs...)
	} else {
		require.Equal(t, 1, len(r.Start), msgAndArgs...)
		require.Equal(t, start.Kind(), r.Start[0].Kind(), msgAndArgs...)
		require.Equal(t, start.GetValue(), r.Start[0].GetValue(), msgAndArgs...)
	}

	if end.IsNull() {
		require.Nil(t, r.End, msgAndArgs...)
	} else {
		require.Equal(t, 1, len(r.End), msgAndArgs...)
		require.Equal(t, end.Kind(), r.End[0].Kind(), msgAndArgs...)
		require.Equal(t, end.GetValue(), r.End[0].GetValue(), msgAndArgs...)
	}
}

func TestSplitTTLScanRangesWithSignedInt(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	tbls := []*cache.PhysicalTable{
		createTTLTable(t, tk, "t1", "tinyint"),
		createTTLTable(t, tk, "t2", "smallint"),
		createTTLTable(t, tk, "t3", "mediumint"),
		createTTLTable(t, tk, "t4", "int"),
		createTTLTable(t, tk, "t5", "bigint"),
		createTTLTable(t, tk, "t6", ""), // no clustered
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		// test only one region
		tikvStore.clearRegions()
		ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test share regions with other table
		tikvStore.clearRegions()
		tikvStore.addRegion(
			tablecodec.GenTablePrefix(tbl.ID-1),
			tablecodec.GenTablePrefix(tbl.ID+1),
		)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test one table has multiple regions
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(0))
		end := tikvStore.batchAddIntHandleRegions(tbl.ID, 8, 100, 0)
		tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 4, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.NewIntDatum(200))
		checkRange(t, ranges[1], types.NewIntDatum(200), types.NewIntDatum(500))
		checkRange(t, ranges[2], types.NewIntDatum(500), types.NewIntDatum(700))
		checkRange(t, ranges[3], types.NewIntDatum(700), types.Datum{})

		// test one table has multiple regions and one table region across 0
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(-350))
		end = tikvStore.batchAddIntHandleRegions(tbl.ID, 8, 100, -350)
		tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 5)
		require.NoError(t, err)
		require.Equal(t, 5, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.NewIntDatum(-250))
		checkRange(t, ranges[1], types.NewIntDatum(-250), types.NewIntDatum(-50))
		checkRange(t, ranges[2], types.NewIntDatum(-50), types.NewIntDatum(150))
		checkRange(t, ranges[3], types.NewIntDatum(150), types.NewIntDatum(350))
		checkRange(t, ranges[4], types.NewIntDatum(350), types.Datum{})
	}
}

func TestSplitTTLScanRangesWithUnsignedInt(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	tbls := []*cache.PhysicalTable{
		createTTLTable(t, tk, "t1", "tinyint unsigned"),
		createTTLTable(t, tk, "t2", "smallint unsigned"),
		createTTLTable(t, tk, "t3", "mediumint unsigned"),
		createTTLTable(t, tk, "t4", "int unsigned"),
		createTTLTable(t, tk, "t5", "bigint unsigned"),
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		// test only one region
		tikvStore.clearRegions()
		ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test share regions with other table
		tikvStore.clearRegions()
		tikvStore.addRegion(
			tablecodec.GenTablePrefix(tbl.ID-1),
			tablecodec.GenTablePrefix(tbl.ID+1),
		)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test one table has multiple regions: [MinInt64, a) [a, b) [b, 0) [0, c) [c, d) [d, MaxInt64]
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(-200))
		end := tikvStore.batchAddIntHandleRegions(tbl.ID, 4, 100, -200)
		tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 6)
		require.NoError(t, err)
		require.Equal(t, 6, len(ranges))
		checkRange(t, ranges[0],
			types.NewUintDatum(uint64(math.MaxInt64)+1), types.NewUintDatum(uint64(math.MaxUint64)-199))
		checkRange(t, ranges[1],
			types.NewUintDatum(uint64(math.MaxUint64)-199), types.NewUintDatum(uint64(math.MaxUint64)-99))
		checkRange(t, ranges[2],
			types.NewUintDatum(uint64(math.MaxUint64)-99), types.Datum{})
		checkRange(t, ranges[3],
			types.Datum{}, types.NewUintDatum(100))
		checkRange(t, ranges[4],
			types.NewUintDatum(100), types.NewUintDatum(200))
		checkRange(t, ranges[5],
			types.NewUintDatum(200), types.NewUintDatum(uint64(math.MaxInt64)+1))

		// test one table has multiple regions: [MinInt64, a) [a, b) [b, c) [c, d) [d, MaxInt64], b < 0 < c
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(-150))
		end = tikvStore.batchAddIntHandleRegions(tbl.ID, 3, 100, -150)
		tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 5)
		require.NoError(t, err)
		require.Equal(t, 6, len(ranges))
		checkRange(t, ranges[0],
			types.NewUintDatum(uint64(math.MaxInt64)+1), types.NewUintDatum(uint64(math.MaxUint64)-149))
		checkRange(t, ranges[1],
			types.NewUintDatum(uint64(math.MaxUint64)-149), types.NewUintDatum(uint64(math.MaxUint64)-49))
		checkRange(t, ranges[2],
			types.NewUintDatum(uint64(math.MaxUint64)-49), types.Datum{})
		checkRange(t, ranges[3],
			types.Datum{}, types.NewUintDatum(50))
		checkRange(t, ranges[4],
			types.NewUintDatum(50), types.NewUintDatum(150))
		checkRange(t, ranges[5],
			types.NewUintDatum(150), types.NewUintDatum(uint64(math.MaxInt64)+1))
	}
}

func TestSplitTTLScanRangesCommonHandleSignedInt(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)
	tbls := []*cache.PhysicalTable{
		create2PKTTLTable(t, tk, "t1", "bigint"),
		create2PKTTLTable(t, tk, "t2", "int"),
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		// test only one region
		tikvStore.clearRegions()
		ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test share regions with other table
		tikvStore.clearRegions()
		tikvStore.addRegion(
			tablecodec.GenTablePrefix(tbl.ID-1),
			tablecodec.GenTablePrefix(tbl.ID+1),
		)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test one table has multiple regions
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, commonHandle(t, types.NewIntDatum(-21)))
		tikvStore.addRegionWithTablePrefix(tbl.ID,
			commonHandle(t, types.NewIntDatum(-21)),
			commonHandle(t, types.NewIntDatum(-19), types.NewIntDatum(0)),
		)
		tikvStore.addRegionWithTablePrefix(tbl.ID,
			commonHandle(t, types.NewIntDatum(-19), types.NewIntDatum(0)),
			commonHandle(t, types.NewIntDatum(2)),
		)
		tikvStore.addRegionEndWithTablePrefix(commonHandle(t, types.NewIntDatum(2)), tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 4, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.NewIntDatum(-21))
		checkRange(t, ranges[1], types.NewIntDatum(-21), types.NewIntDatum(-18))
		checkRange(t, ranges[2], types.NewIntDatum(-18), types.NewIntDatum(2))
		checkRange(t, ranges[3], types.NewIntDatum(2), types.Datum{})
	}
}

func TestSplitTTLScanRangesCommonHandleUnsignedInt(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)
	tbls := []*cache.PhysicalTable{
		create2PKTTLTable(t, tk, "t1", "bigint unsigned"),
		create2PKTTLTable(t, tk, "t2", "int unsigned"),
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		// test only one region
		tikvStore.clearRegions()
		ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test share regions with other table
		tikvStore.clearRegions()
		tikvStore.addRegion(
			tablecodec.GenTablePrefix(tbl.ID-1),
			tablecodec.GenTablePrefix(tbl.ID+1),
		)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test one table has multiple regions
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, commonHandle(t, types.NewUintDatum(9)))
		tikvStore.addRegionWithTablePrefix(tbl.ID,
			commonHandle(t, types.NewUintDatum(9)),
			commonHandle(t, types.NewUintDatum(23), types.NewUintDatum(0)),
		)
		tikvStore.addRegionWithTablePrefix(tbl.ID,
			commonHandle(t, types.NewUintDatum(23), types.NewUintDatum(0)),
			commonHandle(t, types.NewUintDatum(math.MaxInt64+9)),
		)
		tikvStore.addRegionEndWithTablePrefix(commonHandle(t, types.NewUintDatum(math.MaxInt64+9)), tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 4, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.NewUintDatum(9))
		checkRange(t, ranges[1], types.NewUintDatum(9), types.NewUintDatum(24))
		checkRange(t, ranges[2], types.NewUintDatum(24), types.NewUintDatum(math.MaxInt64+9))
		checkRange(t, ranges[3], types.NewUintDatum(math.MaxInt64+9), types.Datum{})
	}
}

func TestSplitTTLScanRangesWithBytes(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	tbls := []*cache.PhysicalTable{
		createTTLTable(t, tk, "t1", "binary(32)"),
		createTTLTable(t, tk, "t2", "char(32) CHARACTER SET BINARY"),
		createTTLTable(t, tk, "t3", "varchar(32) CHARACTER SET BINARY"),
		createTTLTable(t, tk, "t4", "bit(32)"),
		create2PKTTLTable(t, tk, "t5", "binary(32)"),
		createTTLTable(t, tk, "t6", "varbinary(32)"),
		createTTLTable(t, tk, "t7", "char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin"),
		createTTLTable(t, tk, "t8", "char(32) CHARACTER SET utf8 COLLATE utf8_bin"),
		create2PKTTLTable(t, tk, "t9", "char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin"),
	}

	cases := []struct {
		name           string
		regionEdges    []kv.Handle
		splitCnt       int
		binaryExpected [][]types.Datum
		stringExpected [][]types.Datum
	}{
		{
			name: "2 regions with binary split",
			regionEdges: []kv.Handle{
				bytesHandle(t, []byte{1, 2, 3}),
			},
			splitCnt: 4,
			binaryExpected: [][]types.Datum{
				{types.Datum{}, types.NewBytesDatum([]byte{1, 2, 3})},
				{types.NewBytesDatum([]byte{1, 2, 3}), types.Datum{}},
			},
			stringExpected: [][]types.Datum{
				{types.Datum{}, types.Datum{}},
			},
		},
		{
			name: "6 regions with binary split",
			regionEdges: []kv.Handle{
				bytesHandle(t, []byte{1, 2, 3}),
				bytesHandle(t, []byte{1, 2, 3, 4}),
				bytesHandle(t, []byte{1, 2, 3, 4, 5}),
				bytesHandle(t, []byte{1, 2, 4}),
				bytesHandle(t, []byte{1, 2, 5}),
			},
			splitCnt: 4,
			binaryExpected: [][]types.Datum{
				{types.Datum{}, types.NewBytesDatum([]byte{1, 2, 3, 4})},
				{types.NewBytesDatum([]byte{1, 2, 3, 4}), types.NewBytesDatum([]byte{1, 2, 4})},
				{types.NewBytesDatum([]byte{1, 2, 4}), types.NewBytesDatum([]byte{1, 2, 5})},
				{types.NewBytesDatum([]byte{1, 2, 5}), types.Datum{}},
			},
			stringExpected: [][]types.Datum{
				{types.Datum{}, types.Datum{}},
			},
		},
		{
			name: "2 regions with utf8 split",
			regionEdges: []kv.Handle{
				bytesHandle(t, []byte("中文")),
			},
			splitCnt: 4,
			binaryExpected: [][]types.Datum{
				{types.Datum{}, types.NewBytesDatum([]byte("中文"))},
				{types.NewBytesDatum([]byte("中文")), types.Datum{}},
			},
			stringExpected: [][]types.Datum{
				{types.Datum{}, types.Datum{}},
			},
		},
		{
			name: "several regions with mixed split",
			regionEdges: []kv.Handle{
				bytesHandle(t, []byte("abc")),
				bytesHandle(t, []byte("ab\x7f0")),
				bytesHandle(t, []byte("ab\xff0")),
				bytesHandle(t, []byte("ac\x001")),
				bytesHandle(t, []byte("ad\x0a1")),
				bytesHandle(t, []byte("ad23")),
				bytesHandle(t, []byte("ad230\xff")),
				bytesHandle(t, []byte("befh")),
				bytesHandle(t, []byte("中文")),
			},
			splitCnt: 10,
			binaryExpected: [][]types.Datum{
				{types.Datum{}, types.NewBytesDatum([]byte("abc"))},
				{types.NewBytesDatum([]byte("abc")), types.NewBytesDatum([]byte("ab\x7f0"))},
				{types.NewBytesDatum([]byte("ab\x7f0")), types.NewBytesDatum([]byte("ab\xff0"))},
				{types.NewBytesDatum([]byte("ab\xff0")), types.NewBytesDatum([]byte("ac\x001"))},
				{types.NewBytesDatum([]byte("ac\x001")), types.NewBytesDatum([]byte("ad\x0a1"))},
				{types.NewBytesDatum([]byte("ad\x0a1")), types.NewBytesDatum([]byte("ad23"))},
				{types.NewBytesDatum([]byte("ad23")), types.NewBytesDatum([]byte("ad230\xff"))},
				{types.NewBytesDatum([]byte("ad230\xff")), types.NewBytesDatum([]byte("befh"))},
				{types.NewBytesDatum([]byte("befh")), types.NewBytesDatum([]byte("中文"))},
				{types.NewBytesDatum([]byte("中文")), types.Datum{}},
			},
			stringExpected: [][]types.Datum{
				{types.Datum{}, types.NewStringDatum("abc")},
				{types.NewStringDatum("abc"), types.NewStringDatum("ac")},
				{types.NewStringDatum("ac"), types.NewStringDatum("ad\n1")},
				{types.NewStringDatum("ad\n1"), types.NewStringDatum("ad23")},
				{types.NewStringDatum("ad23"), types.NewStringDatum("ad230")},
				{types.NewStringDatum("ad230"), types.NewStringDatum("befh")},
				{types.NewStringDatum("befh"), types.Datum{}},
			},
		},
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		for _, c := range cases {
			tikvStore.clearRegions()
			require.Greater(t, len(c.regionEdges), 0)
			for i, edge := range c.regionEdges {
				if i == 0 {
					tikvStore.addRegionBeginWithTablePrefix(tbl.ID, edge)
				} else {
					tikvStore.addRegionWithTablePrefix(tbl.ID, c.regionEdges[i-1], edge)
				}
			}
			tikvStore.addRegionEndWithTablePrefix(c.regionEdges[len(c.regionEdges)-1], tbl.ID)
			ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, c.splitCnt)
			require.NoError(t, err)

			keyTp := tbl.KeyColumnTypes[0]
			var expected [][]types.Datum
			if keyTp.GetType() == mysql.TypeBit || mysql.HasBinaryFlag(keyTp.GetFlag()) {
				expected = c.binaryExpected
			} else {
				expected = c.stringExpected
			}

			require.Equal(t, len(expected), len(ranges), "tbl: %s, case: %s", tbl.Name, c.name)
			for i, r := range ranges {
				checkRange(t, r, expected[i][0], expected[i][1],
					"tbl: %s, case: %s, i: %d", tbl.Name, c.name, i)
			}
		}
	}
}

func TestNoTTLSplitSupportTables(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	tbls := []*cache.PhysicalTable{
		createTTLTable(t, tk, "t1", "decimal(32, 2)"),
		createTTLTable(t, tk, "t2", "date"),
		createTTLTable(t, tk, "t3", "datetime"),
		createTTLTable(t, tk, "t4", "timestamp"),
		createTTLTable(t, tk, "t5", "varchar(32) character set utf8mb4 collate utf8mb4_general_ci"),
		createTTLTable(t, tk, "t6", "varchar(32) character set utf8mb4 collate utf8mb4_0900_ai_ci"),
	}

	tikvStore := newMockTiKVStore(t)
	for _, tbl := range tbls {
		// test only one region
		tikvStore.clearRegions()
		ranges, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test share regions with other table
		tikvStore.clearRegions()
		tikvStore.addRegion(
			tablecodec.GenTablePrefix(tbl.ID-1),
			tablecodec.GenTablePrefix(tbl.ID+1),
		)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 4)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})

		// test one table has multiple regions
		tikvStore.clearRegions()
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, bytesHandle(t, []byte{1, 2, 3}))
		tikvStore.addRegionWithTablePrefix(tbl.ID, bytesHandle(t, []byte{1, 2, 3}), bytesHandle(t, []byte{1, 2, 3, 4}))
		tikvStore.addRegionEndWithTablePrefix(bytesHandle(t, []byte{1, 2, 3, 4}), tbl.ID)
		ranges, err = tbl.SplitScanRanges(context.TODO(), tikvStore, 3)
		require.NoError(t, err)
		require.Equal(t, 1, len(ranges))
		checkRange(t, ranges[0], types.Datum{}, types.Datum{})
	}
}

func TestGetNextBytesHandleDatum(t *testing.T) {
	tblID := int64(7)
	buildHandleBytes := func(data []byte) []byte {
		handleBytes, err := codec.EncodeKey(time.UTC, nil, types.NewBytesDatum(data))
		require.NoError(t, err)
		return handleBytes
	}

	buildRowKey := func(handleBytes []byte) kv.Key {
		return tablecodec.EncodeRowKey(tblID, handleBytes)
	}

	buildBytesRowKey := func(data []byte) kv.Key {
		return buildRowKey(buildHandleBytes(data))
	}

	binaryDataStartPos := len(tablecodec.GenTableRecordPrefix(tblID)) + 1
	cases := []struct {
		key    any
		result []byte
		isNull bool
	}{
		{
			key:    buildBytesRowKey([]byte{}),
			result: []byte{},
		},
		{
			key:    buildBytesRowKey([]byte{1, 2, 3}),
			result: []byte{1, 2, 3},
		},
		{
			key:    buildBytesRowKey([]byte{1, 2, 3, 0}),
			result: []byte{1, 2, 3, 0},
		},
		{
			key:    buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		},
		{
			key:    buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9}),
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
		},
		{
			key:    buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 0}),
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 0},
		},
		{
			key:    append(buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 0}), 0),
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 0, 0},
		},
		{
			key:    append(buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 0}), 1),
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 0, 0},
		},
		{
			key:    []byte{},
			result: []byte{},
		},
		{
			key:    tablecodec.GenTableRecordPrefix(tblID),
			result: []byte{},
		},
		{
			key:    tablecodec.GenTableRecordPrefix(tblID - 1),
			result: []byte{},
		},
		{
			key:    tablecodec.GenTablePrefix(tblID).PrefixNext(),
			isNull: true,
		},
		{
			key:    buildRowKey([]byte{0}),
			result: []byte{},
		},
		{
			key:    buildRowKey([]byte{1}),
			result: []byte{},
		},
		{
			key:    buildRowKey([]byte{2}),
			isNull: true,
		},
		{
			// recordPrefix + bytesFlag + [0]
			key:    buildBytesRowKey([]byte{})[:binaryDataStartPos+1],
			result: []byte{},
		},
		{
			// recordPrefix + bytesFlag + [0, 0, 0, 0, 0, 0, 0, 0]
			key:    buildBytesRowKey([]byte{})[:binaryDataStartPos+8],
			result: []byte{},
		},
		{
			// recordPrefix + bytesFlag + [1]
			key:    buildBytesRowKey([]byte{1, 2, 3})[:binaryDataStartPos+1],
			result: []byte{1},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3]
			key:    buildBytesRowKey([]byte{1, 2, 3})[:binaryDataStartPos+3],
			result: []byte{1, 2, 3},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 0]
			key:    buildBytesRowKey([]byte{1, 2, 3})[:binaryDataStartPos+4],
			result: []byte{1, 2, 3},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 0, 0, 0, 0, 0, 247]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3})
				bs[len(bs)-1] = 247
				return bs
			},
			result: []byte{1, 2, 3},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 0, 0, 0, 0, 0, 0]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3})
				bs[len(bs)-1] = 0
				return bs
			},
			result: []byte{1, 2, 3},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 8, 254, 9, 0, 0, 0, 0, 0, 0, 0, 248]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
				bs[len(bs)-10] = 254
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 0, 254, 9, 0, 0, 0, 0, 0, 0, 0, 248]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 0, 9})
				bs[len(bs)-10] = 254
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 0},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 0, 253, 9, 0, 0, 0, 0, 0, 0, 0, 248]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 0, 9})
				bs[len(bs)-10] = 253
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 8, 255, 9, 0, 0, 0, 0, 0, 0, 0, 247]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
				bs[len(bs)-1] = 247
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 8, 255, 9, 0, 0, 0, 0, 0, 0, 0, 0]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
				bs[len(bs)-1] = 0
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 246]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8})
				bs = bs[:len(bs)-1]
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		},
		{
			// recordPrefix + bytesFlag + [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 246]
			key: func() []byte {
				bs := buildBytesRowKey([]byte{1, 2, 3, 4, 5, 6, 7, 8})
				bs = bs[:len(bs)-1]
				return bs
			},
			result: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		},
	}

	for i, c := range cases {
		var key kv.Key
		switch k := c.key.(type) {
		case kv.Key:
			key = k
		case []byte:
			key = k
		case func() []byte:
			key = k()
		case func() kv.Key:
			key = k()
		default:
			require.FailNow(t, "%d", i)
		}

		d := cache.GetNextBytesHandleDatum(key, tablecodec.GenTableRecordPrefix(tblID))
		if c.isNull {
			require.True(t, d.IsNull(), i)
		} else {
			require.Equal(t, types.KindBytes, d.Kind(), i)
			require.Equal(t, c.result, d.GetBytes(), i)
		}
	}
}

func TestGetASCIIPrefixDatumFromBytes(t *testing.T) {
	cases := []struct {
		bytes    []byte
		expected string
	}{
		{bytes: nil, expected: ""},
		{bytes: []byte{}, expected: ""},
		{bytes: []byte{0}, expected: ""},
		{bytes: []byte{1}, expected: ""},
		{bytes: []byte{8}, expected: ""},
		{bytes: []byte{9}, expected: "\t"},
		{bytes: []byte{10}, expected: "\n"},
		{bytes: []byte{11}, expected: ""},
		{bytes: []byte{12}, expected: ""},
		{bytes: []byte{13}, expected: "\r"},
		{bytes: []byte{14}, expected: ""},
		{bytes: []byte{0x19}, expected: ""},
		{bytes: []byte{0x20}, expected: " "},
		{bytes: []byte{0x21}, expected: "!"},
		{bytes: []byte{0x7D}, expected: "}"},
		{bytes: []byte{0x7E}, expected: "~"},
		{bytes: []byte{0x7F}, expected: ""},
		{bytes: []byte{0xFF}, expected: ""},
		{bytes: []byte{0x0, 'a', 'b'}, expected: ""},
		{bytes: []byte{0xFF, 'a', 'b'}, expected: ""},
		{bytes: []byte{'0', '1', 0x0, 'a', 'b'}, expected: "01"},
		{bytes: []byte{'0', '1', 0x15, 'a', 'b'}, expected: "01"},
		{bytes: []byte{'0', '1', 0xFF, 'a', 'b'}, expected: "01"},
		{bytes: []byte{'a', 'b', 0x0}, expected: "ab"},
		{bytes: []byte{'a', 'b', 0x15}, expected: "ab"},
		{bytes: []byte{'a', 'b', 0xFF}, expected: "ab"},
		{bytes: []byte("ab\rcd\tef\nAB!~GH()tt ;;"), expected: "ab\rcd\tef\nAB!~GH()tt ;;"},
		{bytes: []byte("中文"), expected: ""},
		{bytes: []byte("cn中文"), expected: "cn"},
		{bytes: []byte("😀"), expected: ""},
		{bytes: []byte("emoji😀"), expected: "emoji"},
	}

	for i, c := range cases {
		d := cache.GetASCIIPrefixDatumFromBytes(c.bytes)
		require.Equalf(t, types.KindString, d.Kind(), "i: %d", i)
		require.Equalf(t, c.expected, d.GetString(), "i: %d, bs: %v", i, c.bytes)
	}
}

func TestGetNextIntHandle(t *testing.T) {
	tblID := int64(7)
	cases := []struct {
		key    any
		result int64
		isNull bool
	}{
		{
			key:    tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(0)),
			result: 0,
		},
		{
			key:    tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(3)),
			result: 3,
		},
		{
			key:    tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(math.MaxInt64)),
			result: math.MaxInt64,
		},
		{
			key:    tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(math.MinInt64)),
			result: math.MinInt64,
		},
		{
			key:    append(tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(7)), 0),
			result: 8,
		},
		{
			key:    append(tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(math.MaxInt64)), 0),
			isNull: true,
		},
		{
			key:    append(tablecodec.EncodeRowKeyWithHandle(tblID, kv.IntHandle(math.MinInt64)), 0),
			result: math.MinInt64 + 1,
		},
		{
			key:    []byte{},
			result: math.MinInt64,
		},
		{
			key:    tablecodec.GenTableRecordPrefix(tblID),
			result: math.MinInt64,
		},
		{
			key:    tablecodec.GenTableRecordPrefix(tblID - 1),
			result: math.MinInt64,
		},
		{
			key:    tablecodec.GenTablePrefix(tblID).PrefixNext(),
			isNull: true,
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{0}),
			result: codec.DecodeCmpUintToInt(0),
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{0, 1, 2, 3}),
			result: codec.DecodeCmpUintToInt(0x0001020300000000),
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{8, 1, 2, 3}),
			result: codec.DecodeCmpUintToInt(0x0801020300000000),
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{0, 1, 2, 3, 4, 5, 6, 7, 0}),
			result: codec.DecodeCmpUintToInt(0x0001020304050607) + 1,
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{8, 1, 2, 3, 4, 5, 6, 7, 0}),
			result: codec.DecodeCmpUintToInt(0x0801020304050607) + 1,
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}),
			result: math.MaxInt64,
		},
		{
			key:    tablecodec.EncodeRowKey(tblID, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0}),
			isNull: true,
		},
	}

	for i, c := range cases {
		var key kv.Key
		switch k := c.key.(type) {
		case kv.Key:
			key = k
		case []byte:
			key = k
		case func() []byte:
			key = k()
		case func() kv.Key:
			key = k()
		default:
			require.FailNow(t, "%d", i)
		}

		v := cache.GetNextIntHandle(key, tablecodec.GenTableRecordPrefix(tblID))
		if c.isNull {
			require.Nil(t, v, i)
		} else {
			require.IsType(t, kv.IntHandle(0), v, i)
			require.Equal(t, c.result, v.IntValue())
		}
	}
}

func TestGetNextIntDatumFromCommonHandle(t *testing.T) {
	encode := func(tblID int64, d ...types.Datum) kv.Key {
		encoded, err := codec.EncodeKey(time.UTC, nil, d...)
		require.NoError(t, err)
		h, err := kv.NewCommonHandle(encoded)
		require.NoError(t, err)
		return tablecodec.EncodeRowKey(tblID, h.Encoded())
	}

	var nullDatum types.Datum
	nullDatum.SetNull()
	tblID := int64(7)
	fixedLen := len(encode(tblID, types.NewIntDatum(0)))

	cases := []struct {
		key      kv.Key
		d        types.Datum
		unsigned bool
	}{
		{
			key: encode(tblID, types.NewIntDatum(0)),
			d:   types.NewIntDatum(0),
		},
		{
			key: encode(tblID, types.NewIntDatum(1)),
			d:   types.NewIntDatum(1),
		},
		{
			key: encode(tblID, types.NewIntDatum(1024)),
			d:   types.NewIntDatum(1024),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MaxInt64)),
			d:   types.NewIntDatum(math.MaxInt64),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MaxInt64/2)),
			d:   types.NewIntDatum(math.MaxInt64 / 2),
		},
		{
			key: encode(tblID, types.NewIntDatum(-1)),
			d:   types.NewIntDatum(-1),
		},
		{
			key: encode(tblID, types.NewIntDatum(-1024)),
			d:   types.NewIntDatum(-1024),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MinInt64)),
			d:   types.NewIntDatum(math.MinInt64),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MinInt64/2)),
			d:   types.NewIntDatum(math.MinInt64 / 2),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MaxInt64))[:fixedLen-1],
			d:   types.NewIntDatum(math.MaxInt64 - 0xFF),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MaxInt64), types.NewIntDatum(0)),
			d:   nullDatum,
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MaxInt64-1), types.NewIntDatum(0)),
			d:   types.NewIntDatum(math.MaxInt64),
		},
		{
			key: encode(tblID, types.NewIntDatum(123), types.NewIntDatum(0)),
			d:   types.NewIntDatum(124),
		},
		{
			key: encode(tblID, types.NewIntDatum(-123), types.NewIntDatum(0)),
			d:   types.NewIntDatum(-122),
		},
		{
			key: encode(tblID, types.NewIntDatum(math.MinInt64), types.NewIntDatum(0)),
			d:   types.NewIntDatum(math.MinInt64 + 1),
		},
		{
			key:      encode(tblID, types.NewUintDatum(0)),
			d:        types.NewUintDatum(0),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(1)),
			d:        types.NewUintDatum(1),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(1024)),
			d:        types.NewUintDatum(1024),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(math.MaxInt64)),
			d:        types.NewUintDatum(math.MaxInt64),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(math.MaxInt64+1)),
			d:        types.NewUintDatum(math.MaxInt64 + 1),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(math.MaxUint64)),
			d:        types.NewUintDatum(math.MaxUint64),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(math.MaxUint64))[:fixedLen-1],
			d:        types.NewUintDatum(math.MaxUint64 - 0xFF),
			unsigned: true,
		},

		{
			key: encode(tblID, types.NewUintDatum(math.MaxUint64), types.NewIntDatum(0)),
			d:   nullDatum,
		},
		{
			key:      encode(tblID, types.NewUintDatum(math.MaxUint64-1), types.NewIntDatum(0)),
			d:        types.NewUintDatum(math.MaxUint64),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(123), types.NewIntDatum(0)),
			d:        types.NewUintDatum(124),
			unsigned: true,
		},
		{
			key:      encode(tblID, types.NewUintDatum(0), types.NewIntDatum(0)),
			d:        types.NewUintDatum(1),
			unsigned: true,
		},
		{
			key: []byte{},
			d:   types.NewIntDatum(math.MinInt64),
		},
		{
			key:      []byte{},
			d:        types.NewUintDatum(0),
			unsigned: true,
		},
		{
			key: tablecodec.GenTableRecordPrefix(tblID),
			d:   types.NewIntDatum(math.MinInt64),
		},
		{
			key:      tablecodec.GenTableRecordPrefix(tblID),
			d:        types.NewUintDatum(0),
			unsigned: true,
		},
		{
			// 3 is encoded intFlag
			key: append(tablecodec.GenTableRecordPrefix(tblID), []byte{3}...),
			d:   types.NewIntDatum(math.MinInt64),
		},
		{
			// 3 is encoded intFlag
			key:      append(tablecodec.GenTableRecordPrefix(tblID), []byte{3}...),
			d:        types.NewUintDatum(0),
			unsigned: true,
		},
		{
			// 4 is encoded uintFlag
			key: append(tablecodec.GenTableRecordPrefix(tblID), []byte{4}...),
			d:   nullDatum,
		},
		{
			// 4 is encoded uintFlag
			key:      append(tablecodec.GenTableRecordPrefix(tblID), []byte{4}...),
			d:        types.NewUintDatum(0),
			unsigned: true,
		},
		{
			// 5
			key: append(tablecodec.GenTableRecordPrefix(tblID), []byte{5}...),
			d:   nullDatum,
		},
		{
			// 5
			key:      append(tablecodec.GenTableRecordPrefix(tblID), []byte{5}...),
			d:        nullDatum,
			unsigned: true,
		},
	}

	for _, c := range cases {
		if !c.d.IsNull() {
			if c.unsigned {
				require.Equal(t, types.KindUint64, c.d.Kind())
			} else {
				require.Equal(t, types.KindInt64, c.d.Kind())
			}
		}

		d := cache.GetNextIntDatumFromCommonHandle(c.key, tablecodec.GenTableRecordPrefix(tblID), c.unsigned)
		require.Equal(t, c.d, d)
	}
}

func TestMergeRegion(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	tikvStore := newMockTiKVStore(t)
	tbl := create2PKTTLTable(t, tk, "t1", "int")
	startKey, endKey := tablecodec.GetTableHandleKeyRange(tbl.ID)

	tikvStore.clearRegions()
	// create a table with 102 regions
	tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(0))
	end := tikvStore.batchAddIntHandleRegions(tbl.ID, 100, 1000000, 0)
	tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)

	regionCount := 102
	for regionCount >= 2 {
		regionCount--
		require.Equal(t, regionCount, tikvStore.randomlyMergeRegions())
		regionIDs, err := tikvStore.GetRegionCache().ListRegionIDsInKeyRange(
			tikv.NewBackofferWithVars(context.Background(), 20000, nil), startKey, endKey)
		require.NoError(t, err)
		require.Equal(t, regionCount, len(regionIDs))
	}
}

func TestRegionDisappearDuringSplitRange(t *testing.T) {
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	testStartTime := time.Now()
	testDuration := 5 * time.Second

	i := 0
	for time.Since(testStartTime) < testDuration {
		i++

		tikvStore := newMockTiKVStore(t)
		tbl := create2PKTTLTable(t, tk, fmt.Sprintf("t%d", i), "int")
		tikvStore.clearRegions()
		// create a table with 100 regions
		tikvStore.addRegionBeginWithTablePrefix(tbl.ID, kv.IntHandle(0))
		end := tikvStore.batchAddIntHandleRegions(tbl.ID, 100, 100, 0)
		tikvStore.addRegionEndWithTablePrefix(end, tbl.ID)

		mergeStopCh := make(chan struct{})
		// merge regions while splitting ranges
		go func() {
			for tikvStore.randomlyMergeRegions() >= 2 {
			}
			close(mergeStopCh)
		}()

	loop:
		for {
			select {
			case <-mergeStopCh:
				// merging finished
				break loop
			default:
				_, err := tbl.SplitScanRanges(context.TODO(), tikvStore, 16)
				require.NoError(t, err)
			}
		}

		tikvStore.cache.Close()
	}
}
