/*
Copyright 2024 The Kubernetes 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 main

import (
	"bytes"
	"cmp"
	"fmt"
	"io"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"unicode"

	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/code-generator/cmd/validation-gen/validators"
	"k8s.io/gengo/v2/generator"
	"k8s.io/gengo/v2/namer"
	"k8s.io/gengo/v2/parser/tags"
	"k8s.io/gengo/v2/types"
	"k8s.io/klog/v2"
)

func mkPkgNames(pkg string, names ...string) []types.Name {
	result := make([]types.Name, 0, len(names))
	for _, name := range names {
		result = append(result, types.Name{Package: pkg, Name: name})
	}
	return result
}

var (
	fieldPkg            = "k8s.io/apimachinery/pkg/util/validation/field"
	fieldPkgSymbols     = mkPkgNames(fieldPkg, "ErrorList", "InternalError", "Path")
	fmtPkgSymbols       = mkPkgNames("fmt", "Errorf")
	safePkg             = "k8s.io/apimachinery/pkg/api/safe"
	safePkgSymbols      = mkPkgNames(safePkg, "Field", "Cast")
	operationPkg        = "k8s.io/apimachinery/pkg/api/operation"
	operationPkgSymbols = mkPkgNames(operationPkg, "Operation")
	contextPkg          = "context"
	contextPkgSymbols   = mkPkgNames(contextPkg, "Context")
)

// genValidations produces a file with autogenerated validations.
type genValidations struct {
	generator.GoGenerator
	outputPackage  string
	inputToPkg     map[string]string // Maps input packages to generated validation packages
	rootTypes      []*types.Type
	discovered     *typeDiscoverer
	imports        namer.ImportTracker
	schemeRegistry types.Name
}

// NewGenValidations cretes a new generator for the specified package.
func NewGenValidations(outputFilename, outputPackage string, rootTypes []*types.Type, discovered *typeDiscoverer, inputToPkg map[string]string, schemeRegistry types.Name) generator.Generator {
	return &genValidations{
		GoGenerator: generator.GoGenerator{
			OutputFilename: outputFilename,
		},
		outputPackage:  outputPackage,
		inputToPkg:     inputToPkg,
		rootTypes:      rootTypes,
		discovered:     discovered,
		imports:        generator.NewImportTrackerForPackage(outputPackage),
		schemeRegistry: schemeRegistry,
	}
}

func (g *genValidations) Namers(_ *generator.Context) namer.NameSystems {
	// Have the raw namer for this file track what it imports.
	return namer.NameSystems{
		"raw": namer.NewRawNamer(g.outputPackage, g.imports),
	}
}

func (g *genValidations) Filter(_ *generator.Context, t *types.Type) bool {
	// We want to emit code for all root types.
	for _, rt := range g.rootTypes {
		if rt == t {
			return true
		}
	}
	// We want to emit for any other type that is transitively part of a root
	// type and has validations.
	n := g.discovered.typeNodes[t]
	return n != nil && hasValidations(n)
}

func (g *genValidations) Imports(_ *generator.Context) (imports []string) {
	var importLines []string
	for _, singleImport := range g.imports.ImportLines() {
		if g.isOtherPackage(singleImport) {
			importLines = append(importLines, singleImport)
		}
	}
	return importLines
}

func (g *genValidations) isOtherPackage(pkg string) bool {
	if pkg == g.outputPackage {
		return false
	}
	if strings.HasSuffix(pkg, `"`+g.outputPackage+`"`) {
		return false
	}
	return true
}

func (g *genValidations) Init(c *generator.Context, w io.Writer) error {
	klog.V(5).Infof("emitting registration code")
	sw := generator.NewSnippetWriter(w, c, "$", "$")
	g.emitRegisterFunction(c, g.schemeRegistry, sw)
	if err := sw.Error(); err != nil {
		return err
	}
	return nil
}

func (g *genValidations) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
	klog.V(5).Infof("emitting validation code for type %v", t)

	sw := generator.NewSnippetWriter(w, c, "$", "$")
	g.emitValidationVariables(c, t, sw)
	g.emitValidationFunction(c, t, sw)
	if err := sw.Error(); err != nil {
		return err
	}
	return nil
}

// This is a global cache of whether a type has validations.
var hasValidationsCache = map[*typeNode]bool{}

// hasValidations checks and caches whether the given typeNode has any
// validations, transitively.  Callers must be SURE that the typeNode has
// already been full discovered, or this may return wrong answers.  It should
// be totally safe in the generation phase, but discovery needs to be careful!
func hasValidations(n *typeNode) bool {
	seen := map[*typeNode]bool{}
	return hasValidationsImpl(n, seen)
}

// hasValidationsImpl implements hasValidations without risk of infinite
// recursion.
func hasValidationsImpl(n *typeNode, seen map[*typeNode]bool) bool {
	if n == nil {
		return false
	}

	if seen[n] {
		return false // prevent infinite recursion
	}
	seen[n] = true

	if r, found := hasValidationsCache[n]; found {
		return r
	}

	r := hasValidationsMiss(n, seen)
	hasValidationsCache[n] = r
	return r
}

// hasValidationsMiss is called in case of a cache miss.
func hasValidationsMiss(n *typeNode, seen map[*typeNode]bool) bool {
	if !n.typeValidations.Empty() {
		return true
	}
	allChildren := n.fields
	if n.key != nil {
		allChildren = append(allChildren, n.key)
	}
	if n.elem != nil {
		allChildren = append(allChildren, n.elem)
	}
	if n.underlying != nil {
		allChildren = append(allChildren, n.underlying)
	}
	for _, c := range allChildren {
		if !c.fieldValidations.Empty() {
			return true
		}
		if hasValidationsImpl(c.node, seen) {
			return true
		}
	}
	return false
}

// typeDiscoverer contains fields necessary to build graphs of types.
type typeDiscoverer struct {
	validator  validators.Validator
	inputToPkg map[string]string

	// typeNodes holds a map of gengo Type to typeNode for all of the types
	// encountered during discovery.
	typeNodes map[*types.Type]*typeNode
}

// NewTypeDiscoverer creates and initializes a NewTypeDiscoverer.
func NewTypeDiscoverer(validator validators.Validator, inputToPkg map[string]string) *typeDiscoverer {
	return &typeDiscoverer{
		validator:  validator,
		inputToPkg: inputToPkg,
		typeNodes:  map[*types.Type]*typeNode{},
	}
}

// childNode represents a type which is used in another type (e.g. a struct
// field).
type childNode struct {
	name      string      // the field name in the parent, populated when this node is a struct field
	jsonName  string      // always populated when name is populated
	childType *types.Type // the real type of the child (may be a pointer)
	node      *typeNode   // the node of the child's value type, or nil if it is in a foreign package

	fieldValidations validators.Validations // validations on the field
}

// typeNode represents a node in the type-graph, annotated with information
// about validations.  Everything in this type, transitively, is assoctiated
// with the type, and not any specific instance of that type (e.g. when used as
// a field in a struct.
type typeNode struct {
	valueType *types.Type // never a pointer, but may be a map, slice, struct, etc.
	funcName  types.Name  // populated when this type is has a validation function

	fields     []*childNode // populated when this type is a struct
	key        *childNode   // populated when this type is a map
	elem       *childNode   // populated when this type is a map or slice
	underlying *childNode   // populated when this type is an alias

	typeValidations validators.Validations // validations on the type
}

// lookupField returns the childNode with the specified JSON name.
func (n typeNode) lookupField(jsonName string) *childNode {
	for _, fld := range n.fields {
		if fld.jsonName == jsonName {
			return fld
		}
	}
	return nil
}

// DiscoverType walks the given type recursively, building a type-graph in this
// typeDiscoverer.  If this is called multiple times for different types, the
// graphs will be will be merged.
func (td *typeDiscoverer) DiscoverType(t *types.Type) error {
	if t.Kind == types.Pointer {
		return fmt.Errorf("type %v: pointer root-types are not supported", t)
	}
	fldPath := field.NewPath(t.Name.String())
	if node, err := td.discover(t, fldPath); err != nil {
		return err
	} else if node == nil {
		panic(fmt.Sprintf("discovered a nil node for type %v", t))
	}
	return nil
}

// unalias returns the unaliased type.
func unalias(t *types.Type) *types.Type {
	for t.Kind == types.Alias {
		t = t.Underlying
	}
	return t
}

// discover walks the given type recursively and returns a typeNode
// representing it. This does not distinguish between discovering a type
// definition and discovering a field of a struct.  The first time it
// encounters a type it has not seen before, it will discover that type. If it
// finds a type it has already processed, it will return the existing node.
func (td *typeDiscoverer) discover(t *types.Type, fldPath *field.Path) (*typeNode, error) {
	// With the exception of builtins (which gengo puts in package ""), we
	// can't traverse into packages which are not being processed by this tool.
	if t.Name.Package != "" {
		_, ok := td.inputToPkg[t.Name.Package]
		if !ok {
			return nil, nil
		}
	}

	// Catch some edge cases that we don't want to handle (yet?).  This happens
	// as early as possible to make all the other code simpler.
	switch t.Kind {
	case types.Builtin, types.Struct:
		// Allowed
	case types.Interface:
		// We can't do much with interfaces, but they pop up in some places
		// like RawExtension.
	case types.Alias:
		if t.Underlying.Kind == types.Pointer {
			return nil, fmt.Errorf("field %s (%s): typedefs to pointers are not supported", fldPath.String(), t)
		}
	case types.Pointer:
		pointee := unalias(t.Elem)
		switch pointee.Kind {
		case types.Pointer:
			return nil, fmt.Errorf("field %s (%s): pointers to pointers are not supported", fldPath.String(), t)
		case types.Slice, types.Array:
			return nil, fmt.Errorf("field %s (%s): pointers to lists are not supported", fldPath.String(), t)
		case types.Map:
			return nil, fmt.Errorf("field %s (%s): pointers to maps are not supported", fldPath.String(), t)
		}
	case types.Array:
		return nil, fmt.Errorf("field %s (%s): fixed-size arrays are not supported", fldPath.String(), t)
	case types.Slice:
		elem := unalias(t.Elem)
		switch elem.Kind {
		case types.Pointer:
			return nil, fmt.Errorf("field %s (%s): lists of pointers are not supported", fldPath.String(), t)
		case types.Slice:
			if unalias(elem.Elem) != types.Byte {
				return nil, fmt.Errorf("field %s (%s): lists of lists are not supported", fldPath.String(), t)
			}
		case types.Map:
			return nil, fmt.Errorf("field %s (%s): lists of maps are not supported", fldPath.String(), t)
		}
	case types.Map:
		key := unalias(t.Key)
		if key != types.String {
			return nil, fmt.Errorf("field %s (%s): maps with non-string keys are not supported", fldPath.String(), t)
		}
		elem := unalias(t.Elem)
		switch elem.Kind {
		case types.Pointer:
			return nil, fmt.Errorf("field %s (%s): maps of pointers are not supported", fldPath.String(), t)
		case types.Slice:
			if unalias(elem.Elem) != types.Byte {
				return nil, fmt.Errorf("field %s (%s): maps of lists are not supported", fldPath.String(), t)
			}
		case types.Map:
			return nil, fmt.Errorf("field %s (%s): maps of maps are not supported", fldPath.String(), t)
		}
	default:
		return nil, fmt.Errorf("field %s (%v, kind %v) is not supported", fldPath.String(), t, t.Kind)
	}

	// Discovery applies to values, not pointers.
	if t.Kind == types.Pointer {
		return td.discover(t.Elem, fldPath)
	}

	// If we have done this type already, we can stop here and break any
	// recursion.
	if node, found := td.typeNodes[t]; found {
		return node, nil
	}

	// If we are descending into a named type, reboot the field path for better
	// logging.  Otherwise the field path might come in as something like
	// <type1>.<field1>.<field2> which is true, but not super useful.
	switch t.Kind {
	case types.Alias, types.Struct:
		fldPath = field.NewPath(t.Name.String())
	}

	// This is the type-node being assembled in the rest of this function.
	thisNode := &typeNode{
		valueType: t,
	}
	td.typeNodes[t] = thisNode

	// If this is a known, named type, we can call its validation function.
	switch t.Kind {
	case types.Alias, types.Struct:
		if fn, ok := td.getValidationFunctionName(t); ok {
			thisNode.funcName = fn
		}
	}

	// Discover into this type before extracting type validations.
	switch t.Kind {
	case types.Builtin, types.Interface:
		// Nothing more to do.
	case types.Alias:
		// Discover the underlying type.
		//
		// Note: By the language definition, what gengo calls "Aliases" (really
		// just "type definitions") have underlying types of the type literal.
		// In other words, if we define `type T1 string` and `type T2 T1`, the
		// underlying type of T2 is string, not T1.  This means that:
		//    1) We will emit code for both underlying types. If the underlying
		//       type is a struct with many fields, we will emit two identical
		//       functions.
		//    2) Validating a field of type T2 will NOT call any validation
		//       defined on the type T1.
		//    3) In the case of a type definition whose RHS is a struct which
		//       has fields with validation tags, the validation for those fields
		//       WILL be called from the generated for for the new type.
		if node, err := td.discover(t.Underlying, fldPath); err != nil {
			return nil, err
		} else {
			thisNode.underlying = &childNode{
				childType: t.Underlying,
				node:      node,
			}
		}
	case types.Struct:
		// Discover into this struct, recursively.
		if err := td.discoverStruct(thisNode, fldPath); err != nil {
			return nil, err
		}
	case types.Slice:
		// Discover the element type.
		if node, err := td.discover(t.Elem, fldPath.Key("vals")); err != nil {
			return nil, err
		} else {
			thisNode.elem = &childNode{
				childType: t.Elem,
				node:      node,
			}
		}
	case types.Map:
		// Discover the key type.
		if node, err := td.discover(t.Key, fldPath.Key("keys")); err != nil {
			return nil, err
		} else {
			thisNode.key = &childNode{
				childType: t.Key,
				node:      node,
			}
		}

		// Discover the element type.
		if node, err := td.discover(t.Elem, fldPath.Key("vals")); err != nil {
			return nil, err
		} else {
			thisNode.elem = &childNode{
				childType: t.Elem,
				node:      node,
			}
		}
	}

	// Extract any type-attached validation rules.  We do this AFTER descending
	// into the type, so that these validators have access to the full type.
	// For example, all struct field validators get called before the type
	// validators.  This does not influence the order in which the validations
	// are called in emitted code, just how we evaluate what to emit.
	//
	// This should only ever hit Struct and Alias types, since that is the only
	// opportunity to have type-attached comments to process.
	context := validators.Context{
		Scope: validators.ScopeType,
		Type:  t,
		Path:  fldPath,
	}
	if t.Kind == types.Alias {
		context.Parent = t
		context.Type = t.Underlying
	}
	if validations, err := td.validator.ExtractValidations(context, t.CommentLines); err != nil {
		return nil, fmt.Errorf("%v: %w", fldPath, err)
	} else if !validations.Empty() {
		klog.V(5).InfoS("found type-attached validations", "n", validations.Len())
		thisNode.typeValidations.Add(validations)
	}

	// Handle type definitions whose output depends on the rest of type
	// discovery being complete. In particular, aliases to lists and maps need
	// iteration, but we don't want to iterate them if the key or value types
	// don't actually have validations. We also want to handle non-included
	// types and make users tell us what they intended. Lastly, we want to
	// handle recursive types, but we need to finish discovering the type
	// before we know if there are other validations, again so we don't emit
	// empty functions.
	if t.Kind == types.Alias {
		underlying := thisNode.underlying

		switch t.Underlying.Kind {
		case types.Slice:
			// Validate each value.
			if elemNode := underlying.node.elem.node; elemNode == nil {
				if !thisNode.typeValidations.OpaqueValType {
					return nil, fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						fldPath, underlying.node.elem.childType)
				}
			} else if thisNode.typeValidations.OpaqueValType {
				// If the type is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// We only need the iteration function if the underlying
					// type has validations, otherwise it is noise.
					if hasValidations(underlying.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(fldPath, underlying.childType,
							validators.Function("iterateListValues", validators.DefaultFlags, funcName))
						if err != nil {
							return nil, fmt.Errorf("generating list iteration: %w", err)
						} else {
							thisNode.typeValidations.Add(v)
						}
					}
				}
			}
		case types.Map:
			// Validate each key.
			if keyNode := underlying.node.key.node; keyNode == nil {
				if !thisNode.typeValidations.OpaqueKeyType {
					return nil, fmt.Errorf("%v: key type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachKey=+k8s:opaqueType to the field to skip validation",
						fldPath, underlying.node.elem.childType)
				}
			} else if thisNode.typeValidations.OpaqueKeyType {
				// If the type is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the key type is a named type, call the validation
				// function for each key.
				if funcName := keyNode.funcName; funcName.Name != "" {
					// We only need the iteration function if the underlying
					// type has validations, otherwise it is noise.
					if hasValidations(underlying.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachKey(fldPath, underlying.childType,
							validators.Function("iterateMapKeys", validators.DefaultFlags, funcName))
						if err != nil {
							return nil, fmt.Errorf("generating map key iteration: %w", err)
						} else {
							thisNode.typeValidations.Add(v)
						}
					}
				}
			}
			// Validate each value.
			if elemNode := underlying.node.elem.node; elemNode == nil {
				if !thisNode.typeValidations.OpaqueValType {
					return nil, fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						fldPath, underlying.node.elem.childType)
				}
			} else if thisNode.typeValidations.OpaqueValType {
				// If the type is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// We only need the iteration function if the underlying
					// type has validations, otherwise it is noise.
					if hasValidations(underlying.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(fldPath, underlying.childType,
							validators.Function("iterateMapValues", validators.DefaultFlags, funcName))
						if err != nil {
							return nil, fmt.Errorf("generating map value iteration: %w", err)
						} else {
							thisNode.typeValidations.Add(v)
						}
					}
				}
			}
		}
	}

	return thisNode, nil
}

// discoverStruct walks a struct type recursively.
func (td *typeDiscoverer) discoverStruct(thisNode *typeNode, fldPath *field.Path) error {
	var fields []*childNode

	// Discover into each field of this struct.
	for _, memb := range thisNode.valueType.Members {
		name := memb.Name
		if len(name) == 0 { // embedded fields
			if memb.Type.Kind == types.Pointer {
				name = memb.Type.Elem.Name.Name
			} else {
				name = memb.Type.Name.Name
			}
		}
		// Only do exported fields.
		if unicode.IsLower([]rune(name)[0]) {
			continue
		}

		// If we try to emit code for this field and find no JSON name, we
		// will abort.
		jsonName := ""
		if commentTags, ok := tags.LookupJSON(memb); ok {
			jsonName = commentTags.Name
		}

		klog.V(5).InfoS("field", "name", name, "jsonName", jsonName, "type", memb.Type)

		// Discover the field type.
		childPath := fldPath.Child(name)
		childType := memb.Type
		var child *childNode
		if node, err := td.discover(childType, childPath); err != nil {
			return err
		} else {
			child = &childNode{
				name:      name,
				jsonName:  jsonName,
				childType: childType,
				node:      node,
			}
		}

		// Extract any field-attached validation rules.
		context := validators.Context{
			Scope:  validators.ScopeField,
			Type:   childType,
			Parent: thisNode.valueType,
			Member: &memb,
			Path:   childPath,
		}
		if validations, err := td.validator.ExtractValidations(context, memb.CommentLines); err != nil {
			return fmt.Errorf("field %s: %w", childPath.String(), err)
		} else if !validations.Empty() {
			klog.V(5).InfoS("found field-attached validations", "n", validations.Len())
			child.fieldValidations.Add(validations)
			if len(validations.Variables) > 0 {
				return fmt.Errorf("%v: variable generation is not supported for field validations", childPath)
			}
		}

		// Handle non-included types.
		switch nonPtrType(childType).Kind {
		case types.Struct, types.Alias:
			if child.node == nil { // a non-included type
				if !child.fieldValidations.OpaqueType {
					return fmt.Errorf("%v: type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:opaqueType to the field to skip validation",
						childPath, childType.String())
				}
			} else if child.fieldValidations.OpaqueType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
				child.node = nil
			}
		}

		// Add any other field-attached "special" validators. We need to do
		// this after all the other field validation has been processed,
		// because some of this is conditional on whether other validations
		// were emitted (to avoid emitting empty functions).
		//
		// We do this here, rather than in discover() because we need to know
		// information about the field, not just the type.
		switch childType.Kind {
		case types.Slice:
			// Validate each value of a list field.
			if elemNode := child.node.elem.node; elemNode == nil {
				if !child.fieldValidations.OpaqueValType {
					return fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Elem.String())
				}
			} else if child.fieldValidations.OpaqueValType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the list's value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// We only emit the iteration function if the field
					// has other validations, otherwise it is noise.
					if hasValidations(child.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(childPath, childType,
							validators.Function("iterateListValues", validators.DefaultFlags, funcName))
						if err != nil {
							return fmt.Errorf("generating list iteration: %w", err)
						} else {
							child.fieldValidations.Add(v)
						}
					}
				}
			}
		case types.Map:
			// Validate each key of a map field.
			if keyNode := child.node.key.node; keyNode == nil {
				if !child.fieldValidations.OpaqueKeyType {
					return fmt.Errorf("%v: key type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachKey=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Key.String())
				}
			} else if child.fieldValidations.OpaqueKeyType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the map's key type is a named type, call the validation
				// function for each key.
				if funcName := keyNode.funcName; funcName.Name != "" {
					// We only emit the iteration function if the field
					// has other validations, otherwise it is noise.
					if hasValidations(child.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachKey(childPath, childType,
							validators.Function("iterateMapKeys", validators.DefaultFlags, funcName))
						if err != nil {
							return fmt.Errorf("generating map key iteration: %w", err)
						} else {
							child.fieldValidations.Add(v)
						}
					}
				}
			}
			// Validate each value of a map field.
			if elemNode := child.node.elem.node; elemNode == nil {
				if !child.fieldValidations.OpaqueValType {
					return fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Elem.String())
				}
			} else if child.fieldValidations.OpaqueValType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the map's value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// We only emit the iteration function if the field
					// has other validations, otherwise it is noise.
					if hasValidations(child.node) {
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(childPath, childType,
							validators.Function("iterateMapValues", validators.DefaultFlags, funcName))
						if err != nil {
							return fmt.Errorf("generating map value iteration: %w", err)
						} else {
							child.fieldValidations.Add(v)
						}
					}
				}
			}
		}

		fields = append(fields, child)
	}

	thisNode.fields = fields
	return nil
}

// nonPtrType removes any pointerness from the type.
func nonPtrType(t *types.Type) *types.Type {
	for t.Kind == types.Pointer {
		t = t.Elem
	}
	return t
}

// getValidationFunctionName looks up the name of the specified type's
// validation function.
//
// TODO: Currently this is a "blind" call - we hope that the expected function
// exists, but we don't verify that, and we only emit calls into packages which
// are being processed by this generator. For cross-package calls we will need
// to verify the target, either by naming convention + fingerprint or by
// explicit comment-tags or something.
func (td *typeDiscoverer) getValidationFunctionName(t *types.Type) (types.Name, bool) {
	pkg, ok := td.inputToPkg[t.Name.Package]
	if !ok {
		return types.Name{}, false
	}
	return types.Name{Package: pkg, Name: "Validate_" + t.Name.Name}, true
}

func mkSymbolArgs(c *generator.Context, names []types.Name) generator.Args {
	args := generator.Args{}
	for _, name := range names {
		args[name.Name] = c.Universe.Type(name)
	}
	return args
}

// emitRegisterFunction emits the type-registration logic for validation
// functions.
func (g *genValidations) emitRegisterFunction(c *generator.Context, schemeRegistry types.Name, sw *generator.SnippetWriter) {
	scheme := c.Universe.Type(schemeRegistry)
	schemePtr := &types.Type{
		Kind: types.Pointer,
		Elem: scheme,
	}

	sw.Do("func init() { localSchemeBuilder.Register(RegisterValidations)}\n\n", nil)

	sw.Do("// RegisterValidations adds validation functions to the given scheme.\n", nil)
	sw.Do("// Public to allow building arbitrary schemes.\n", nil)
	sw.Do("func RegisterValidations(scheme $.|raw$) error {\n", schemePtr)
	for _, rootType := range g.rootTypes {
		if !hasValidations(g.discovered.typeNodes[rootType]) {
			continue
		}

		node := g.discovered.typeNodes[rootType]
		if node == nil {
			panic(fmt.Sprintf("found nil node for root-type %v", rootType))
		}

		// TODO: It would be nice if these were not hard-coded.
		var statusType *types.Type
		var statusField string
		if status := node.lookupField("status"); status != nil {
			statusType = status.node.valueType
			statusField = status.name
		}

		targs := generator.Args{
			"rootType":    rootType,
			"typePfx":     "",
			"statusType":  statusType,
			"statusField": statusField,
			"field":       mkSymbolArgs(c, fieldPkgSymbols),
			"fmt":         mkSymbolArgs(c, fmtPkgSymbols),
			"operation":   mkSymbolArgs(c, operationPkgSymbols),
			"safe":        mkSymbolArgs(c, safePkgSymbols),
			"context":     mkSymbolArgs(c, contextPkgSymbols),
		}
		if !isNilableType(rootType) {
			targs["typePfx"] = "*"
		}

		// This uses a typed nil pointer, rather than a real instance because
		// we need the type information, but not an instance of the type.
		sw.Do("scheme.AddValidationFunc(", targs)
		sw.Do("    ($.typePfx$$.rootType|raw$)(nil), ", targs)
		sw.Do("    func(ctx $.context.Context$, op $.operation.Operation|raw$, obj, oldObj interface{}, ", targs)
		sw.Do("    subresources ...string) $.field.ErrorList|raw$ {\n", targs)
		sw.Do("  if len(subresources) == 0 {\n", targs)
		sw.Do("    return $.rootType|objectvalidationfn$(", targs)
		sw.Do("               ctx, ", targs)
		sw.Do("               op, ", targs)
		sw.Do("               nil /* fldPath */, ", targs)
		sw.Do("               obj.($.typePfx$$.rootType|raw$), ", targs)
		sw.Do("               $.safe.Cast|raw$[$.typePfx$$.rootType|raw$](oldObj))\n", targs)
		sw.Do("  }\n", targs)

		if statusType != nil {
			targs["statusTypePfx"] = ""
			targs["statusTypePtrPfx"] = ""
			if !isNilableType(statusType) {
				targs["statusTypePfx"] = "*"
				targs["statusTypePtrPfx"] = "&"
			}
			sw.Do("  if len(subresources) == 1 && subresources[0] == \"status\" {\n", targs)
			if hasValidations(g.discovered.typeNodes[statusType]) {
				sw.Do("    root := obj.($.typePfx$$.rootType|raw$)\n", targs)
				sw.Do("    return $.statusType|objectvalidationfn$(", targs)
				sw.Do("               ctx, ", targs)
				sw.Do("               op, ", targs)
				sw.Do("               nil /* fldPath */, ", targs)
				sw.Do("               &root.$.statusField$, ", targs)
				sw.Do("               $.safe.Field|raw$(", targs)
				sw.Do("                   $.safe.Cast|raw$[$.typePfx$$.rootType|raw$](oldObj), ", targs)
				sw.Do("                   func(oldObj $.typePfx$$.rootType|raw$) $.statusTypePfx$$.statusType|raw$ { ", targs)
				sw.Do("                       return $.statusTypePtrPfx$oldObj.$.statusField$ ", targs)
				sw.Do("                   }))\n", targs)
			} else {
				sw.Do("    return nil // $.statusType|raw$ has no validation\n", targs)
			}
			sw.Do("  }\n", targs)
		}
		sw.Do("  return $.field.ErrorList|raw${", targs)
		sw.Do("      $.field.InternalError|raw$(", targs)
		sw.Do("          nil, ", targs)
		sw.Do("          $.fmt.Errorf|raw$(\"no validation found for %T, subresources: %v\", obj, subresources))", targs)
		sw.Do("  }\n", targs)
		sw.Do("})\n", targs)
	}
	sw.Do("return nil\n", nil)
	sw.Do("}\n\n", nil)
}

// emitValidationFunction emits a validation function for the specified type.
func (g *genValidations) emitValidationFunction(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) {
	if !hasValidations(g.discovered.typeNodes[t]) {
		return
	}

	targs := generator.Args{
		"inType":     t,
		"field":      mkSymbolArgs(c, fieldPkgSymbols),
		"operation":  mkSymbolArgs(c, operationPkgSymbols),
		"context":    mkSymbolArgs(c, contextPkgSymbols),
		"objTypePfx": "*",
	}
	if isNilableType(t) {
		targs["objTypePfx"] = ""
	}

	node := g.discovered.typeNodes[t]
	if node == nil {
		panic(fmt.Sprintf("found nil node for root-type %v", t))
	}
	sw.Do("func $.inType|objectvalidationfn$(", targs)
	sw.Do("    ctx $.context.Context|raw$, ", targs)
	sw.Do("    op $.operation.Operation|raw$, ", targs)
	sw.Do("    fldPath *$.field.Path|raw$, ", targs)
	sw.Do("    obj, oldObj $.objTypePfx$$.inType|raw$) ", targs)
	sw.Do("(errs $.field.ErrorList|raw$) {\n", targs)
	fakeChild := &childNode{
		node:      node,
		childType: t,
	}
	g.emitValidationForChild(c, fakeChild, sw)
	sw.Do("return errs\n", nil)
	sw.Do("}\n\n", nil)
}

// emitValidationForChild emits code for the specified childNode, calling
// type-attached validations and then descending into the type (e.g. struct
// fields).
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
//
// This function assumes that thisChild.node is not nil.
func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild *childNode, sw *generator.SnippetWriter) {
	thisNode := thisChild.node
	inType := thisNode.valueType

	targs := generator.Args{
		"inType": inType,
		"field":  mkSymbolArgs(c, fieldPkgSymbols),
		"safe":   mkSymbolArgs(c, safePkgSymbols),
	}

	didSome := false // for prettier output later

	// Emit code for type-attached validations.
	if validations := thisNode.typeValidations; !validations.Empty() {
		switch thisNode.valueType.Kind {
		case types.Struct, types.Alias: // OK
		default:
			panic(fmt.Sprintf("unexpected type-validations on type %v, kind %s", thisNode.valueType, thisNode.valueType.Kind))
		}
		sw.Do("// type $.inType|raw$\n", targs)
		emitComments(validations.Comments, sw)
		emitCallsToValidators(c, validations.Functions, sw)
		sw.Do("\n", nil)
		didSome = true
	}

	// Descend into the type.
	switch inType.Kind {
	case types.Builtin:
		// Nothing further.
	case types.Slice:
		// Nothing further
	case types.Map:
		// Nothing further
	case types.Alias:
		g.emitValidationForChild(c, thisNode.underlying, sw)
	case types.Struct:
		for _, fld := range thisNode.fields {
			if len(fld.name) == 0 {
				panic(fmt.Sprintf("missing field name in type %s (field-type %s)", thisNode.valueType, fld.childType))
			}
			// Missing JSON name is checked iff we have code to emit.

			// Accumulate into a buffer so we don't emit empty functions.
			buf := bytes.NewBuffer(nil)
			bufsw := sw.Dup(buf)

			validations := fld.fieldValidations
			if !validations.Empty() {
				emitComments(validations.Comments, bufsw)
				emitCallsToValidators(c, validations.Functions, bufsw)
			}

			// If the node is nil, this must be a type in a package we are not
			// handling - it's effectively opaque to us.
			if fld.node != nil {
				// Get to the real type.
				switch fld.node.valueType.Kind {
				case types.Alias, types.Struct:
					// If this field is another type, call its validation function.
					g.emitCallToOtherTypeFunc(c, fld.node, bufsw)
				default:
					// Descend into this field.
					g.emitValidationForChild(c, fld, bufsw)
				}
			}

			if buf.Len() > 0 {
				if len(fld.jsonName) == 0 {
					continue // TODO: Embedded (inline) types are expected to be unnamed.
				}

				leafType, typePfx, exprPfx := getLeafTypeAndPrefixes(fld.childType)
				targs := targs.WithArgs(generator.Args{
					"fieldName":    fld.name,
					"fieldJSON":    fld.jsonName,
					"fieldType":    leafType,
					"fieldTypePfx": typePfx,
					"fieldExprPfx": exprPfx,
				})

				if didSome {
					sw.Do("\n", nil)
				}
				sw.Do("// field $.inType|raw$.$.fieldName$\n", targs)
				sw.Do("errs = append(errs,\n", targs)
				sw.Do("  func(fldPath *$.field.Path|raw$, obj, oldObj $.fieldTypePfx$$.fieldType|raw$) (errs $.field.ErrorList|raw$) {\n", targs)
				if err := sw.Merge(buf, bufsw); err != nil {
					panic(fmt.Sprintf("failed to merge buffer: %v", err))
				}
				sw.Do("    return\n", targs)
				sw.Do("  }(fldPath.Child(\"$.fieldJSON$\"), ", targs)
				sw.Do("    $.fieldExprPfx$obj.$.fieldName$, ", targs)
				sw.Do("    $.safe.Field|raw$(oldObj, ", targs)
				sw.Do("        func(oldObj *$.inType|raw$) $.fieldTypePfx$$.fieldType|raw$ {", targs)
				sw.Do("            return $.fieldExprPfx$oldObj.$.fieldName$", targs)
				sw.Do("        }),", targs)
				sw.Do("    )...)\n", targs)
				sw.Do("\n", nil)
			} else {
				targs := targs.WithArgs(generator.Args{
					"fieldName": fld.name,
				})
				sw.Do("// field $.inType|raw$.$.fieldName$ has no validation\n", targs)
			}
			didSome = true
		}
	default:
		panic(fmt.Sprintf("unhandled type: %v (kind %s)", inType, inType.Kind))
	}
}

// emitCallToOtherTypeFunc generates a call to the specified node's generated
// validation function for a field in some parent context.
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
func (g *genValidations) emitCallToOtherTypeFunc(c *generator.Context, node *typeNode, sw *generator.SnippetWriter) {
	// If this type has no validations (transitively) then we don't need to do
	// anything.
	if !hasValidations(node) {
		return
	}

	targs := generator.Args{
		"funcName": c.Universe.Type(node.funcName),
	}
	sw.Do("errs = append(errs, $.funcName|raw$(ctx, op, fldPath, obj, oldObj)...)\n", targs)
}

// emitCallsToValidators emits calls to a list of validation functions for
// a single field or type. validations is a list of functions to call, with
// arguments.
//
// When calling registered validators, we always pass a nilable type.  E.g. if
// the field's type is string, we pass *string, and if the field's type is
// *string, we also pass *string.  This means that validators need to do
// nil-checks themselves, if they intend to dereference the pointer.  This
// makes updates more consistent.
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
func emitCallsToValidators(c *generator.Context, validations []validators.FunctionGen, sw *generator.SnippetWriter) {
	// Helper func
	sort := func(in []validators.FunctionGen) []validators.FunctionGen {
		sooner := make([]validators.FunctionGen, 0, len(in))
		later := make([]validators.FunctionGen, 0, len(in))

		for _, fg := range in {
			isShortCircuit := (fg.Flags.IsSet(validators.ShortCircuit))

			if isShortCircuit {
				sooner = append(sooner, fg)
			} else {
				later = append(later, fg)
			}
		}
		result := sooner
		result = append(result, later...)
		return result
	}

	validations = sort(validations)

	for _, v := range validations {
		isShortCircuit := v.Flags.IsSet(validators.ShortCircuit)
		isNonError := v.Flags.IsSet(validators.NonError)

		targs := generator.Args{
			"funcName": c.Universe.Type(v.Function),
			"field":    mkSymbolArgs(c, fieldPkgSymbols),
		}

		emitCall := func() {
			sw.Do("$.funcName|raw$", targs)
			if typeArgs := v.TypeArgs; len(typeArgs) > 0 {
				sw.Do("[", nil)
				for i, typeArg := range typeArgs {
					sw.Do("$.|raw$", c.Universe.Type(typeArg))
					if i < len(typeArgs)-1 {
						sw.Do(",", nil)
					}
				}
				sw.Do("]", nil)
			}
			sw.Do("(ctx, op, fldPath, obj, oldObj", targs)
			for _, arg := range v.Args {
				sw.Do(", ", nil)
				toGolangSourceDataLiteral(sw, c, arg)
			}
			sw.Do(")", targs)
		}

		// If validation is conditional, wrap the validation function with a conditions check.
		if !v.Conditions.Empty() {
			emitBaseFunction := emitCall
			emitCall = func() {
				sw.Do("func() $.field.ErrorList|raw$ {\n", targs)
				sw.Do("  if ", nil)
				firstCondition := true
				if len(v.Conditions.OptionEnabled) > 0 {
					sw.Do("op.Options.Has($.$)", strconv.Quote(v.Conditions.OptionEnabled))
					firstCondition = false
				}
				if len(v.Conditions.OptionDisabled) > 0 {
					if !firstCondition {
						sw.Do(" && ", nil)
					}
					sw.Do("!op.Options.Has($.$)", strconv.Quote(v.Conditions.OptionDisabled))
				}
				sw.Do(" {\n", nil)
				sw.Do("    return ", nil)
				emitBaseFunction()
				sw.Do("\n", nil)
				sw.Do("  } else {\n", nil)
				sw.Do("    return nil // skip validation\n", nil)
				sw.Do("  }\n", nil)
				sw.Do("}()", nil)
			}
		}

		for _, comment := range v.Comments {
			sw.Do("// $.$\n", comment)
		}
		if isShortCircuit {
			sw.Do("if e := ", nil)
			emitCall()
			sw.Do("; len(e) != 0 {\n", nil)
			if !isNonError {
				sw.Do("errs = append(errs, e...)\n", nil)
			}
			sw.Do("    return // do not proceed\n", nil)
			sw.Do("}\n", nil)
		} else {
			if isNonError {
				emitCall()
			} else {
				sw.Do("errs = append(errs, ", nil)
				emitCall()
				sw.Do("...)\n", nil)
			}
		}
	}
}

func emitComments(comments []string, sw *generator.SnippetWriter) {
	for _, comment := range comments {
		sw.Do("// ", nil)
		sw.Do(comment, nil)
		sw.Do("\n", nil)
	}
}

// emitValidationVariables emits a list of variable declarations. Each variable declaration has a
// private (unexported) variable name, and a function invocation declaration that is expected
// to initialize the value of the variable.
func (g *genValidations) emitValidationVariables(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) {
	tn := g.discovered.typeNodes[t]

	variables := tn.typeValidations.Variables
	slices.SortFunc(variables, func(a, b validators.VariableGen) int {
		return cmp.Compare(a.Variable.Name, b.Variable.Name)
	})
	for _, variable := range variables {
		fn := variable.InitFunc
		targs := generator.Args{
			"varName": c.Universe.Type(types.Name(variable.Variable)),
			"initFn":  c.Universe.Type(fn.Function),
		}
		for _, comment := range fn.Comments {
			sw.Do("// $.$\n", comment)
		}
		sw.Do("var $.varName|private$ = $.initFn|raw$", targs)
		if typeArgs := fn.TypeArgs; len(typeArgs) > 0 {
			sw.Do("[", nil)
			for i, typeArg := range typeArgs {
				sw.Do("$.|raw$", c.Universe.Type(typeArg))
				if i < len(typeArgs)-1 {
					sw.Do(",", nil)
				}
			}
			sw.Do("]", nil)
		}
		sw.Do("(", targs)
		for i, arg := range fn.Args {
			if i != 0 {
				sw.Do(", ", nil)
			}
			toGolangSourceDataLiteral(sw, c, arg)
		}
		sw.Do(")\n", nil)

	}
}

func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context, value any) {
	// For safety, be strict in what values we output to visited source, and ensure strings
	// are quoted.

	switch v := value.(type) {
	case uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64, bool:
		sw.Do(fmt.Sprintf("%v", value), nil)
	case string:
		// If the incoming string was quoted, we still do it ourselves, JIC.
		str := value.(string)
		if s, err := strconv.Unquote(str); err == nil {
			str = s
		}
		sw.Do(fmt.Sprintf("%q", str), nil)
	case *types.Type:
		sw.Do("$.|raw$", v)
	case types.Member:
		sw.Do("obj."+v.Name, nil)
	case validators.Identifier:
		sw.Do("$.|raw$", c.Universe.Type(types.Name(v)))
	case *validators.Identifier:
		sw.Do("$.|raw$", c.Universe.Type(types.Name(*v)))
	case validators.PrivateVar:
		sw.Do("$.|private$", c.Universe.Type(types.Name(v)))
	case *validators.PrivateVar:
		sw.Do("$.|private$", c.Universe.Type(types.Name(*v)))
	case validators.WrapperFunction:
		if extraArgs := v.Function.Args; len(extraArgs) == 0 {
			// If the function to be wrapped has no additional arguments, we can
			// just use it directly.
			targs := generator.Args{
				"funcName": c.Universe.Type(v.Function.Function),
			}
			for _, comment := range v.Function.Comments {
				sw.Do("// $.$\n", comment)
			}
			sw.Do("$.funcName|raw$", targs)
		} else {
			// If the function to be wrapped has additional arguments, we need
			// a "standard signature" validation function to wrap it.
			targs := generator.Args{
				"funcName":   c.Universe.Type(v.Function.Function),
				"field":      mkSymbolArgs(c, fieldPkgSymbols),
				"operation":  mkSymbolArgs(c, operationPkgSymbols),
				"context":    mkSymbolArgs(c, contextPkgSymbols),
				"objType":    v.ObjType,
				"objTypePfx": "*",
			}
			if isNilableType(v.ObjType) {
				targs["objTypePfx"] = ""
			}

			emitCall := func() {
				sw.Do("return $.funcName|raw$", targs)
				typeArgs := v.Function.TypeArgs
				if len(typeArgs) > 0 {
					sw.Do("[", nil)
					for i, typeArg := range typeArgs {
						sw.Do("$.|raw$", c.Universe.Type(typeArg))
						if i < len(typeArgs)-1 {
							sw.Do(",", nil)
						}
					}
					sw.Do("]", nil)
				}
				sw.Do("(ctx, op, fldPath, obj, oldObj", targs)
				for _, arg := range extraArgs {
					sw.Do(", ", nil)
					toGolangSourceDataLiteral(sw, c, arg)
				}
				sw.Do(")", targs)
			}
			sw.Do("func(", targs)
			sw.Do("    ctx $.context.Context|raw$, ", targs)
			sw.Do("    op $.operation.Operation|raw$, ", targs)
			sw.Do("    fldPath *$.field.Path|raw$, ", targs)
			sw.Do("    obj, oldObj $.objTypePfx$$.objType|raw$ ", targs)
			sw.Do(")    $.field.ErrorList|raw$ {\n", targs)
			emitCall()
			sw.Do("\n}", targs)
		}
	case validators.Literal:
		sw.Do("$.$", v)
	case validators.FunctionLiteral:
		sw.Do("func(", nil)
		for i, param := range v.Parameters {
			if i > 0 {
				sw.Do(", ", nil)
			}
			targs := generator.Args{
				"name": param.Name,
				"type": param.Type,
			}
			sw.Do("$.name$ $.type|raw$", targs)
		}
		sw.Do(")", nil)
		if len(v.Results) > 1 {
			sw.Do(" (", nil)
		}
		for i, ret := range v.Results {
			if i > 0 {
				sw.Do(", ", nil)
			}
			targs := generator.Args{
				"name": ret.Name,
				"type": ret.Type,
			}
			sw.Do("$.name$ $.type|raw$", targs)
		}
		if len(v.Results) > 1 {
			sw.Do(")", nil)
		}
		sw.Do(" { $.$ }", v.Body)
	default:
		rv := reflect.ValueOf(value)
		switch rv.Kind() {
		case reflect.Slice, reflect.Array:
			arraySize := ""
			if rv.Kind() == reflect.Array {
				arraySize = strconv.Itoa(rv.Len())
			}
			var itemType string
			switch rv.Type().Elem().Kind() {
			case reflect.String: // For now, only support lists of strings.
				itemType = rv.Type().Elem().Name()
			default:
				panic(fmt.Sprintf("Unsupported extraArg type: %T", value))
			}
			rv.Type().Elem()
			sw.Do("[$.arraySize$]$.itemType${", map[string]string{"arraySize": arraySize, "itemType": itemType})
			for i := 0; i < rv.Len(); i++ {
				val := rv.Index(i)
				toGolangSourceDataLiteral(sw, c, val.Interface())
				if i < rv.Len()-1 {
					sw.Do(", ", nil)
				}
			}
			sw.Do("}", nil)
		default:
			// TODO: check this during discovery and emit an error with more useful information
			panic(fmt.Sprintf("Unsupported extraArg type: %T", value))
		}
	}
}

// isNilableType returns true if the argument type can be compared to nil.
func isNilableType(t *types.Type) bool {
	t = unalias(t)
	switch t.Kind {
	case types.Pointer, types.Map, types.Slice, types.Interface: // Note: Arrays are not nilable
		return true
	}
	return false
}

// getLeafTypeAndPrefixes returns the "leaf value type" for a given type, as
// well as type and expression prefix strings for the input type.  The type
// prefix can be prepended to the given type's name to produce the nilable form
// of that type.  The expression prefix can be prepended to a variable of the
// given type to produce the nilable form of that value.
//
// Example: Given an input type "string" this should produce (string, "*", "&").
// That is to say: the value-type is "string", which yields "*string" when the
// type prefix is applied, and a variable "x" becomes "&x" when the expression
// prefix is applied.
//
// Example: Given an input type "*string" this should produce (string, "*", "").
// That is to say: the value-type is "string", which yields "*string" when the
// type prefix is applied, and a variable "x" remains "x" when the expression
// prefix is applied.
func getLeafTypeAndPrefixes(inType *types.Type) (*types.Type, string, string) {
	leafType := inType
	typePfx := ""
	exprPfx := ""

	nPtrs := 0
	for leafType.Kind == types.Pointer {
		nPtrs++
		leafType = leafType.Elem
	}
	if !isNilableType(leafType) {
		typePfx = "*"
		if nPtrs == 0 {
			exprPfx = "&"
		} else {
			exprPfx = strings.Repeat("*", nPtrs-1)
		}
	} else {
		exprPfx = strings.Repeat("*", nPtrs)
	}

	return leafType, typePfx, exprPfx
}

// FixtureTests generates a test file that checks all validateFalse validations.
func FixtureTests(outputFilename string, testFixtureTags sets.Set[string]) generator.Generator {
	return &fixtureTestGen{
		GoGenerator: generator.GoGenerator{
			OutputFilename: outputFilename,
		},
		testFixtureTags: testFixtureTags,
	}
}

type fixtureTestGen struct {
	generator.GoGenerator
	testFixtureTags sets.Set[string]
}

func (g *fixtureTestGen) Imports(_ *generator.Context) (imports []string) {
	return []string{`"testing"`}
}

func (g *fixtureTestGen) Init(c *generator.Context, w io.Writer) error {
	if g.testFixtureTags.Has("validateFalse") {
		sw := generator.NewSnippetWriter(w, c, "$", "$")
		sw.Do("func TestValidation(t *testing.T) {\n", nil)
		sw.Do("  localSchemeBuilder.Test(t).ValidateFixtures()\n", nil)
		sw.Do("}\n", nil)
	}
	return nil
}
