package main

import (
	"regexp"
	"strings"
	"testing"

	"github.com/pingcap/tidb/pkg/parser/ast"
	"github.com/pingcap/tidb/pkg/plugin"
	"github.com/pingcap/tidb/pkg/sessionctx/variable"
	"github.com/stretchr/testify/require"
)

func Test_check(t *testing.T) {
	tableReg, _ := regexp.Compile("\\w+[23]")
	db, _ := regexp.Compile("sbtest2")
	c := AuditObjects([]AuditItem{
		{DB: db, Table: tableReg},
	})
	if !c.check("root", "sbtest2", "sbtest2", "") {
		t.Fatal("check failed")
	}
}

func TestEnabledGeneralEvents(t *testing.T) {
	cases := []struct {
		val     string
		enabled []plugin.GeneralEvent
		err     string
	}{
		{
			// test do nothing for the first time and an empty configuration will be returned
		},
		{
			val:     DefAuditLogGeneralEvents,
			enabled: []plugin.GeneralEvent{plugin.Starting, plugin.Completed, plugin.Error},
		},
		{
			val:     "STARTING",
			enabled: []plugin.GeneralEvent{plugin.Starting},
		},
		{
			val:     "STARTING,COMPLETED",
			enabled: []plugin.GeneralEvent{plugin.Starting, plugin.Completed},
		},
		{
			val:     "  error, completed    ",
			enabled: []plugin.GeneralEvent{plugin.Error, plugin.Completed},
		},
		{
			val:     "STARTING,",
			enabled: []plugin.GeneralEvent{plugin.Starting},
		},
		{
			val:     "STARTING,,  ,ERROR,",
			enabled: []plugin.GeneralEvent{plugin.Starting, plugin.Error},
		},
		{
			val: "EVENT1",
			err: "Invalid general event: EVENT1",
		},
		{
			val: "STARTING,EVENT2,ERROR",
			err: "Invalid general event: EVENT2",
		},
		{
			val: "STARTING COMPLETED",
			err: "Invalid general event: STARTING COMPLETED",
		},
	}

	var events AuditLogEnabledEvents
	enabled := make([]plugin.GeneralEvent, 0)

	for _, c := range cases {
		var err error
		if c.val != "" {
			err = events.SetSysVarTidbAuditLogGeneralEvents(c.val)
		}

		if c.err == "" {
			require.NoError(t, err)
			enabled = c.enabled
		} else {
			require.EqualError(t, err, c.err)
		}

		enabledStrs := make([]string, 0)
		enabledMap := make([]bool, plugin.GeneralEventCount)
		for _, e := range enabled {
			enabledMap[e] = true
			enabledStrs = append(enabledStrs, e.String())
		}
		for i, expectEnabled := range enabledMap {
			require.Equal(t, expectEnabled, events.IsGeneralEventEnabled(plugin.GeneralEvent(i)))
		}

		if c.err == "" {
			normalized, err := CheckAndNormalizeAuditLogGeneralEvents(c.val)
			require.NoError(t, err)
			require.Equal(t, strings.Join(enabledStrs, ","), normalized)
		} else {
			_, err = CheckAndNormalizeAuditLogGeneralEvents(c.val)
			require.EqualError(t, err, c.err)
		}
	}
}

func TestIsPasswordStmt(t *testing.T) {
	cases := []struct {
		stmtType    string
		sql         string
		hasPassword bool
	}{
		{
			stmtType:    ast.GetStmtLabel(&ast.CreateUserStmt{}),
			sql:         "create user u1",
			hasPassword: false,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.CreateUserStmt{}),
			sql:         "create user u1 identified by '123'",
			hasPassword: true,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.CreateUserStmt{}),
			sql:         "create user u1 identified with authentication_ldap_sasl as 'uid=u1_ldap,ou=People,dc=example,dc=com'",
			hasPassword: true,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.AlterUserStmt{}),
			sql:         "alter user u1",
			hasPassword: false,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.AlterUserStmt{}),
			sql:         "alter user u1 identified by '123'",
			hasPassword: true,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.AlterUserStmt{}),
			sql:         "alter user u1 identified with authentication_ldap_sasl as 'uid=u1_ldap,ou=People,dc=example,dc=com'",
			hasPassword: true,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.SetPwdStmt{}),
			sql:         "set password for u1 = '123'",
			hasPassword: true,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.SetStmt{}),
			sql:         "set @a=1",
			hasPassword: false,
		},
		{
			stmtType:    ast.GetStmtLabel(&ast.SelectStmt{}),
			sql:         "select * from t",
			hasPassword: false,
		},
	}

	for _, c := range cases {
		sessVars := variable.NewSessionVars(nil)
		sessVars.StmtCtx.StmtType = c.stmtType
		sessVars.StmtCtx.OriginalSQL = c.sql
		require.Equal(t, c.hasPassword, isPasswordStmt(sessVars))
	}
}

func TestAuditLogExtras(t *testing.T) {
	cases := []struct {
		val         string
		shouldError bool
		errorMsg    string
		extras      map[string]bool
	}{
		{
			val:    "",
			extras: map[string]bool{},
		},
		{
			val:    "CLOSE",
			extras: map[string]bool{"CLOSE": true},
		},
		{
			val:    "close",
			extras: map[string]bool{"CLOSE": true},
		},
		{
			val:    "  CLOSE  ",
			extras: map[string]bool{"CLOSE": true},
		},
		{
			val:    "CLOSE,CLOSE",
			extras: map[string]bool{"CLOSE": true},
		},
		{
			val:    " CLOSE , CLOSE ",
			extras: map[string]bool{"CLOSE": true},
		},
		{
			val:         "CLOSE,",
			shouldError: true,
			errorMsg:    "invalid extra \"\"",
		},
		{
			val:         "INVALID",
			shouldError: true,
			errorMsg:    "invalid extra \"INVALID\"",
		},
		{
			val:         "CLOSE,INVALID",
			shouldError: true,
			errorMsg:    "invalid extra \"INVALID\"",
		},
		{
			val:         ",",
			shouldError: true,
			errorMsg:    "invalid extra \"\"",
		},
	}

	for _, c := range cases {
		t.Run(c.val, func(t *testing.T) {
			// Test validation function
			_, err := auditLogIncludeExtrasSv.Validation(nil, c.val, c.val, 0)
			if c.shouldError {
				require.Error(t, err)
				require.Contains(t, err.Error(), c.errorMsg)
				return
			}
			require.NoError(t, err)

			err = auditLogIncludeExtrasSv.SetGlobal(nil, nil, c.val)
			require.NoError(t, err)

			for extra, expected := range c.extras {
				require.Equal(t, expected, auditLogExtrasIncludes(extra))
			}

			require.False(t, auditLogExtrasIncludes("NONEXISTENT"))
		})
	}
}
