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

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"testing"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/pingcap/tidb/pkg/kv"
	"github.com/pingcap/tidb/pkg/testkit"
	"github.com/stretchr/testify/require"
	"go.opencensus.io/stats/view"
)

var (
	tidbRootPassword       = flag.String("passwd", "", "tidb root password")
	tidbAPort              = flag.Int("tidb_a_port", 4001, "first tidb server listening port")
	tidbBPort              = flag.Int("tidb_b_port", 4002, "second tidb server listening port")
	ReadOnlyErrMsg         = "Error 1836: Running in read-only mode"
	ConflictErrMsg         = "Error 1105: can't turn off tidb_super_read_only when tidb_restricted_read_only is on"
	PriviledgedErrMsg      = "Error 1227: Access denied; you need (at least one of) the SUPER or SYSTEM_VARIABLES_ADMIN privilege(s) for this operation"
	TiDBRestrictedReadOnly = "tidb_restricted_read_only"
	TiDBSuperReadOnly      = "tidb_super_read_only"
)

type ReadOnlySuite struct {
	db  *sql.DB
	udb *sql.DB
	rdb *sql.DB
}

func checkVariable(t *testing.T, db *sql.DB, variable string, on bool) {
	var name, status string
	rs, err := db.Query(fmt.Sprintf("show variables like '%s'", variable))
	require.NoError(t, err)
	require.NoError(t, rs.Err())
	require.True(t, rs.Next())

	require.NoError(t, rs.Scan(&name, &status))
	require.Equal(t, name, variable)
	if on {
		require.Equal(t, "ON", status)
	} else {
		require.Equal(t, "OFF", status)
	}
	require.NoError(t, rs.Close())
}

func setVariableNoError(t *testing.T, db *sql.DB, variable string, status int) {
	_, err := db.Exec(fmt.Sprintf("set global %s=%d", variable, status))
	require.NoError(t, err)
}

func setVariable(_ *testing.T, db *sql.DB, variable string, status int) error {
	_, err := db.Exec(fmt.Sprintf("set global %s=%d", variable, status))
	return err
}

func createReadOnlySuite(t *testing.T) *ReadOnlySuite {
	s := new(ReadOnlySuite)
	var err error
	s.db, err = sql.Open("mysql", fmt.Sprintf("root:%s@(%s:%d)/test", *tidbRootPassword, "127.0.0.1", *tidbAPort))
	require.NoError(t, err)

	setVariableNoError(t, s.db, TiDBRestrictedReadOnly, 0)
	setVariableNoError(t, s.db, TiDBSuperReadOnly, 0)

	_, err = s.db.Exec("drop user if exists 'u1'@'%'")
	require.NoError(t, err)
	_, err = s.db.Exec("create user 'u1'@'%' identified by 'password'")
	require.NoError(t, err)
	_, err = s.db.Exec("grant all privileges on test.* to 'u1'@'%'")
	require.NoError(t, err)
	s.udb, err = sql.Open("mysql", fmt.Sprintf("u1:password@(%s:%d)/test", "127.0.0.1", *tidbBPort))
	require.NoError(t, err)
	_, err = s.db.Exec("drop user if exists 'r1'@'%'")
	require.NoError(t, err)
	_, err = s.db.Exec("create user 'r1'@'%' identified by 'password'")
	require.NoError(t, err)
	_, err = s.db.Exec("grant all privileges on test.* to 'r1'@'%'")
	require.NoError(t, err)
	_, err = s.db.Exec("grant RESTRICTED_REPLICA_WRITER_ADMIN on *.* to 'r1'@'%'")
	require.NoError(t, err)
	s.rdb, err = sql.Open("mysql", fmt.Sprintf("r1:password@(%s:%d)/test", "127.0.0.1", *tidbBPort))
	require.NoError(t, err)
	t.Cleanup(func() {
		require.NoError(t, s.db.Close())
		require.NoError(t, s.rdb.Close())
		require.NoError(t, s.udb.Close())
		view.Stop()
	})
	return s
}

func TestRestriction(t *testing.T) {
	s := createReadOnlySuite(t)

	var err error
	_, err = s.db.Exec("drop table if exists t")
	require.NoError(t, err)
	_, err = s.udb.Exec("create table t (a int primary key, b int)")
	require.NoError(t, err)
	_, err = s.udb.Exec("insert into t values (1, 1)")
	require.NoError(t, err)
	_, err = s.udb.Exec("update t set b = 2 where a = 1")
	require.NoError(t, err)

	setVariable(t, s.db, TiDBRestrictedReadOnly, 1)
	time.Sleep(1)

	checkVariable(t, s.udb, TiDBRestrictedReadOnly, true)
	checkVariable(t, s.udb, TiDBSuperReadOnly, true)

	checkVariable(t, s.rdb, TiDBRestrictedReadOnly, true)
	checkVariable(t, s.rdb, TiDBSuperReadOnly, true)

	// can't create table
	_, err = s.udb.Exec("create table t(a int)")
	require.Error(t, err)
	require.Equal(t, err.Error(), ReadOnlyErrMsg)

	// can't do point update when tidb_restricted_read_only is on
	_, err = s.udb.Exec("update t set b = 2 where a = 1")
	require.Error(t, err)
	require.Equal(t, err.Error(), ReadOnlyErrMsg)

	// can't insert
	_, err = s.udb.Exec("insert into t values (2, 3)")
	require.Error(t, err)
	require.Equal(t, err.Error(), ReadOnlyErrMsg)

	// can't turn off tidb_super_read_only if tidb_restricted_read_only is on
	err = setVariable(t, s.db, TiDBSuperReadOnly, 0)
	require.Error(t, err)
	require.Equal(t, err.Error(), ConflictErrMsg)

	// can't change global variable
	err = setVariable(t, s.udb, TiDBSuperReadOnly, 0)
	require.Error(t, err)
	require.Equal(t, err.Error(), PriviledgedErrMsg)

	err = setVariable(t, s.rdb, TiDBSuperReadOnly, 0)
	require.Error(t, err)
	require.Equal(t, err.Error(), PriviledgedErrMsg)

	// can't do flashback cluster
	_, err = s.udb.Exec("flashback cluster to timestamp ''")
	require.Error(t, err)
	require.Equal(t, err.Error(), ReadOnlyErrMsg)

	// can do some Admin stmts
	_, err = s.udb.Exec("admin show ddl jobs")
	require.NoError(t, err)
	_, err = s.udb.Exec("admin show slow recent 1")
	require.NoError(t, err)

	// turn off tidb_restricted_read_only does not affect tidb_super_read_only
	setVariableNoError(t, s.db, TiDBRestrictedReadOnly, 0)

	checkVariable(t, s.udb, TiDBRestrictedReadOnly, false)
	checkVariable(t, s.rdb, TiDBRestrictedReadOnly, false)

	checkVariable(t, s.udb, TiDBSuperReadOnly, true)
	checkVariable(t, s.rdb, TiDBSuperReadOnly, true)

	// it is now allowed to turn off tidb_super_read_only
	setVariableNoError(t, s.db, TiDBSuperReadOnly, 0)

	checkVariable(t, s.udb, TiDBRestrictedReadOnly, false)
	checkVariable(t, s.rdb, TiDBRestrictedReadOnly, false)

	checkVariable(t, s.udb, TiDBSuperReadOnly, false)
	checkVariable(t, s.rdb, TiDBSuperReadOnly, false)
}

func TestRestrictionWithConnectionPool(t *testing.T) {
	s := createReadOnlySuite(t)
	var err error
	_, err = s.db.Exec("drop table if exists t")
	require.NoError(t, err)
	_, err = s.db.Exec("create table t (a int)")
	require.NoError(t, err)

	conn, err := s.udb.Conn(context.Background())
	require.NoError(t, err)
	defer func() {
		require.NoError(t, conn.Close())
	}()
	done := make(chan bool)
	go func(conn *sql.Conn, done chan bool) {
		ticker := time.NewTicker(50 * time.Millisecond)
		for {
			t := <-ticker.C
			_, err := conn.ExecContext(context.Background(), fmt.Sprintf("insert into t values (%d)", t.Nanosecond()))
			if err != nil {
				if err.Error() == ReadOnlyErrMsg {
					done <- true
				} else {
					done <- false
				}
				return
			}
		}
	}(conn, done)
	time.Sleep(1 * time.Second)

	timer := time.NewTimer(10 * time.Second)
	setVariableNoError(t, s.db, TiDBRestrictedReadOnly, 1)
	select {
	case <-timer.C:
		require.Fail(t, "failed")
	case success := <-done:
		require.True(t, success)
	}
}

func TestReplicationWriter(t *testing.T) {
	s := createReadOnlySuite(t)
	_, err := s.db.Exec("set global tidb_restricted_read_only=0")
	require.NoError(t, err)
	_, err = s.db.Exec("drop table if exists t")
	require.NoError(t, err)
	_, err = s.db.Exec("create table t (a int)")
	require.NoError(t, err)

	conn, err := s.rdb.Conn(context.Background())
	require.NoError(t, err)
	defer func() {
		require.NoError(t, conn.Close())
	}()
	done := make(chan struct{})
	go func(conn *sql.Conn) {
		ticker := time.NewTicker(50 * time.Millisecond)
		for {
			select {
			case t1 := <-ticker.C:
				_, err := conn.ExecContext(context.Background(), fmt.Sprintf("insert into t values (%d)", t1.Nanosecond()))
				require.NoError(t, err)
			case <-done:
				return
			}
		}
	}(conn)
	time.Sleep(1 * time.Second)
	timer := time.NewTimer(3 * time.Second)
	_, err = s.db.Exec("set global tidb_restricted_read_only=1")
	require.NoError(t, err)
	// SUPER user can't write
	_, err = s.db.Exec("insert into t values (1)")
	require.Equal(t, err.Error(), ReadOnlyErrMsg)
	<-timer.C
	done <- struct{}{}
}

func TestInternalSQL(t *testing.T) {
	ctx := kv.WithInternalSourceType(context.Background(), kv.InternalTxnStats)
	store := testkit.CreateMockStore(t)
	tk := testkit.NewTestKit(t, store)

	defer func() {
		tk.MustExec("set global tidb_restricted_read_only=default")
		tk.MustExec("set global tidb_super_read_only=default")
	}()

	tk.MustExec("set global tidb_restricted_read_only=On")
	tk.MustExec("set global tidb_super_read_only=On")

	sql := "insert into mysql.stats_top_n (table_id, is_index, hist_id, value, count) values (874, 0, 1, 'a', 3)"
	_, err := tk.Session().ExecuteInternal(ctx, sql)
	require.NoError(t, err)
}
