// Copyright 2023 The go-fuzz-headers Authors.
//
// 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 gofuzzheaders

import (
	"fmt"
	"strings"
)

// returns a keyword by index
func getKeyword(f *ConsumeFuzzer) (string, error) {
	index, err := f.GetInt()
	if err != nil {
		return keywords[0], err
	}
	for i, k := range keywords {
		if i == index {
			return k, nil
		}
	}
	return keywords[0], fmt.Errorf("could not get a kw")
}

// Simple utility function to check if a string
// slice contains a string.
func containsString(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

// These keywords are used specifically for fuzzing Vitess
var keywords = []string{
	"accessible", "action", "add", "after", "against", "algorithm",
	"all", "alter", "always", "analyze", "and", "as", "asc", "asensitive",
	"auto_increment", "avg_row_length", "before", "begin", "between",
	"bigint", "binary", "_binary", "_utf8mb4", "_utf8", "_latin1", "bit",
	"blob", "bool", "boolean", "both", "by", "call", "cancel", "cascade",
	"cascaded", "case", "cast", "channel", "change", "char", "character",
	"charset", "check", "checksum", "coalesce", "code", "collate", "collation",
	"column", "columns", "comment", "committed", "commit", "compact", "complete",
	"compressed", "compression", "condition", "connection", "constraint", "continue",
	"convert", "copy", "cume_dist", "substr", "substring", "create", "cross",
	"csv", "current_date", "current_time", "current_timestamp", "current_user",
	"cursor", "data", "database", "databases", "day", "day_hour", "day_microsecond",
	"day_minute", "day_second", "date", "datetime", "dec", "decimal", "declare",
	"default", "definer", "delay_key_write", "delayed", "delete", "dense_rank",
	"desc", "describe", "deterministic", "directory", "disable", "discard",
	"disk", "distinct", "distinctrow", "div", "double", "do", "drop", "dumpfile",
	"duplicate", "dynamic", "each", "else", "elseif", "empty", "enable",
	"enclosed", "encryption", "end", "enforced", "engine", "engines", "enum",
	"error", "escape", "escaped", "event", "exchange", "exclusive", "exists",
	"exit", "explain", "expansion", "export", "extended", "extract", "false",
	"fetch", "fields", "first", "first_value", "fixed", "float", "float4",
	"float8", "flush", "for", "force", "foreign", "format", "from", "full",
	"fulltext", "function", "general", "generated", "geometry", "geometrycollection",
	"get", "global", "gtid_executed", "grant", "group", "grouping", "groups",
	"group_concat", "having", "header", "high_priority", "hosts", "hour", "hour_microsecond",
	"hour_minute", "hour_second", "if", "ignore", "import", "in", "index", "indexes",
	"infile", "inout", "inner", "inplace", "insensitive", "insert", "insert_method",
	"int", "int1", "int2", "int3", "int4", "int8", "integer", "interval",
	"into", "io_after_gtids", "is", "isolation", "iterate", "invoker", "join",
	"json", "json_table", "key", "keys", "keyspaces", "key_block_size", "kill", "lag",
	"language", "last", "last_value", "last_insert_id", "lateral", "lead", "leading",
	"leave", "left", "less", "level", "like", "limit", "linear", "lines",
	"linestring", "load", "local", "localtime", "localtimestamp", "lock", "logs",
	"long", "longblob", "longtext", "loop", "low_priority", "manifest",
	"master_bind", "match", "max_rows", "maxvalue", "mediumblob", "mediumint",
	"mediumtext", "memory", "merge", "microsecond", "middleint", "min_rows", "minute",
	"minute_microsecond", "minute_second", "mod", "mode", "modify", "modifies",
	"multilinestring", "multipoint", "multipolygon", "month", "name",
	"names", "natural", "nchar", "next", "no", "none", "not", "no_write_to_binlog",
	"nth_value", "ntile", "null", "numeric", "of", "off", "offset", "on",
	"only", "open", "optimize", "optimizer_costs", "option", "optionally",
	"or", "order", "out", "outer", "outfile", "over", "overwrite", "pack_keys",
	"parser", "partition", "partitioning", "password", "percent_rank", "plugins",
	"point", "polygon", "precision", "primary", "privileges", "processlist",
	"procedure", "query", "quarter", "range", "rank", "read", "reads", "read_write",
	"real", "rebuild", "recursive", "redundant", "references", "regexp", "relay",
	"release", "remove", "rename", "reorganize", "repair", "repeat", "repeatable",
	"replace", "require", "resignal", "restrict", "return", "retry", "revert",
	"revoke", "right", "rlike", "rollback", "row", "row_format", "row_number",
	"rows", "s3", "savepoint", "schema", "schemas", "second", "second_microsecond",
	"security", "select", "sensitive", "separator", "sequence", "serializable",
	"session", "set", "share", "shared", "show", "signal", "signed", "slow",
	"smallint", "spatial", "specific", "sql", "sqlexception", "sqlstate",
	"sqlwarning", "sql_big_result", "sql_cache", "sql_calc_found_rows",
	"sql_no_cache", "sql_small_result", "ssl", "start", "starting",
	"stats_auto_recalc", "stats_persistent", "stats_sample_pages", "status",
	"storage", "stored", "straight_join", "stream", "system", "vstream",
	"table", "tables", "tablespace", "temporary", "temptable", "terminated",
	"text", "than", "then", "time", "timestamp", "timestampadd", "timestampdiff",
	"tinyblob", "tinyint", "tinytext", "to", "trailing", "transaction", "tree",
	"traditional", "trigger", "triggers", "true", "truncate", "uncommitted",
	"undefined", "undo", "union", "unique", "unlock", "unsigned", "update",
	"upgrade", "usage", "use", "user", "user_resources", "using", "utc_date",
	"utc_time", "utc_timestamp", "validation", "values", "variables", "varbinary",
	"varchar", "varcharacter", "varying", "vgtid_executed", "virtual", "vindex",
	"vindexes", "view", "vitess", "vitess_keyspaces", "vitess_metadata",
	"vitess_migration", "vitess_migrations", "vitess_replication_status",
	"vitess_shards", "vitess_tablets", "vschema", "warnings", "when",
	"where", "while", "window", "with", "without", "work", "write", "xor",
	"year", "year_month", "zerofill",
}

// Keywords that could get an additional keyword
var needCustomString = []string{
	"DISTINCTROW", "FROM", // Select keywords:
	"GROUP BY", "HAVING", "WINDOW",
	"FOR",
	"ORDER BY", "LIMIT",
	"INTO", "PARTITION", "AS", // Insert Keywords:
	"ON DUPLICATE KEY UPDATE",
	"WHERE", "LIMIT", // Delete keywords
	"INFILE", "INTO TABLE", "CHARACTER SET", // Load keywords
	"TERMINATED BY", "ENCLOSED BY",
	"ESCAPED BY", "STARTING BY",
	"TERMINATED BY", "STARTING BY",
	"IGNORE",
	"VALUE", "VALUES", // Replace tokens
	"SET",                                   // Update tokens
	"ENGINE =",                              // Drop tokens
	"DEFINER =", "ON SCHEDULE", "RENAME TO", // Alter tokens
	"COMMENT", "DO", "INITIAL_SIZE = ", "OPTIONS",
}

var alterTableTokens = [][]string{
	{"CUSTOM_FUZZ_STRING"},
	{"CUSTOM_ALTTER_TABLE_OPTIONS"},
	{"PARTITION_OPTIONS_FOR_ALTER_TABLE"},
}

var alterTokens = [][]string{
	{
		"DATABASE", "SCHEMA", "DEFINER = ", "EVENT", "FUNCTION", "INSTANCE",
		"LOGFILE GROUP", "PROCEDURE", "SERVER",
	},
	{"CUSTOM_FUZZ_STRING"},
	{
		"ON SCHEDULE", "ON COMPLETION PRESERVE", "ON COMPLETION NOT PRESERVE",
		"ADD UNDOFILE", "OPTIONS",
	},
	{"RENAME TO", "INITIAL_SIZE = "},
	{"ENABLE", "DISABLE", "DISABLE ON SLAVE", "ENGINE"},
	{"COMMENT"},
	{"DO"},
}

var setTokens = [][]string{
	{"CHARACTER SET", "CHARSET", "CUSTOM_FUZZ_STRING", "NAMES"},
	{"CUSTOM_FUZZ_STRING", "DEFAULT", "="},
	{"CUSTOM_FUZZ_STRING"},
}

var dropTokens = [][]string{
	{"TEMPORARY", "UNDO"},
	{
		"DATABASE", "SCHEMA", "EVENT", "INDEX", "LOGFILE GROUP",
		"PROCEDURE", "FUNCTION", "SERVER", "SPATIAL REFERENCE SYSTEM",
		"TABLE", "TABLESPACE", "TRIGGER", "VIEW",
	},
	{"IF EXISTS"},
	{"CUSTOM_FUZZ_STRING"},
	{"ON", "ENGINE = ", "RESTRICT", "CASCADE"},
}

var renameTokens = [][]string{
	{"TABLE"},
	{"CUSTOM_FUZZ_STRING"},
	{"TO"},
	{"CUSTOM_FUZZ_STRING"},
}

var truncateTokens = [][]string{
	{"TABLE"},
	{"CUSTOM_FUZZ_STRING"},
}

var createTokens = [][]string{
	{"OR REPLACE", "TEMPORARY", "UNDO"}, // For create spatial reference system
	{
		"UNIQUE", "FULLTEXT", "SPATIAL", "ALGORITHM = UNDEFINED", "ALGORITHM = MERGE",
		"ALGORITHM = TEMPTABLE",
	},
	{
		"DATABASE", "SCHEMA", "EVENT", "FUNCTION", "INDEX", "LOGFILE GROUP",
		"PROCEDURE", "SERVER", "SPATIAL REFERENCE SYSTEM", "TABLE", "TABLESPACE",
		"TRIGGER", "VIEW",
	},
	{"IF NOT EXISTS"},
	{"CUSTOM_FUZZ_STRING"},
}

/*
// For future use.
var updateTokens = [][]string{
	{"LOW_PRIORITY"},
	{"IGNORE"},
	{"SET"},
	{"WHERE"},
	{"ORDER BY"},
	{"LIMIT"},
}
*/

var replaceTokens = [][]string{
	{"LOW_PRIORITY", "DELAYED"},
	{"INTO"},
	{"PARTITION"},
	{"CUSTOM_FUZZ_STRING"},
	{"VALUES", "VALUE"},
}

var loadTokens = [][]string{
	{"DATA"},
	{"LOW_PRIORITY", "CONCURRENT", "LOCAL"},
	{"INFILE"},
	{"REPLACE", "IGNORE"},
	{"INTO TABLE"},
	{"PARTITION"},
	{"CHARACTER SET"},
	{"FIELDS", "COLUMNS"},
	{"TERMINATED BY"},
	{"OPTIONALLY"},
	{"ENCLOSED BY"},
	{"ESCAPED BY"},
	{"LINES"},
	{"STARTING BY"},
	{"TERMINATED BY"},
	{"IGNORE"},
	{"LINES", "ROWS"},
	{"CUSTOM_FUZZ_STRING"},
}

// These Are everything that comes after "INSERT"
var insertTokens = [][]string{
	{"LOW_PRIORITY", "DELAYED", "HIGH_PRIORITY", "IGNORE"},
	{"INTO"},
	{"PARTITION"},
	{"CUSTOM_FUZZ_STRING"},
	{"AS"},
	{"ON DUPLICATE KEY UPDATE"},
}

// These are everything that comes after "SELECT"
var selectTokens = [][]string{
	{"*", "CUSTOM_FUZZ_STRING", "DISTINCTROW"},
	{"HIGH_PRIORITY"},
	{"STRAIGHT_JOIN"},
	{"SQL_SMALL_RESULT", "SQL_BIG_RESULT", "SQL_BUFFER_RESULT"},
	{"SQL_NO_CACHE", "SQL_CALC_FOUND_ROWS"},
	{"CUSTOM_FUZZ_STRING"},
	{"FROM"},
	{"WHERE"},
	{"GROUP BY"},
	{"HAVING"},
	{"WINDOW"},
	{"ORDER BY"},
	{"LIMIT"},
	{"CUSTOM_FUZZ_STRING"},
	{"FOR"},
}

// These are everything that comes after "DELETE"
var deleteTokens = [][]string{
	{"LOW_PRIORITY", "QUICK", "IGNORE", "FROM", "AS"},
	{"PARTITION"},
	{"WHERE"},
	{"ORDER BY"},
	{"LIMIT"},
}

var alter_table_options = []string{
	"ADD", "COLUMN", "FIRST", "AFTER", "INDEX", "KEY", "FULLTEXT", "SPATIAL",
	"CONSTRAINT", "UNIQUE", "FOREIGN KEY", "CHECK", "ENFORCED", "DROP", "ALTER",
	"NOT", "INPLACE", "COPY", "SET", "VISIBLE", "INVISIBLE", "DEFAULT", "CHANGE",
	"CHARACTER SET", "COLLATE", "DISABLE", "ENABLE", "KEYS", "TABLESPACE", "LOCK",
	"FORCE", "MODIFY", "SHARED", "EXCLUSIVE", "NONE", "ORDER BY", "RENAME COLUMN",
	"AS", "=", "ASC", "DESC", "WITH", "WITHOUT", "VALIDATION", "ADD PARTITION",
	"DROP PARTITION", "DISCARD PARTITION", "IMPORT PARTITION", "TRUNCATE PARTITION",
	"COALESCE PARTITION", "REORGANIZE PARTITION", "EXCHANGE PARTITION",
	"ANALYZE PARTITION", "CHECK PARTITION", "OPTIMIZE PARTITION", "REBUILD PARTITION",
	"REPAIR PARTITION", "REMOVE PARTITIONING", "USING", "BTREE", "HASH", "COMMENT",
	"KEY_BLOCK_SIZE", "WITH PARSER", "AUTOEXTEND_SIZE", "AUTO_INCREMENT", "AVG_ROW_LENGTH",
	"CHECKSUM", "INSERT_METHOD", "ROW_FORMAT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT",
	"COMPACT", "SECONDARY_ENGINE_ATTRIBUTE", "STATS_AUTO_RECALC", "STATS_PERSISTENT",
	"STATS_SAMPLE_PAGES", "ZLIB", "LZ4", "ENGINE_ATTRIBUTE", "KEY_BLOCK_SIZE", "MAX_ROWS",
	"MIN_ROWS", "PACK_KEYS", "PASSWORD", "COMPRESSION", "CONNECTION", "DIRECTORY",
	"DELAY_KEY_WRITE", "ENCRYPTION", "STORAGE", "DISK", "MEMORY", "UNION",
}

// Creates an 'alter table' statement. 'alter table' is an exception
// in that it has its own function. The majority of statements
// are created by 'createStmt()'.
func createAlterTableStmt(f *ConsumeFuzzer) (string, error) {
	maxArgs, err := f.GetInt()
	if err != nil {
		return "", err
	}
	maxArgs = maxArgs % 30
	if maxArgs == 0 {
		return "", fmt.Errorf("could not create alter table stmt")
	}

	var stmt strings.Builder
	stmt.WriteString("ALTER TABLE ")
	for i := 0; i < maxArgs; i++ {
		// Calculate if we get existing token or custom string
		tokenType, err := f.GetInt()
		if err != nil {
			return "", err
		}
		if tokenType%4 == 1 {
			customString, err := f.GetString()
			if err != nil {
				return "", err
			}
			stmt.WriteString(" " + customString)
		} else {
			tokenIndex, err := f.GetInt()
			if err != nil {
				return "", err
			}
			stmt.WriteString(" " + alter_table_options[tokenIndex%len(alter_table_options)])
		}
	}
	return stmt.String(), nil
}

func chooseToken(tokens []string, f *ConsumeFuzzer) (string, error) {
	index, err := f.GetInt()
	if err != nil {
		return "", err
	}
	var token strings.Builder
	token.WriteString(tokens[index%len(tokens)])
	if token.String() == "CUSTOM_FUZZ_STRING" {
		customFuzzString, err := f.GetString()
		if err != nil {
			return "", err
		}
		return customFuzzString, nil
	}

	// Check if token requires an argument
	if containsString(needCustomString, token.String()) {
		customFuzzString, err := f.GetString()
		if err != nil {
			return "", err
		}
		token.WriteString(" " + customFuzzString)
	}
	return token.String(), nil
}

var stmtTypes = map[string][][]string{
	"DELETE":      deleteTokens,
	"INSERT":      insertTokens,
	"SELECT":      selectTokens,
	"LOAD":        loadTokens,
	"REPLACE":     replaceTokens,
	"CREATE":      createTokens,
	"DROP":        dropTokens,
	"RENAME":      renameTokens,
	"TRUNCATE":    truncateTokens,
	"SET":         setTokens,
	"ALTER":       alterTokens,
	"ALTER TABLE": alterTableTokens, // ALTER TABLE has its own set of tokens
}

var stmtTypeEnum = map[int]string{
	0:  "DELETE",
	1:  "INSERT",
	2:  "SELECT",
	3:  "LOAD",
	4:  "REPLACE",
	5:  "CREATE",
	6:  "DROP",
	7:  "RENAME",
	8:  "TRUNCATE",
	9:  "SET",
	10: "ALTER",
	11: "ALTER TABLE",
}

func createStmt(f *ConsumeFuzzer) (string, error) {
	stmtIndex, err := f.GetInt()
	if err != nil {
		return "", err
	}
	stmtIndex = stmtIndex % len(stmtTypes)

	queryType := stmtTypeEnum[stmtIndex]
	tokens := stmtTypes[queryType]

	// We have custom creator for ALTER TABLE
	if queryType == "ALTER TABLE" {
		query, err := createAlterTableStmt(f)
		if err != nil {
			return "", err
		}
		return query, nil
	}

	// Here we are creating a query that is not
	// an 'alter table' query. For available
	// queries, see "stmtTypes"

	// First specify the first query keyword:
	var query strings.Builder
	query.WriteString(queryType)

	// Next create the args for the
	queryArgs, err := createStmtArgs(tokens, f)
	if err != nil {
		return "", err
	}
	query.WriteString(" " + queryArgs)
	return query.String(), nil
}

// Creates the arguments of a statements. In a select statement
// that would be everything after "select".
func createStmtArgs(tokenslice [][]string, f *ConsumeFuzzer) (string, error) {
	var query, token strings.Builder

	// We go through the tokens in the tokenslice,
	// create the respective token and add it to
	// "query"
	for _, tokens := range tokenslice {
		// For extra randomization, the fuzzer can
		// choose to not include this token.
		includeThisToken, err := f.GetBool()
		if err != nil {
			return "", err
		}
		if !includeThisToken {
			continue
		}

		// There may be several tokens to choose from:
		if len(tokens) > 1 {
			chosenToken, err := chooseToken(tokens, f)
			if err != nil {
				return "", err
			}
			query.WriteString(" " + chosenToken)
		} else {
			token.WriteString(tokens[0])

			// In case the token is "CUSTOM_FUZZ_STRING"
			// we will then create a non-structured string
			if token.String() == "CUSTOM_FUZZ_STRING" {
				customFuzzString, err := f.GetString()
				if err != nil {
					return "", err
				}
				query.WriteString(" " + customFuzzString)
				continue
			}

			// Check if token requires an argument.
			// Tokens that take an argument can be found
			// in 'needCustomString'. If so, we add a
			// non-structured string to the token.
			if containsString(needCustomString, token.String()) {
				customFuzzString, err := f.GetString()
				if err != nil {
					return "", err
				}
				token.WriteString(fmt.Sprintf(" %s", customFuzzString))
			}
			query.WriteString(fmt.Sprintf(" %s", token.String()))
		}
	}
	return query.String(), nil
}

// Creates a semi-structured query. It creates a string
// that is a combination of the keywords and random strings.
func createQuery(f *ConsumeFuzzer) (string, error) {
	queryLen, err := f.GetInt()
	if err != nil {
		return "", err
	}
	maxLen := queryLen % 60
	if maxLen == 0 {
		return "", fmt.Errorf("could not create a query")
	}
	var query strings.Builder
	for i := 0; i < maxLen; i++ {
		// Get a new token:
		useKeyword, err := f.GetBool()
		if err != nil {
			return "", err
		}
		if useKeyword {
			keyword, err := getKeyword(f)
			if err != nil {
				return "", err
			}
			query.WriteString(" " + keyword)
		} else {
			customString, err := f.GetString()
			if err != nil {
				return "", err
			}
			query.WriteString(" " + customString)
		}
	}
	if query.String() == "" {
		return "", fmt.Errorf("could not create a query")
	}
	return query.String(), nil
}

// GetSQLString is the API that users interact with.
//
// Usage:
//
//	f := NewConsumer(data)
//	sqlString, err := f.GetSQLString()
func (f *ConsumeFuzzer) GetSQLString() (string, error) {
	var query string
	veryStructured, err := f.GetBool()
	if err != nil {
		return "", err
	}
	if veryStructured {
		query, err = createStmt(f)
		if err != nil {
			return "", err
		}
	} else {
		query, err = createQuery(f)
		if err != nil {
			return "", err
		}
	}
	return query, nil
}
