//go:build integration
// +build integration

package integration_test

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

	"github.com/everything-on-mysql/cassandra-on-mysql/pkg/cql"
)

func TestUserDDLLifecycle(t *testing.T) {
	session := newSession(t, sessionOptions{disableHostLookup: true})

	user := uniqueTiDBUser()
	password1 := "secret-1"
	password2 := "secret-2"
	dropTiDBUser(t, user)
	t.Cleanup(func() {
		dropTiDBUser(t, user)
	})

	create := fmt.Sprintf("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s' NOSUPERUSER", user, password1)
	if err := session.Query(create).Exec(); err != nil {
		t.Fatalf("create user failed: %v", err)
	}
	if err := pingTiDBUser(user, password1); err != nil {
		t.Fatalf("new TiDB user cannot authenticate: %v", err)
	}
	userSession := newSession(t, sessionOptions{
		disableHostLookup: true,
		username:          user,
		password:          password1,
	})
	if _, err := scanRows(userSession, "SELECT key FROM system.local"); err != nil {
		t.Fatalf("new CQL user cannot query system.local: %v", err)
	}

	rows, err := scanRows(session, "LIST USERS")
	if err != nil {
		t.Fatalf("list users failed: %v", err)
	}
	if !rowsContainUser(rows, user) {
		t.Fatalf("LIST USERS did not include %q: %#v", user, rows)
	}
	authRows, err := scanRows(session, "SELECT role, can_login FROM system_auth.roles")
	if err != nil {
		t.Fatalf("system_auth.roles query failed: %v", err)
	}
	if !authRowsContainRole(authRows, user) {
		t.Fatalf("system_auth.roles did not include %q: %#v", user, authRows)
	}
	keyspaceRows, err := scanRows(session, "SELECT keyspace_name FROM system_schema.keyspaces WHERE keyspace_name = 'system_auth'")
	if err != nil {
		t.Fatalf("system_auth keyspace metadata query failed: %v", err)
	}
	if len(keyspaceRows) == 0 {
		t.Fatalf("system_schema.keyspaces did not include system_auth")
	}

	alter := fmt.Sprintf("ALTER USER IF EXISTS %s WITH PASSWORD '%s'", user, password2)
	if err := session.Query(alter).Exec(); err != nil {
		t.Fatalf("alter user failed: %v", err)
	}
	if err := pingTiDBUser(user, password1); err == nil {
		t.Fatalf("old password still authenticates after ALTER USER")
	}
	if err := pingTiDBUser(user, password2); err != nil {
		t.Fatalf("new password does not authenticate after ALTER USER: %v", err)
	}

	drop := fmt.Sprintf("DROP USER IF EXISTS %s", user)
	if err := session.Query(drop).Exec(); err != nil {
		t.Fatalf("drop user failed: %v", err)
	}
	if err := pingTiDBUser(user, password2); err == nil {
		t.Fatalf("dropped user still authenticates")
	}
	if droppedSession, err := tryNewSession(t, sessionOptions{
		disableHostLookup: true,
		username:          user,
		password:          password2,
	}); err == nil {
		droppedSession.Close()
		t.Fatalf("dropped CQL user still authenticates")
	}
	if err := session.Query(drop).Exec(); err != nil {
		t.Fatalf("drop missing user with IF EXISTS failed: %v", err)
	}
}

func TestUserDDLSuperuserRejected(t *testing.T) {
	session := newSession(t, sessionOptions{disableHostLookup: true})

	user := uniqueTiDBUser()
	err := session.Query(fmt.Sprintf("CREATE USER %s WITH PASSWORD 'secret' SUPERUSER", user)).Exec()
	requireErrorCode(t, err, cql.ErrorCodeInvalid)
}

func TestGrantKeyspaceToUser(t *testing.T) {
	session := newSession(t, sessionOptions{disableHostLookup: true})

	keyspace := uniqueKeyspace()
	table := "grant_items"
	user := uniqueTiDBUser()
	password := "grant-secret"
	dropTiDBUser(t, user)
	t.Cleanup(func() {
		dropTiDBUser(t, user)
		_ = session.Query(dropKeyspaceCQL(keyspace)).Exec()
	})

	if err := session.Query(createKeyspaceCQL(keyspace)).Exec(); err != nil {
		t.Fatalf("create keyspace failed: %v", err)
	}
	if err := session.Query(fmt.Sprintf("CREATE TABLE %s.%s (id int PRIMARY KEY, name text)", keyspace, table)).Exec(); err != nil {
		t.Fatalf("create table failed: %v", err)
	}
	if err := session.Query(fmt.Sprintf("INSERT INTO %s.%s (id, name) VALUES (1, 'seed')", keyspace, table)).Exec(); err != nil {
		t.Fatalf("seed insert failed: %v", err)
	}
	if err := session.Query(fmt.Sprintf("CREATE USER %s WITH PASSWORD '%s' NOSUPERUSER", user, password)).Exec(); err != nil {
		t.Fatalf("create user failed: %v", err)
	}

	if err := session.Query(fmt.Sprintf("GRANT SELECT ON KEYSPACE %s TO %s", keyspace, user)).Exec(); err != nil {
		if strings.Contains(err.Error(), "Grant Option") {
			t.Skipf("current TiDB test user lacks GRANT OPTION: %v", err)
		}
		t.Fatalf("grant select failed: %v", err)
	}
	userSession := newSession(t, sessionOptions{
		disableHostLookup: true,
		username:          user,
		password:          password,
		keyspace:          keyspace,
	})
	var name string
	if err := userSession.Query(fmt.Sprintf("SELECT name FROM %s WHERE id = 1", table)).Scan(&name); err != nil {
		t.Fatalf("user cannot select from granted keyspace: %v", err)
	}
	if name != "seed" {
		t.Fatalf("selected name = %q, want seed", name)
	}
	if err := userSession.Query(fmt.Sprintf("INSERT INTO %s (id, name) VALUES (2, 'blocked')", table)).Exec(); err == nil {
		t.Fatalf("insert succeeded before MODIFY grant")
	}

	if err := session.Query(fmt.Sprintf("GRANT MODIFY ON KEYSPACE %s TO %s", keyspace, user)).Exec(); err != nil {
		if strings.Contains(err.Error(), "Grant Option") {
			t.Skipf("current TiDB test user lacks GRANT OPTION: %v", err)
		}
		t.Fatalf("grant modify failed: %v", err)
	}
	userSession.Close()
	userSession = newSession(t, sessionOptions{
		disableHostLookup: true,
		username:          user,
		password:          password,
		keyspace:          keyspace,
	})
	if err := userSession.Query(fmt.Sprintf("INSERT INTO %s (id, name) VALUES (2, 'granted')", table)).Exec(); err != nil {
		t.Fatalf("user cannot insert after MODIFY grant: %v", err)
	}
}

func uniqueTiDBUser() string {
	now := time.Now().UnixNano()
	return fmt.Sprintf("cu_%x", now)
}

func pingTiDBUser(user, password string) error {
	cfg := mysqlConfig{
		host:     mysqlCfg.host,
		port:     mysqlCfg.port,
		user:     user,
		password: password,
	}
	db, err := sql.Open("mysql", mysqlDSN(cfg))
	if err != nil {
		return err
	}
	defer db.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return db.PingContext(ctx)
}

func dropTiDBUser(t *testing.T, user string) {
	t.Helper()
	db, err := sql.Open("mysql", mysqlDSN(mysqlCfg))
	if err != nil {
		t.Fatalf("open mysql cleanup connection failed: %v", err)
	}
	defer db.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	query := fmt.Sprintf("DROP USER IF EXISTS %s", quoteTiDBTestAccount(user, "%"))
	if _, err := db.ExecContext(ctx, query); err != nil {
		t.Fatalf("cleanup drop user failed: %v", err)
	}
}

func rowsContainUser(rows []map[string]interface{}, user string) bool {
	for _, row := range rows {
		if asString(row["name"]) == user {
			return true
		}
	}
	return false
}

func authRowsContainRole(rows []map[string]interface{}, role string) bool {
	for _, row := range rows {
		if asString(row["role"]) == role {
			return true
		}
	}
	return false
}

func quoteTiDBTestAccount(user, host string) string {
	return quoteTiDBTestString(user) + "@" + quoteTiDBTestString(host)
}

func quoteTiDBTestString(value string) string {
	escaped := strings.ReplaceAll(value, `'`, `''`)
	return "'" + escaped + "'"
}
