// 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"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/pingcap/failpoint"
	"github.com/pingcap/tidb/pkg/session"
	"github.com/pingcap/tidb/pkg/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")

	if tk.MustQuery("select @@tidb_schema_cache_size > 0").Equal(testkit.Rows("1")) {
		// infoschema v2 requires network, so it cannot be tested this way.
		t.Skip()
	}

	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/pkg/store/mockstore/unistore/rpcServerBusy", "return(true)"))
	defer func() {
		require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/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()
		if _, err := session.ResultSetToStringSlice(ctx, tk1.Session(), rs); err != nil {
			_ = rs.Close()
		}
		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
	tk.MustHavePlan("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
	tk.MustHavePlan("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
	tk.MustHavePlan("select * from tmp_t", "TableReader")
	tk.MustQuery("select * from tmp_t").Check(testkit.Rows("1 1 1", "2 2 2"))

	// Index reader
	tk.MustHavePlan("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
	tk.MustHavePlan("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")
}

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

	tk.MustExec("use test")
	tk.MustExec("drop table if exists users, users1;")
	tk.MustExec("CREATE GLOBAL TEMPORARY TABLE users (     id BIGINT,     v1 int,     v2 int,  v3 int, v4 int,   PRIMARY KEY(id), index v1_index(v1,v2,v3) ) ON COMMIT DELETE ROWS;")
	tk.MustExec("create table users1(id int, value int, index index_value(value));")
	tk.MustExec("insert into users1 values(1,2);")
	tk.MustExec("begin;")
	res := tk.MustQuery("explain analyze select /*+ inl_join(users) */ * from users use index(v1_index) where v1 in (select value from users1);").Rows()
	for _, row := range res {
		// if access object contains 'table:users', the execution info should be empty.
		if strings.Contains(row[4].(string), "table:users") && !strings.Contains(row[4].(string), "table:users1") {
			require.Len(t, row[5].(string), 0)
		}
	}
}
