// 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 executor_test

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/pingcap/failpoint"
	"github.com/pingcap/tidb/session"
	"github.com/pingcap/tidb/testkit"
	"github.com/stretchr/testify/require"
)

func TestNormalGlobalTemporaryTableNoNetwork(t *testing.T) {
	assertTemporaryTableNoNetwork(t, func(tk *testkit.TestKit) {
		tk.MustExec("create global temporary table tmp_t (id int primary key, a int, b int, index(a)) on commit delete rows")
		tk.MustExec("begin")
	})
}

func TestGlobalTemporaryTableNoNetworkWithCreateAndTruncate(t *testing.T) {
	assertTemporaryTableNoNetwork(t, func(tk *testkit.TestKit) {
		tk.MustExec("create global temporary table tmp_t (id int primary key, a int, b int, index(a)) on commit delete rows")
		tk.MustExec("truncate table tmp_t")
		tk.MustExec("begin")
	})
}

func TestGlobalTemporaryTableNoNetworkWithCreateAndThenCreateNormalTable(t *testing.T) {
	assertTemporaryTableNoNetwork(t, func(tk *testkit.TestKit) {
		tk.MustExec("create global temporary table tmp_t (id int primary key, a int, b int, index(a)) on commit delete rows")
		tk.MustExec("create table txx(a int)")
		tk.MustExec("begin")
	})
}

func TestLocalTemporaryTableNoNetworkWithCreateOutsideTxn(t *testing.T) {
	assertTemporaryTableNoNetwork(t, func(tk *testkit.TestKit) {
		tk.MustExec("create temporary table tmp_t (id int primary key, a int, b int, index(a))")
		tk.MustExec("begin")
	})
}

func TestLocalTemporaryTableNoNetworkWithInsideTxn(t *testing.T) {
	assertTemporaryTableNoNetwork(t, func(tk *testkit.TestKit) {
		tk.MustExec("begin")
		tk.MustExec("create temporary table tmp_t (id int primary key, a int, b int, index(a))")
	})
}

func assertTemporaryTableNoNetwork(t *testing.T, createTable func(*testkit.TestKit)) {
	var done sync.WaitGroup
	defer done.Wait()

	store := testkit.CreateMockStore(t)

	// Test that table reader/index reader/index lookup on the temporary table do not need to visit TiKV.
	tk := testkit.NewTestKit(t, store)
	tk1 := testkit.NewTestKit(t, store)

	tk.MustExec("use test")
	tk1.MustExec("use test")
	tk.MustExec("drop table if exists normal, tmp_t")
	tk.MustExec("create table normal (id int, a int, index(a))")
	createTable(tk)

	require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/store/mockstore/unistore/rpcServerBusy", "return(true)"))
	defer func() {
		require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/store/mockstore/unistore/rpcServerBusy"))
	}()

	tk.MustExec("insert into tmp_t values (1, 1, 1)")
	tk.MustExec("insert into tmp_t values (2, 2, 2)")

	// Make sure the fail point works.
	// With that failpoint, all requests to the TiKV is discard.
	rs, err := tk1.Exec("select * from normal")
	require.NoError(t, err)

	blocked := make(chan struct{}, 1)
	ctx, cancelFunc := context.WithCancel(context.Background())
	done.Add(1)
	go func() {
		defer done.Done()
		_, _ = session.ResultSetToStringSlice(ctx, tk1.Session(), rs)
		blocked <- struct{}{}
	}()

	select {
	case <-blocked:
		cancelFunc()
		require.FailNow(t, "The query should block when the failpoint is enabled.")
	case <-time.After(200 * time.Millisecond):
		cancelFunc()
	}

	// Check the temporary table do not send request to TiKV.
	// PointGet
	require.True(t, tk.HasPlan("select * from tmp_t where id=1", "Point_Get"))
	tk.MustQuery("select * from tmp_t where id=1").Check(testkit.Rows("1 1 1"))

	// BatchPointGet
	require.True(t, tk.HasPlan("select * from tmp_t where id in (1, 2)", "Batch_Point_Get"))
	tk.MustQuery("select * from tmp_t where id in (1, 2)").Check(testkit.Rows("1 1 1", "2 2 2"))

	// Table reader
	require.True(t, tk.HasPlan("select * from tmp_t", "TableReader"))
	tk.MustQuery("select * from tmp_t").Check(testkit.Rows("1 1 1", "2 2 2"))

	// Index reader
	require.True(t, tk.HasPlan("select /*+ USE_INDEX(tmp_t, a) */ a from tmp_t", "IndexReader"))
	tk.MustQuery("select /*+ USE_INDEX(tmp_t, a) */ a from tmp_t").Check(testkit.Rows("1", "2"))

	// Index lookup
	require.True(t, tk.HasPlan("select /*+ USE_INDEX(tmp_t, a) */ b from tmp_t where a = 1", "IndexLookUp"))
	tk.MustQuery("select /*+ USE_INDEX(tmp_t, a) */ b from tmp_t where a = 1").Check(testkit.Rows("1"))
	tk.MustExec("rollback")

	// prepare some data for local temporary table, when for global temporary table, the below operations have no effect.
	tk.MustExec("insert into tmp_t value(10, 10, 10)")
	tk.MustExec("insert into tmp_t value(11, 11, 11)")

	// Pessimistic lock
	tk.MustExec("begin pessimistic")
	tk.MustExec("insert into tmp_t values (3, 3, 3)")
	tk.MustExec("insert ignore into tmp_t values (4, 4, 4)")
	tk.MustExec("insert into tmp_t values (5, 5, 5) on duplicate key update a=100")
	tk.MustExec("insert into tmp_t values (10, 10, 10) on duplicate key update a=100")
	tk.MustExec("insert ignore into tmp_t values (10, 10, 10) on duplicate key update id=11")
	tk.MustExec("replace into tmp_t values(6, 6, 6)")
	tk.MustExec("replace into tmp_t values(11, 100, 100)")
	tk.MustExec("update tmp_t set id = id + 1 where a = 1")
	tk.MustExec("delete from tmp_t where a > 1")
	tk.MustQuery("select count(*) from tmp_t where a >= 1 for update")
	tk.MustExec("rollback")

	// Check 'for update' will not write any lock too when table is unmodified
	tk.MustExec("begin pessimistic")
	tk.MustExec("select * from tmp_t where id=1 for update")
	tk.MustExec("select * from tmp_t where id in (1, 2, 3) for update")
	tk.MustExec("select * from tmp_t where id > 1 for update")
	tk.MustExec("rollback")
}
