// Copyright 2021 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 core_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/pingcap/failpoint"
	"github.com/pingcap/tidb/config"
	"github.com/pingcap/tidb/domain"
	"github.com/pingcap/tidb/executor"
	"github.com/pingcap/tidb/parser"
	"github.com/pingcap/tidb/parser/model"
	"github.com/pingcap/tidb/planner"
	plannercore "github.com/pingcap/tidb/planner/core"
	"github.com/pingcap/tidb/sessionctx"
	"github.com/pingcap/tidb/sessionctx/stmtctx"
	"github.com/pingcap/tidb/statistics"
	"github.com/pingcap/tidb/statistics/handle"
	"github.com/pingcap/tidb/testkit"
	"github.com/stretchr/testify/require"
)

func TestPlanStatsLoad(t *testing.T) {
	p := parser.New()
	store, dom := testkit.CreateMockStoreAndDomain(t)

	tk := testkit.NewTestKit(t, store)
	tk.MustExec("use test")
	ctx := tk.Session().(sessionctx.Context)
	tk.MustExec("drop table if exists t")
	tk.MustExec("set @@session.tidb_analyze_version=2")
	tk.MustExec("set @@session.tidb_partition_prune_mode = 'static'")
	tk.MustExec("set @@session.tidb_stats_load_sync_wait = 60000")
	tk.MustExec("create table t(a int, b int, c int, d int, primary key(a), key idx(b))")
	tk.MustExec("insert into t values (1,1,1,1),(2,2,2,2),(3,3,3,3)")
	tk.MustExec("create table pt(a int, b int, c int) partition by range(a) (partition p0 values less than (10), partition p1 values less than (20), partition p2 values less than maxvalue)")
	tk.MustExec("insert into pt values (1,1,1),(2,2,2),(13,13,13),(14,14,14),(25,25,25),(36,36,36)")

	oriLease := dom.StatsHandle().Lease()
	dom.StatsHandle().SetLease(1)
	defer func() {
		dom.StatsHandle().SetLease(oriLease)
	}()
	tk.MustExec("analyze table t")
	tk.MustExec("analyze table pt")

	testCases := []struct {
		sql   string
		skip  bool
		check func(p plannercore.Plan, tableInfo *model.TableInfo)
	}{
		{ // DataSource
			sql: "select * from t where c>1",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				switch pp := p.(type) {
				case *plannercore.PhysicalTableReader:
					stats := pp.Stats().HistColl
					require.Equal(t, 0, countFullStats(stats, tableInfo.Columns[1].ID))
					require.Greater(t, countFullStats(stats, tableInfo.Columns[2].ID), 0)
				default:
					t.Error("unexpected plan:", pp)
				}
			},
		},
		{ // PartitionTable
			sql: "select * from pt where a < 15 and c > 1",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				pua, ok := p.(*plannercore.PhysicalUnionAll)
				require.True(t, ok)
				for _, child := range pua.Children() {
					require.Greater(t, countFullStats(child.Stats().HistColl, tableInfo.Columns[2].ID), 0)
				}
			},
		},
		{ // Join
			sql: "select * from t t1 inner join t t2 on t1.b=t2.b where t1.d=3",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				pp, ok := p.(plannercore.PhysicalPlan)
				require.True(t, ok)
				require.Greater(t, countFullStats(pp.Children()[0].Stats().HistColl, tableInfo.Columns[3].ID), 0)
				require.Greater(t, countFullStats(pp.Children()[1].Stats().HistColl, tableInfo.Columns[3].ID), 0)
			},
		},
		{ // Apply
			sql: "select * from t t1 where t1.b > (select count(*) from t t2 where t2.c > t1.a and t2.d>1) and t1.c>2",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				pp, ok := p.(*plannercore.PhysicalProjection)
				require.True(t, ok)
				pa, ok := pp.Children()[0].(*plannercore.PhysicalApply)
				require.True(t, ok)
				left := pa.PhysicalHashJoin.Children()[0]
				right := pa.PhysicalHashJoin.Children()[0]
				require.Greater(t, countFullStats(left.Stats().HistColl, tableInfo.Columns[2].ID), 0)
				require.Greater(t, countFullStats(right.Stats().HistColl, tableInfo.Columns[3].ID), 0)
			},
		},
		{ // > Any
			sql: "select * from t where t.b > any(select d from t where t.c > 2)",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ph, ok := p.(*plannercore.PhysicalHashJoin)
				require.True(t, ok)
				ptr, ok := ph.Children()[0].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(ptr.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // in
			sql: "select * from t where t.b in (select d from t where t.c > 2)",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ph, ok := p.(*plannercore.PhysicalHashJoin)
				require.True(t, ok)
				ptr, ok := ph.Children()[1].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(ptr.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // not in
			sql: "select * from t where t.b not in (select d from t where t.c > 2)",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ph, ok := p.(*plannercore.PhysicalHashJoin)
				require.True(t, ok)
				ptr, ok := ph.Children()[1].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(ptr.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // exists
			sql: "select * from t t1 where exists (select * from t t2 where t1.b > t2.d and t2.c>1)",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ph, ok := p.(*plannercore.PhysicalHashJoin)
				require.True(t, ok)
				ptr, ok := ph.Children()[1].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(ptr.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // not exists
			sql: "select * from t t1 where not exists (select * from t t2 where t1.b > t2.d and t2.c>1)",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ph, ok := p.(*plannercore.PhysicalHashJoin)
				require.True(t, ok)
				ptr, ok := ph.Children()[1].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(ptr.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // CTE
			sql: "with cte(x, y) as (select d + 1, b from t where c > 1) select * from cte where x < 3",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				ps, ok := p.(*plannercore.PhysicalSelection)
				require.True(t, ok)
				pc, ok := ps.Children()[0].(*plannercore.PhysicalCTE)
				require.True(t, ok)
				pp, ok := pc.SeedPlan.(*plannercore.PhysicalProjection)
				require.True(t, ok)
				reader, ok := pp.Children()[0].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(reader.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // recursive CTE
			sql: "with recursive cte(x, y) as (select a, b from t where c > 1 union select x + 1, y from cte where x < 5) select * from cte",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				pc, ok := p.(*plannercore.PhysicalCTE)
				require.True(t, ok)
				pp, ok := pc.SeedPlan.(*plannercore.PhysicalProjection)
				require.True(t, ok)
				reader, ok := pp.Children()[0].(*plannercore.PhysicalTableReader)
				require.True(t, ok)
				require.Greater(t, countFullStats(reader.Stats().HistColl, tableInfo.Columns[2].ID), 0)
			},
		},
		{ // check idx(b)
			sql: "select * from t USE INDEX(idx) where b >= 10",
			check: func(p plannercore.Plan, tableInfo *model.TableInfo) {
				pr, ok := p.(*plannercore.PhysicalIndexLookUpReader)
				require.True(t, ok)
				pis, ok := pr.IndexPlans[0].(*plannercore.PhysicalIndexScan)
				require.True(t, ok)
				require.True(t, pis.Stats().HistColl.Indices[1].IsEssentialStatsLoaded())
			},
		},
	}
	for _, testCase := range testCases {
		if testCase.skip {
			continue
		}
		is := dom.InfoSchema()
		dom.StatsHandle().Clear() // clear statsCache
		require.NoError(t, dom.StatsHandle().Update(is))
		stmt, err := p.ParseOneStmt(testCase.sql, "", "")
		require.NoError(t, err)
		err = executor.ResetContextOfStmt(ctx, stmt)
		require.NoError(t, err)
		p, _, err := planner.Optimize(context.TODO(), ctx, stmt, is)
		require.NoError(t, err)
		tbl, err := is.TableByName(model.NewCIStr("test"), model.NewCIStr("t"))
		require.NoError(t, err)
		tableInfo := tbl.Meta()
		testCase.check(p, tableInfo)
	}
}

func countFullStats(stats *statistics.HistColl, colID int64) int {
	for _, col := range stats.Columns {
		if col.Info.ID == colID {
			return col.Histogram.Len() + col.TopN.Num()
		}
	}
	return -1
}

func TestPlanStatsLoadTimeout(t *testing.T) {
	p := parser.New()
	originConfig := config.GetGlobalConfig()
	newConfig := config.NewConfig()
	newConfig.Performance.StatsLoadConcurrency = 0 // no worker to consume channel
	newConfig.Performance.StatsLoadQueueSize = 1
	config.StoreGlobalConfig(newConfig)
	defer config.StoreGlobalConfig(originConfig)
	store, dom := testkit.CreateMockStoreAndDomain(t)

	tk := testkit.NewTestKit(t, store)
	tk.MustExec("use test")
	originalVal1 := tk.MustQuery("select @@tidb_stats_load_pseudo_timeout").Rows()[0][0].(string)
	defer func() {
		tk.MustExec(fmt.Sprintf("set global tidb_stats_load_pseudo_timeout = %v", originalVal1))
	}()

	ctx := tk.Session().(sessionctx.Context)
	tk.MustExec("drop table if exists t")
	tk.MustExec("set @@session.tidb_analyze_version=2")
	// since queue full, make sync-wait return as timeout as soon as possible
	tk.MustExec("set @@session.tidb_stats_load_sync_wait = 1")
	tk.MustExec("create table t(a int, b int, c int, primary key(a))")
	tk.MustExec("insert into t values (1,1,1),(2,2,2),(3,3,3)")

	oriLease := dom.StatsHandle().Lease()
	dom.StatsHandle().SetLease(1)
	defer func() {
		dom.StatsHandle().SetLease(oriLease)
	}()
	tk.MustExec("analyze table t")
	is := dom.InfoSchema()
	require.NoError(t, dom.StatsHandle().Update(is))
	tbl, err := is.TableByName(model.NewCIStr("test"), model.NewCIStr("t"))
	require.NoError(t, err)
	tableInfo := tbl.Meta()
	neededColumn := model.TableItemID{TableID: tableInfo.ID, ID: tableInfo.Columns[0].ID, IsIndex: false}
	resultCh := make(chan stmtctx.StatsLoadResult, 1)
	timeout := time.Duration(1<<63 - 1)
	task := &handle.NeededItemTask{
		TableItemID: neededColumn,
		ResultCh:    resultCh,
		ToTimeout:   time.Now().Local().Add(timeout),
	}
	dom.StatsHandle().AppendNeededItem(task, timeout) // make channel queue full
	sql := "select * from t where c>1"
	stmt, err := p.ParseOneStmt(sql, "", "")
	require.NoError(t, err)
	tk.MustExec("set global tidb_stats_load_pseudo_timeout=false")
	_, _, err = planner.Optimize(context.TODO(), ctx, stmt, is)
	require.Error(t, err) // fail sql for timeout when pseudo=false

	tk.MustExec("set global tidb_stats_load_pseudo_timeout=true")
	require.NoError(t, failpoint.Enable("github.com/pingcap/executor/assertSyncStatsFailed", `return(true)`))
	tk.MustExec(sql) // not fail sql for timeout when pseudo=true
	failpoint.Disable("github.com/pingcap/executor/assertSyncStatsFailed")

	plan, _, err := planner.Optimize(context.TODO(), ctx, stmt, is)
	require.NoError(t, err) // not fail sql for timeout when pseudo=true
	switch pp := plan.(type) {
	case *plannercore.PhysicalTableReader:
		stats := pp.Stats().HistColl
		require.Greater(t, countFullStats(stats, tableInfo.Columns[0].ID), 0)
		require.Equal(t, 0, countFullStats(stats, tableInfo.Columns[2].ID)) // pseudo stats
	default:
		t.Error("unexpected plan:", pp)
	}
}

func TestPlanStatsStatusRecord(t *testing.T) {
	restore := config.RestoreFunc()
	defer restore()
	config.UpdateGlobal(func(conf *config.Config) {
		conf.Performance.EnableStatsCacheMemQuota = true
	})
	store, _ := testkit.CreateMockStoreAndDomain(t)
	tk := testkit.NewTestKit(t, store)
	tk.MustExec("use test")
	tk.MustExec(`create table t (b int,key b(b))`)
	tk.MustExec("insert into t (b) values (1)")
	tk.MustExec("analyze table t")
	tk.MustQuery("select * from t where b >= 1")
	require.Len(t, tk.Session().GetSessionVars().StmtCtx.StatsLoadStatus, 0)
	// drop stats in order to change status
	domain.GetDomain(tk.Session()).StatsHandle().SetStatsCacheCapacity(1)
	tk.MustQuery("select * from t where b >= 1")
	require.Len(t, tk.Session().GetSessionVars().StmtCtx.StatsLoadStatus, 2)
	for _, status := range tk.Session().GetSessionVars().StmtCtx.StatsLoadStatus {
		require.Equal(t, status, "allEvicted")
	}
}
