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

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"testing"

	"github.com/pingcap/tidb/kv"
	"github.com/pingcap/tidb/parser/mysql"
	"github.com/pingcap/tidb/plugin"
	"github.com/pingcap/tidb/server"
	"github.com/pingcap/tidb/session"
	"github.com/pingcap/tidb/sessionctx/variable"
	"github.com/pingcap/tidb/testkit"
	"github.com/stretchr/testify/require"
)

// Audit tests cannot run in parallel.
func TestAuditLogNormal(t *testing.T) {
	store := testkit.CreateMockStore(t)
	sv := server.CreateMockServer(t, store)
	defer sv.Close()
	conn := server.CreateMockConn(t, sv)
	defer conn.Close()
	session.DisableStats4Test()
	session.SetSchemaLease(0)

	type normalTest struct {
		sql      string
		text     string
		rows     uint64
		stmtType string
		dbs      string
		tables   string
		cmd      string
		event    plugin.GeneralEvent
		resCnt   int
	}

	tests := []normalTest{
		{
			sql:      "CREATE DATABASE mynewdatabase",
			stmtType: "CreateDatabase",
			dbs:      "mynewdatabase",
		},
		{
			sql:      "CREATE TABLE t1 (a INT NOT NULL)",
			stmtType: "CreateTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "CREATE TABLE t2 LIKE t1",
			stmtType: "CreateTable",
			dbs:      "test,test",
			tables:   "t2,t1",
		},
		{
			sql:      "CREATE INDEX a ON t1 (a)",
			stmtType: "CreateIndex",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "CREATE SEQUENCE seq",
			stmtType: "other",
			dbs:      "test",
			tables:   "seq",
		},
		{
			sql:      " create temporary table t3 (a int)",
			stmtType: "CreateTable",
			dbs:      "test",
			tables:   "t3",
		},
		{
			sql:      "create global temporary table t4 (a int) on commit delete rows",
			stmtType: "CreateTable",
			dbs:      "test",
			tables:   "t4",
		},
		{
			sql:      "CREATE VIEW v1 AS SELECT * FROM t1 WHERE  a> 2",
			stmtType: "CreateView",
			dbs:      "test,test",
			tables:   "t1,v1",
		},
		{
			sql:      "USE test",
			stmtType: "Use",
		},
		{
			sql:      "DROP DATABASE mynewdatabase",
			stmtType: "DropDatabase",
			dbs:      "mynewdatabase",
		},
		{
			sql:      "SHOW CREATE SEQUENCE seq",
			stmtType: "Show",
			dbs:      "test",
			tables:   "seq",
		},
		{
			sql:      "DROP SEQUENCE seq",
			stmtType: "other",
			dbs:      "test",
			tables:   "seq",
		},
		{
			sql:      "DROP TABLE t4",
			stmtType: "DropTable",
			dbs:      "test",
			tables:   "t4",
		},
		{
			sql:      "DROP VIEW v1",
			stmtType: "DropView",
			dbs:      "test",
			tables:   "v1",
		},
		{
			sql:      "ALTER TABLE t1 ADD COLUMN c1 INT NOT NULL",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 MODIFY c1 BIGINT",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 ADD INDEX (c1)",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 ALTER INDEX c1 INVISIBLE",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 RENAME INDEX c1 TO c2",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 DROP INDEX c2",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 CHANGE c1 c2 INT",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "ALTER TABLE t1 DROP COLUMN c2",
			stmtType: "AlterTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "CREATE SESSION BINDING FOR SELECT * FROM t1 WHERE a = 123 USING SELECT * FROM t1 IGNORE INDEX (a) WHERE a = 123",
			stmtType: "CreateBinding",
		},
		{
			sql:      "DROP SESSION BINDING FOR SELECT * FROM t1 WHERE a = 123",
			stmtType: "DropBinding",
		},
		// {
		//	sql: "LOAD STATS '/tmp/stats.json'",
		//	stmtType: "other",
		// },
		// {
		//	sql: "DROP STATS t",
		//	stmtType: "other",
		// },
		{
			sql:      "RENAME TABLE t2 TO t5",
			stmtType: "other",
			dbs:      "test,test",
			tables:   "t2,t5",
		},
		{
			sql:      "TRUNCATE t1",
			stmtType: "TruncateTable",
			dbs:      "test",
			tables:   "t1",
		},
		// {
		//	sql: "FLASHBACK TABLE t TO t1",
		//	stmtType: "other",
		//	dbs: "test",
		//	tables: "t1",
		// },
		// {
		//	sql: "RECOVER TABLE t1",
		//	stmtType: "other",
		//	dbs: "test",
		//	tables: "t1,t2",
		// },
		{
			sql:      "ALTER DATABASE test DEFAULT CHARACTER SET = utf8mb4",
			stmtType: "other",
			dbs:      "test",
		},
		{
			sql:      "ADMIN RELOAD opt_rule_blacklist",
			stmtType: "other",
		},
		// {
		//	sql: "ADMIN PLUGINS ENABLE audit_test",
		//	stmtType: "other",
		// },
		{
			sql:      "ADMIN FLUSH bindings",
			stmtType: "other",
		},
		// {
		//	sql: "ADMIN REPAIR TABLE t1 CREATE TABLE (id int)",
		//	stmtType: "other",
		//	dbs: "test",
		//	tables: "t1",
		// },
		{
			sql:      "ADMIN SHOW SLOW RECENT 10",
			stmtType: "other",
		},
		{
			sql:      "ADMIN SHOW DDL JOBS",
			stmtType: "other",
		},
		// {
		//	sql: "ADMIN CANCEL DDL JOBS 1",
		//	stmtType: "other",
		// },
		{
			sql:      "ADMIN CHECKSUM TABLE t1",
			stmtType: "other",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "ADMIN CHECK TABLE t1",
			stmtType: "other",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "ADMIN CHECK INDEX t1 a",
			stmtType: "other",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "CREATE USER 'newuser' IDENTIFIED BY 'newuserpassword'",
			stmtType: "CreateUser",
		},
		{
			sql:      "ALTER USER 'newuser' IDENTIFIED BY 'newnewpassword'",
			stmtType: "other",
		},
		{
			sql:      "CREATE ROLE analyticsteam",
			stmtType: "CreateUser",
		},
		{
			sql:      "GRANT SELECT ON test.* TO analyticsteam",
			stmtType: "Grant",
			dbs:      "test",
		},
		{
			sql:      "GRANT analyticsteam TO 'newuser'",
			stmtType: "other",
		},
		{
			sql:      "SET DEFAULT ROLE analyticsteam TO newuser;",
			stmtType: "other",
		},
		{
			sql:      "REVOKE SELECT ON test.* FROM 'analyticsteam'",
			stmtType: "Revoke",
			dbs:      "test",
		},
		{
			sql:      "DROP ROLE analyticsteam",
			stmtType: "other",
		},
		{
			sql:      "FLUSH PRIVILEGES",
			stmtType: "other",
		},
		{
			sql:      "SET PASSWORD FOR 'newuser' = 'test'",
			stmtType: "Set",
		},
		// {
		//	sql: "SET ROLE ALL",
		//	stmtType: "other",
		// },
		{
			sql:      "DROP USER 'newuser'",
			stmtType: "other",
		},
		{
			sql:      "analyze table t1",
			stmtType: "AnalyzeTable",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "SPLIT TABLE t1 BETWEEN (0) AND (1000000000) REGIONS 16",
			stmtType: "other",
			// dbs: "test",
			// tables: "t1",
		},
		// {
		//	sql: "BACKUP DATABASE `test` TO '.'",
		//	stmtType: "other",
		//	dbs: "test",
		// },
		// {
		//	sql: "RESTORE DATABASE * FROM '.'",
		//	stmtType: "other",
		// },
		// {
		//	sql: "CHANGE DRAINER TO NODE_STATE ='paused' FOR NODE_ID 'drainer1'",
		//	stmtType: "other",
		// },
		// {
		//	sql: "CHANGE PUMP TO NODE_STATE ='paused' FOR NODE_ID 'pump1'",
		//	stmtType: "other",
		// },
		{
			sql:      "BEGIN",
			stmtType: "Begin",
		},
		{
			sql:      "ROLLBACK",
			stmtType: "Rollback",
		},
		{
			sql:      "START TRANSACTION",
			stmtType: "Begin",
		},
		{
			sql:      "COMMIT",
			stmtType: "Commit",
		},
		// {
		//	sql: "SHOW DRAINER STATUS",
		//	stmtType: "Show",
		// },
		// {
		//	sql: "SHOW PUMP STATUS",
		//	stmtType: "Show",
		// },
		// {
		//	sql: "SHOW GRANTS",
		//	stmtType: "Show",
		// },
		{
			sql:      "SHOW PROCESSLIST",
			stmtType: "Show",
		},
		// {
		//	sql: "SHOW BACKUPS",
		//	stmtType: "Show",
		// },
		// {
		//	sql: "SHOW RESTORES",
		//	stmtType: "Show",
		// },
		{
			sql:      "show analyze status",
			stmtType: "Show",
		},
		{
			sql:      "SHOW SESSION BINDINGS",
			stmtType: "Show",
		},
		{
			sql:      "SHOW BUILTINS",
			stmtType: "Show",
		},
		{
			sql:      "SHOW CHARACTER SET",
			stmtType: "Show",
		},
		{
			sql:      "SHOW COLLATION",
			stmtType: "Show",
		},
		{
			sql:      "show columns from t1",
			stmtType: "Show",
		},
		{
			sql:      "show fields from t1",
			stmtType: "Show",
		},
		// {
		//	sql: "SHOW CONFIG",
		//	stmtType: "Show",
		// },
		{
			sql:      "SHOW CREATE TABLE t1",
			stmtType: "Show",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "SHOW CREATE USER 'root'",
			stmtType: "Show",
		},
		{
			sql:      "SHOW DATABASES",
			stmtType: "Show",
		},
		{
			sql:      "SHOW ENGINES",
			stmtType: "Show",
		},
		{
			sql:      "SHOW ERRORS",
			stmtType: "Show",
		},
		{
			sql:      "SHOW INDEXES FROM t1",
			stmtType: "Show",
		},
		{
			sql:      "SHOW MASTER STATUS",
			stmtType: "Show",
		},
		{
			sql:      "SHOW PLUGINS",
			stmtType: "Show",
		},
		{
			sql:      "show privileges",
			stmtType: "Show",
		},
		{
			sql:      "SHOW PROFILES",
			stmtType: "Show",
		},
		// {
		//	sql: "SHOW PUMP STATUS",
		//	stmtType: "Show",
		// },
		{
			sql:      "SHOW SCHEMAS",
			stmtType: "Show",
		},
		{
			sql:      "SHOW STATS_HEALTHY",
			stmtType: "Show",
			dbs:      "mysql",
		},
		{
			sql:      "show stats_histograms",
			stmtType: "Show",
			dbs:      "mysql",
			tables:   "stats_histograms",
		},
		{
			sql:      "show stats_meta",
			stmtType: "Show",
			dbs:      "mysql",
			tables:   "stats_meta",
		},
		{
			sql:      "show status",
			stmtType: "Show",
		},
		{
			sql:      "show table t1 next_row_id",
			stmtType: "Show",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "show table t1 regions",
			stmtType: "Show",
		},
		{
			sql:      "SHOW TABLE STATUS LIKE 't1'",
			stmtType: "Show",
			resCnt:   3, // Start + SHOW TABLE + Internal SELECT .. FROM IS.TABLES in current session
		},
		{
			sql:      "SHOW TABLES",
			stmtType: "Show",
		},
		{
			sql:      "SHOW VARIABLES",
			stmtType: "Show",
		},
		{
			sql:      "SHOW WARNINGS",
			stmtType: "Show",
		},
		{
			sql:      "SET @number = 5",
			stmtType: "Set",
		},
		{
			sql:      "SET NAMES utf8",
			stmtType: "Set",
		},
		{
			sql:      "SET CHARACTER SET utf8mb4",
			stmtType: "Set",
		},
		{
			sql:      "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
			stmtType: "Set",
		},
		{
			sql:      "SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER'",
			stmtType: "Set",
		},
		{
			sql:      "PREPARE mystmt FROM 'SELECT ? as num FROM DUAL'",
			stmtType: "Prepare",
		},
		{
			sql:      "EXECUTE mystmt USING @number",
			text:     "SELECT ? as num FROM DUAL",
			stmtType: "Select",
		},
		{
			sql:      "DEALLOCATE PREPARE mystmt",
			stmtType: "Deallocate",
		},
		{
			sql:      "INSERT INTO t1 VALUES (1), (2)",
			stmtType: "Insert",
			dbs:      "test",
			tables:   "t1",
			rows:     2,
		},
		{
			sql:      "DELETE FROM t1 WHERE a = 2",
			stmtType: "Delete",
			dbs:      "test",
			tables:   "t1",
			rows:     1,
		},
		{
			sql:      "REPLACE INTO t1 VALUES(3)",
			stmtType: "Replace",
			dbs:      "test",
			tables:   "t1",
			rows:     1,
		},
		{
			sql:      "UPDATE t1 SET a=5 WHERE a=1",
			stmtType: "Update",
			dbs:      "test",
			tables:   "t1",
			rows:     1,
		},
		{
			sql:      "DO 1",
			stmtType: "other",
		},
		// {
		//	sql: "LOAD DATA LOCAL INFILE 'data.csv' INTO TABLE t1 FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\r\n' IGNORE 1 LINES (id)",
		//	stmtType: "LoadData",
		//	dbs: "test",
		//	tables: "t1",
		// },
		{
			sql:      "SELECT * FROM t1",
			stmtType: "Select",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "SELECT 1",
			stmtType: "Select",
		},
		{
			sql:      "TABLE t1",
			stmtType: "Select",
			dbs:      "test",
			tables:   "t1",
		},
		{
			sql:      "EXPLAIN ANALYZE SELECT * FROM t1 WHERE a = 1",
			stmtType: "ExplainAnalyzeSQL",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "EXPLAIN SELECT * FROM t1",
			stmtType: "ExplainSQL",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "EXPLAIN SELECT * FROM t1 WHERE a = 1",
			stmtType: "ExplainSQL",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "DESC SELECT * FROM t1 WHERE a = 1",
			stmtType: "ExplainSQL",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "DESCRIBE SELECT * FROM t1 WHERE a = 1",
			stmtType: "ExplainSQL",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "trace format='row' select * from t1",
			stmtType: "Trace",
			// dbs: "test",
			// tables: "t1",
		},
		{
			sql:      "flush status",
			stmtType: "other",
		},
		{
			sql:      "FLUSH TABLES",
			stmtType: "other",
		},
		// {
		//	sql: "KILL TIDB 2",
		//	stmtType: "other",
		// },
		// {
		//	sql: "SHUTDOWN",
		//	stmtType: "Shutdow",
		// },
		// {
		//	sql: "ALTER INSTANCE RELOAD TLS",
		//	stmtType: "other",
		// },
	}

	testResults := make([]normalTest, 0)
	dbNames := make([]string, 0)
	tableNames := make([]string, 0)
	onGeneralEvent := func(ctx context.Context, sctx *variable.SessionVars, event plugin.GeneralEvent, cmd string) {
		dbNames = dbNames[:0]
		tableNames = tableNames[:0]
		for _, value := range sctx.StmtCtx.Tables {
			dbNames = append(dbNames, value.DB)
			tableNames = append(tableNames, value.Table)
		}
		audit := normalTest{
			text:     sctx.StmtCtx.OriginalSQL,
			rows:     sctx.StmtCtx.AffectedRows(),
			stmtType: sctx.StmtCtx.StmtType,
			dbs:      strings.Join(dbNames, ","),
			tables:   strings.Join(tableNames, ","),
			cmd:      cmd,
			event:    event,
		}
		testResults = append(testResults, audit)
	}
	loadPlugin(t, onGeneralEvent)
	defer plugin.Shutdown(context.Background())

	require.NoError(t, conn.HandleQuery(context.Background(), "use test"))
	for _, test := range tests {
		testResults = testResults[:0]
		errMsg := fmt.Sprintf("statement: %s", test.sql)
		query := append([]byte{mysql.ComQuery}, []byte(test.sql)...)
		ctx := kv.WithInternalSourceType(context.Background(), kv.InternalTxnOthers)
		err := conn.Dispatch(ctx, query)
		require.NoError(t, err, errMsg)
		resultCount := test.resCnt
		if resultCount == 0 {
			resultCount = 2
		}
		require.Equal(t, resultCount, len(testResults), errMsg)

		result := testResults[0]
		require.Equal(t, "Query", result.cmd, errMsg)
		require.Equal(t, plugin.Starting, result.event, errMsg)

		result = testResults[resultCount-1]
		require.Equal(t, "Query", result.cmd, errMsg)
		if test.text == "" {
			require.Equal(t, test.sql, result.text, errMsg)
		} else {
			require.Equal(t, test.text, result.text, errMsg)
		}
		require.Equal(t, test.rows, result.rows, errMsg)
		require.Equal(t, test.stmtType, result.stmtType, errMsg)
		require.Equal(t, test.dbs, result.dbs, errMsg)
		require.Equal(t, test.tables, result.tables, errMsg)
		require.Equal(t, "Query", result.cmd, errMsg)
		require.Equal(t, plugin.Completed, result.event, errMsg)
		for i := 1; i < resultCount-1; i++ {
			result = testResults[i]
			require.Equal(t, "Query", result.cmd, errMsg)
			require.Equal(t, plugin.Completed, result.event, errMsg)
		}
	}
}

func loadPlugin(t *testing.T, onGeneralEvent func(context.Context, *variable.SessionVars, plugin.GeneralEvent, string)) {
	ctx := context.Background()
	pluginName := "audit_test"
	pluginVersion := uint16(1)
	pluginSign := pluginName + "-" + strconv.Itoa(int(pluginVersion))

	cfg := plugin.Config{
		Plugins:    []string{pluginSign},
		PluginDir:  "",
		EnvVersion: map[string]uint16{"go": 1112},
	}

	validate := func(ctx context.Context, manifest *plugin.Manifest) error {
		return nil
	}
	onInit := func(ctx context.Context, manifest *plugin.Manifest) error {
		return nil
	}
	onShutdown := func(ctx context.Context, manifest *plugin.Manifest) error {
		return nil
	}
	onConnectionEvent := func(ctx context.Context, event plugin.ConnectionEvent, info *variable.ConnectionInfo) error {
		return nil
	}

	// setup load test hook.
	loadOne := func(p *plugin.Plugin, dir string, pluginID plugin.ID) (manifest func() *plugin.Manifest, err error) {
		return func() *plugin.Manifest {
			m := &plugin.AuditManifest{
				Manifest: plugin.Manifest{
					Kind:       plugin.Audit,
					Name:       pluginName,
					Version:    pluginVersion,
					OnInit:     onInit,
					OnShutdown: onShutdown,
					Validate:   validate,
				},
				OnGeneralEvent:    onGeneralEvent,
				OnConnectionEvent: onConnectionEvent,
			}
			return plugin.ExportManifest(m)
		}, nil
	}
	plugin.SetTestHook(loadOne)

	// trigger load.
	err := plugin.Load(ctx, cfg)
	require.NoErrorf(t, err, "load plugin [%s] fail, error [%s]\n", pluginSign, err)

	err = plugin.Init(ctx, cfg)
	require.NoErrorf(t, err, "init plugin [%s] fail, error [%s]\n", pluginSign, err)
}
