package sqlbuilder

import (
	"fmt"
	"math/rand"
	"reflect"
	"slices"
	"sync"
	"testing"
)

type fuzzState struct {
	data                    []byte
	dataIndex               int
	callchainRepresentation string
	currentBuilder          reflect.Value
	usedMethods             map[string]bool
}

func (fs *fuzzState) consumeData(size int) []byte {
	if len(fs.data) <= fs.dataIndex+size {
		return []byte{}
	}
	result := make([]byte, size)
	copy(result, fs.data[fs.dataIndex:fs.dataIndex+size])
	fs.dataIndex += size
	return result
}

func (fs *fuzzState) updateCallchain(method string, args []reflect.Value) {
	fs.callchainRepresentation += "." + method + "("
	for i, arg := range args {
		if i > 0 {
			fs.callchainRepresentation += ", "
		}
		fs.callchainRepresentation += fmt.Sprintf("%q", arg)
	}
	fs.callchainRepresentation += ")"
}

func getSelectBuilderMethods() (map[string]reflect.Type, []string) {
	sb := NewSelectBuilder()
	sbType := reflect.TypeOf(sb)
	// Skip methods that are likely to cause issues or don't return builders
	skipMethods := []string{
		"Build", "String", "BuildWithFlavor", "Flavor",
		"NumCol", "NumValue", "NumAssignment", "TableNames", "Var",
	}

	methodList := make(map[string]reflect.Type)
	methodNames := make([]string, 0, sbType.NumMethod())

	for i := 0; i < sbType.NumMethod(); i++ {
		method := sbType.Method(i)
		if slices.Contains(skipMethods, method.Name) {
			continue
		}

		methodList[method.Name] = method.Type
		methodNames = append(methodNames, method.Name)
	}

	return methodList, methodNames
}

func generateMethodArgs(methodType reflect.Type, state *fuzzState) ([]reflect.Value, bool) {
	numArgs := methodType.NumIn() - 1 // Skip receiver
	isVariadic := methodType.IsVariadic()

	if isVariadic {
		return generateVariadicArgs(methodType, numArgs, state)
	}
	return generateFixedArgs(methodType, numArgs, state)
}

func generateFixedArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) {
	args := make([]reflect.Value, numArgs)
	for i := 0; i < numArgs; i++ {
		argType := methodType.In(i + 1) // Skip receiver
		argData := state.consumeData(16)
		args[i] = generateArgumentForType(argType, argData)

		if !args[i].IsValid() {
			return nil, false
		}

		// Additional type compatibility check for complex types
		if argType.Kind() == reflect.Ptr && args[i].Kind() == reflect.Ptr {
			if argType != args[i].Type() {
				return nil, false
			}
		}
	}
	return args, true
}

func generateVariadicArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) {
	numFixedArgs := numArgs - 1 // Last parameter is the variadic slice

	// Generate fixed arguments first
	args := make([]reflect.Value, numFixedArgs)
	for i := 0; i < numFixedArgs; i++ {
		argType := methodType.In(i + 1) // Skip receiver
		argData := state.consumeData(16)
		args[i] = generateArgumentForType(argType, argData)
		if !args[i].IsValid() {
			return nil, false
		}
	}

	// Generate variadic arguments (0-3 arguments to keep it reasonable)
	if numFixedArgs < numArgs {
		variadicType := methodType.In(numArgs).Elem() // Get the element type of the slice
		numVariadicArgs := 0
		if len(state.data) > state.dataIndex {
			// 0-3 variadic args, keep the number small while still exercising multiple values
			// TODO: Possible fuzz improvement to allow for more variadic args. Not sure it's worth it.
			numVariadicArgs = int(state.data[state.dataIndex] % 4)
			state.dataIndex++
		}

		for j := 0; j < numVariadicArgs; j++ {
			argData := state.consumeData(16)
			varArg := generateArgumentForType(variadicType, argData)
			if !varArg.IsValid() {
				return nil, false
			}
			args = append(args, varArg)
		}
	}

	return args, true
}

func tryCallMethod(methodName string, methodType reflect.Type, state *fuzzState, t *testing.T) bool {
	// Check if method exists on current builder
	callableMethod := state.currentBuilder.MethodByName(methodName)
	if !callableMethod.IsValid() {
		return false
	}

	// Generate arguments
	args, canCall := generateMethodArgs(methodType, state)
	if !canCall {
		return false
	}

	// Update call chain representation and log it
	state.updateCallchain(methodName, args)
	t.Log("callchain:", state.callchainRepresentation)

	// Mark this method as used
	state.usedMethods[methodName] = true

	// Call method and capture result for chaining
	result := callableMethod.Call(args)

	// Only chain if method returns the same builder type (SelectBuilder)
	if len(result) > 0 && result[0].IsValid() {
		resultType := result[0].Type()
		if resultType.Kind() == reflect.Ptr &&
			resultType.String() == "*sqlbuilder.SelectBuilder" &&
			!result[0].IsNil() {
			state.currentBuilder = result[0]
		}
	}

	return true
}

func executeMethodChain(methodList map[string]reflect.Type, methodNames []string, state *fuzzState, maxChains uint8, t *testing.T) {
	for nbFunc := uint8(0); nbFunc < maxChains; nbFunc++ {
		methodCalled := false

		// Try to find a method we haven't used yet to create more varied chains
		for _, method := range methodNames {
			// Skip methods we've already used to create more variety
			if state.usedMethods[method] && len(state.usedMethods) < len(methodNames) {
				continue
			}

			if tryCallMethod(method, methodList[method], state, t) {
				methodCalled = true
				break // Move to next chain iteration
			}
		}

		// If no method could be called, break the chain
		if !methodCalled {
			break
		}
	}
}

func finalizeBuild(state *fuzzState) {
	// Always try to build the final SQL to ensure it doesn't panic
	if state.currentBuilder.IsValid() {
		buildMethod := state.currentBuilder.MethodByName("Build")
		if buildMethod.IsValid() {
			buildMethod.Call([]reflect.Value{})
		}
	}
}

func FuzzSelect(f *testing.F) {
	f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) {
		if len(data) == 0 {
			return
		}

		// Get all available methods for SelectBuilder
		methodList, methodNames := getSelectBuilderMethods()

		// Randomize method order deterministically based on seed
		r := rand.New(rand.NewSource(seed))
		r.Shuffle(len(methodNames), func(i, j int) {
			methodNames[i], methodNames[j] = methodNames[j], methodNames[i]
		})

		// Initialize fuzzing state
		state := &fuzzState{
			data:                    data,
			dataIndex:               0,
			callchainRepresentation: "NewSelectBuilder()",
			currentBuilder:          reflect.ValueOf(NewSelectBuilder()),
			usedMethods:             make(map[string]bool),
		}

		// Limit the number of chained functions to prevent infinite loops
		maxChains := numberOfChainedFunction
		if maxChains > 10 {
			maxChains = 10
		}

		// Execute method chain
		executeMethodChain(methodList, methodNames, state, maxChains, t)

		t.Logf("Final callchain: %s", state.callchainRepresentation)
		// Try to build the final result
		finalizeBuild(state)
	})
}

// generateArgumentForType generates a reflect.Value for the given type based on the provided data.
// It will consume the data slice to create a value of the specified type.
// It handles specific custom types like JoinOption and Flavor, and Go will consider them disntinct types than their aliases.
func generateArgumentForType(argType reflect.Type, data []byte) reflect.Value {
	switch argType.Kind() {
	case reflect.String:
		// Handle specific custom string types first
		if argType.String() == "sqlbuilder.JoinOption" {
			joinOptions := []JoinOption{
				FullJoin, FullOuterJoin, InnerJoin,
				LeftJoin, LeftOuterJoin, RightJoin, RightOuterJoin,
			}
			if len(data) > 0 {
				return reflect.ValueOf(joinOptions[int(data[0])%len(joinOptions)])
			}
			return reflect.ValueOf(InnerJoin)
		}
		// Use remaining data as string
		return reflect.ValueOf(string(data))
	case reflect.Int:
		// Handle specific custom int types first
		if argType.String() == "sqlbuilder.Flavor" {
			return reflect.ValueOf(DefaultFlavor)
		}
		if len(data) > 0 {
			return reflect.ValueOf(int(data[0]))
		}
		return reflect.ValueOf(0)
	case reflect.Bool:
		if len(data) > 0 {
			return reflect.ValueOf(data[0]%2 == 0)
		}
		return reflect.ValueOf(false)
	case reflect.Int8:
		if len(data) > 0 {
			return reflect.ValueOf(int8(data[0]))
		}
		return reflect.ValueOf(int8(0))
	case reflect.Int16:
		if len(data) >= 2 {
			return reflect.ValueOf(int16(data[0])<<8 | int16(data[1]))
		}
		return reflect.ValueOf(int16(0))
	case reflect.Int32:
		if len(data) >= 4 {
			return reflect.ValueOf(int32(data[0])<<24 | int32(data[1])<<16 | int32(data[2])<<8 | int32(data[3]))
		}
		return reflect.ValueOf(int32(0))
	case reflect.Int64:
		if len(data) >= 8 {
			val := int64(data[0])<<56 | int64(data[1])<<48 | int64(data[2])<<40 | int64(data[3])<<32 |
				int64(data[4])<<24 | int64(data[5])<<16 | int64(data[6])<<8 | int64(data[7])
			return reflect.ValueOf(val)
		}
		return reflect.ValueOf(int64(0))
	case reflect.Uint:
		if len(data) > 0 {
			return reflect.ValueOf(uint(data[0]))
		}
		return reflect.ValueOf(uint(0))
	case reflect.Uint8:
		if len(data) > 0 {
			return reflect.ValueOf(uint8(data[0]))
		}
		return reflect.ValueOf(uint8(0))
	case reflect.Uint16:
		if len(data) >= 2 {
			return reflect.ValueOf(uint16(data[0])<<8 | uint16(data[1]))
		}
		return reflect.ValueOf(uint16(0))
	case reflect.Uint32:
		if len(data) >= 4 {
			return reflect.ValueOf(uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]))
		}
		return reflect.ValueOf(uint32(0))
	case reflect.Uint64:
		if len(data) >= 8 {
			val := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 |
				uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7])
			return reflect.ValueOf(val)
		}
		return reflect.ValueOf(uint64(0))
	case reflect.Float32:
		if len(data) >= 4 {
			bits := uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3])
			return reflect.ValueOf(float32(bits))
		}
		return reflect.ValueOf(float32(0))
	case reflect.Float64:
		if len(data) >= 8 {
			bits := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 |
				uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7])
			return reflect.ValueOf(float64(bits))
		}
		return reflect.ValueOf(float64(0))
	case reflect.Slice:
		if argType.Elem().Kind() == reflect.String {
			return reflect.ValueOf([]string{string(data)})
		}
		if argType.Elem().Kind() == reflect.Interface {
			return reflect.ValueOf([]interface{}{string(data)})
		}
		return reflect.ValueOf([]interface{}{string(data)})
	case reflect.Ptr:
		// Handle pointer types by creating a pointer to the underlying type
		// Handle specific pointer types
		if argType == reflect.TypeOf((*WhereClause)(nil)) {
			return reflect.ValueOf(NewWhereClause())
		}
		if argType == reflect.TypeOf((*SelectBuilder)(nil)) {
			return reflect.ValueOf(NewSelectBuilder())
		}
		if argType == reflect.TypeOf((*Args)(nil)) {
			return reflect.ValueOf(&Args{})
		}
		if argType == reflect.TypeOf((*CTEBuilder)(nil)) {
			return reflect.ValueOf(DefaultFlavor.NewCTEBuilder())
		}
		if argType == reflect.TypeOf((*InsertBuilder)(nil)) {
			return reflect.ValueOf(DefaultFlavor.NewInsertBuilder())
		}
		if argType == reflect.TypeOf((*UpdateBuilder)(nil)) {
			return reflect.ValueOf(DefaultFlavor.NewUpdateBuilder())
		}
		if argType == reflect.TypeOf((*DeleteBuilder)(nil)) {
			return reflect.ValueOf(DefaultFlavor.NewDeleteBuilder())
		}
		// For other pointer types, create a pointer to the underlying type
		str := string(data)
		return reflect.ValueOf(&str)
	case reflect.Interface:
		// Handle specific interface types
		if argType.String() == "sqlbuilder.Builder" {
			// Create a simple SelectBuilder for Builder interface
			return reflect.ValueOf(NewSelectBuilder())
		}
		return reflect.ValueOf(string(data))
	default:
		// For other types, use zero value
		return reflect.Zero(argType)
	}
}

// FuzzSelectClone fuzzes SelectBuilder.Clone behavior under concurrent usage
// and ensures cloned instances are independent and safe to mutate.
func FuzzSelectClone(f *testing.F) {
	f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) {
		if len(data) == 0 {
			return
		}

		methodList, methodNames := getSelectBuilderMethods()

		r := rand.New(rand.NewSource(seed))
		r.Shuffle(len(methodNames), func(i, j int) {
			methodNames[i], methodNames[j] = methodNames[j], methodNames[i]
		})

		// Build a base template SelectBuilder via fuzzed method chains.
		base := NewSelectBuilder()
		baseState := &fuzzState{
			data:                    data,
			dataIndex:               0,
			callchainRepresentation: "NewSelectBuilder()",
			currentBuilder:          reflect.ValueOf(base),
			usedMethods:             make(map[string]bool),
		}

		maxChains := numberOfChainedFunction
		if maxChains > 10 {
			maxChains = 10
		}
		executeMethodChain(methodList, methodNames, baseState, maxChains, t)

		baseSQLBefore, baseArgsBefore := base.Build()

		// Clone concurrently and mutate clones with fuzzed chains.
		cloneCount := int(r.Uint32()%4) + 1 // 1..4 clones
		var wg sync.WaitGroup
		wg.Add(cloneCount)
		start := make(chan struct{})

		type result struct {
			sql  string
			args []interface{}
		}
		results := make(chan result, cloneCount)

		for i := 0; i < cloneCount; i++ {
			// Use different offsets into the same fuzz data for variety.
			offset := 0
			if len(data) > 0 {
				offset = (i * 17) % len(data)
			}
			go func(off int) {
				defer wg.Done()
				<-start // start all goroutines roughly at the same time

				c := base.Clone()
				st := &fuzzState{
					data:                    data,
					dataIndex:               off,
					callchainRepresentation: "Clone()",
					currentBuilder:          reflect.ValueOf(c),
					usedMethods:             make(map[string]bool),
				}
				executeMethodChain(methodList, methodNames, st, maxChains, t)
				finalizeBuild(st) // ensure no panic on Build
				s, a := c.Build()
				results <- result{sql: s, args: a}
			}(offset)
		}

		close(start)
		wg.Wait()
		close(results)

		// Ensure base builder stays unchanged after concurrent cloning/mutation of clones.
		baseSQLAfter, baseArgsAfter := base.Build()
		if baseSQLBefore != baseSQLAfter || !reflect.DeepEqual(baseArgsBefore, baseArgsAfter) {
			t.Fatalf("base builder mutated by clones:\n before: %s %v\n after: %s %v", baseSQLBefore, baseArgsBefore, baseSQLAfter, baseArgsAfter)
		}

		// Independence check: mutating one clone does not affect another clone.
		cloneA := base.Clone()
		sA1, aA1 := cloneA.Build()

		done := make(chan struct{})
		go func() {
			defer close(done)
			c2 := base.Clone()
			// Apply a deterministic small change; should not affect cloneA.
			c2.OrderBy("id").Desc().Limit(1).Offset(0)
			_, _ = c2.Build()
		}()

		sA2, aA2 := cloneA.Build()
		if sA1 != sA2 || !reflect.DeepEqual(aA1, aA2) {
			t.Fatalf("cloneA changed after mutating another clone")
		}
		<-done

		// Further independence: modifying cloneA should not affect the base.
		cloneA.Limit(3).Asc()
		_ = cloneA.String()
		baseSQLFinal, baseArgsFinal := base.Build()
		if baseSQLFinal != baseSQLAfter || !reflect.DeepEqual(baseArgsFinal, baseArgsAfter) {
			t.Fatalf("base changed after modifying a clone")
		}

		// Drain results to ensure all builds completed; mainly to use the values and avoid lints.
		for range results {
		}
	})
}
