// Copyright 2023 Google LLC
//
// 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 decls

import (
	"reflect"
	"strings"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"

	chkdecls "github.com/google/cel-go/checker/decls"
	"github.com/google/cel-go/common"
	"github.com/google/cel-go/common/operators"
	"github.com/google/cel-go/common/overloads"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
	"github.com/google/cel-go/common/types/traits"

	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

func TestFunctionBindings(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	bindings, err := sizeFunc.Bindings()
	if err != nil {
		t.Fatalf("sizeFunc.Bindings() produced an err: %v", err)
	}
	if len(bindings) != 0 {
		t.Errorf("sizeFunc.Bindings() produced %d bindings, wanted none", len(bindings))
	}
	sizeFuncDef, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType,
			UnaryBinding(func(list ref.Val) ref.Val {
				sizer := list.(traits.Sizer)
				return sizer.Size()
			}),
		),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeMerged, err := sizeFunc.Merge(sizeFuncDef)
	if err != nil {
		t.Fatalf("Merge() failed: %v", err)
	}
	bindings, err = sizeMerged.Bindings()
	if err != nil {
		t.Fatalf("sizeFunc.Bindings() produced an err: %v", err)
	}
	if len(bindings) != 2 {
		t.Errorf("sizeFunc.Bindings() got %d bindings, wanted 2", len(bindings))
	}
	empty := types.DefaultTypeAdapter.NativeToValue([]string{})
	in := types.DefaultTypeAdapter.NativeToValue([]string{"1", "2"})
	for _, binding := range bindings {
		if binding.Unary == nil {
			t.Errorf("binding missing unary implementation: %v", binding.Operator)
			continue
		}
		if binding.Unary(in) != types.Int(2) {
			t.Errorf("binding invocation got %v, wanted 2", binding.Unary(in))
		}
		if binding.Unary(empty) != types.IntZero {
			t.Errorf("binding invocation got %v, wanted 0", binding.Unary(empty))
		}
	}
}

func TestFunctionVariableArgBindings(t *testing.T) {
	splitImpl := func(str, delim string, count int64) ref.Val {
		return types.DefaultTypeAdapter.NativeToValue(strings.SplitN(str, delim, int(count)))
	}
	splitFunc, err := NewFunction("split",
		MemberOverload("string_split",
			[]*types.Type{types.StringType}, types.NewListType(types.StringType),
			UnaryBinding(func(str ref.Val) ref.Val {
				s := str.(types.String)
				return splitImpl(string(s), "", -1)
			})),
		MemberOverload("string_split_string",
			[]*types.Type{types.StringType, types.StringType}, types.NewListType(types.StringType),
			BinaryBinding(func(str, sep ref.Val) ref.Val {
				s := str.(types.String)
				delim := sep.(types.String)
				return splitImpl(string(s), string(delim), -1)
			})),
		MemberOverload("string_split_string_int",
			[]*types.Type{types.StringType, types.StringType, types.IntType}, types.NewListType(types.StringType),
			FunctionBinding(func(args ...ref.Val) ref.Val {
				s := args[0].(types.String)
				delim := args[1].(types.String)
				count := args[2].(types.Int)
				return splitImpl(string(s), string(delim), int64(count))
			})),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	bindings, err := splitFunc.Bindings()
	if err != nil {
		t.Fatalf("sizeFunc.Bindings() produced an err: %v", err)
	}
	if len(bindings) != 4 {
		t.Errorf("sizeFunc.Bindings() produced %d bindings, wanted 4", len(bindings))
	}
	in := types.String("hi")
	sep := types.String("")
	out := types.DefaultTypeAdapter.NativeToValue([]string{"h", "i"})
	for _, binding := range bindings {
		if binding.Unary != nil {
			if binding.Unary(in).Equal(out) != types.True {
				t.Errorf("binding invocation got %v, wanted %v", binding.Unary(in), out)
			}
			celErr := binding.Unary(types.Bytes("hi"))
			if !types.IsError(celErr) || !strings.Contains(celErr.(*types.Err).String(), "no such overload") {
				t.Errorf("binding.Unary(bytes) got %v, wanted no such overload", celErr)
			}
		}
		if binding.Binary != nil {
			if binding.Binary(in, sep).Equal(out) != types.True {
				t.Errorf("binding invocation got %v, wanted %v", binding.Binary(in, sep), out)
			}
			celErr := binding.Binary(types.Bytes("hi"), sep)
			if !types.IsError(celErr) || !strings.Contains(celErr.(*types.Err).String(), "no such overload") {
				t.Errorf("binding.Binary(bytes, string) got %v, wanted no such overload", celErr)
			}
			celUnk := binding.Binary(types.Bytes("hi"), types.NewUnknown(1, types.NewAttributeTrail("x")))
			if !types.IsUnknown(celUnk) {
				t.Errorf("binding.Binary(bytes, unk) got %v, wanted unknown{1}", celUnk)
			}
		}
		if binding.Function != nil {
			if binding.Function(in, sep, types.IntNegOne).Equal(out) != types.True {
				t.Errorf("binding invocation got %v, wanted %v", binding.Function(in, sep, types.IntNegOne), out)
			}
			celErr := binding.Function(types.Bytes("hi"), sep, types.IntOne)
			if !types.IsError(celErr) || !strings.Contains(celErr.(*types.Err).String(), "no such overload") {
				t.Errorf("binding.Function(bytes, string, int) got %v, wanted no such overload", celErr)
			}
			if binding.Operator == "split" {
				if binding.Function(in).Equal(out) != types.True {
					t.Errorf("binding invocation got %v, wanted %v", binding.Function(in), out)
				}
				if binding.Function(in, sep).Equal(out) != types.True {
					t.Errorf("binding invocation got %v, wanted %v", binding.Function(in, sep), out)
				}
				out := binding.Function()
				if !types.IsError(out) || out.(*types.Err).String() != "no such overload: split()" {
					t.Fatalf("binding.Function() got %v, wanted error", out)
				}
			}
		}
	}
}

func TestFunctionZeroArityBinding(t *testing.T) {
	now := types.DefaultTypeAdapter.NativeToValue(time.UnixMilli(1000))
	nowFunc, err := NewFunction("now",
		Overload("now", []*types.Type{}, types.TimestampType,
			FunctionBinding(func(args ...ref.Val) ref.Val {
				return now
			})),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	for _, od := range nowFunc.OverloadDecls() {
		if !od.HasBinding() {
			t.Errorf("Overload %s does not have binding, wanted function binding", od.ID())
		}
	}
	bindings, err := nowFunc.Bindings()
	if err != nil {
		t.Fatalf("nowFunc.Bindings() produced an err: %v", err)
	}
	if len(bindings) != 1 {
		t.Errorf("nowFunc.Bindings() produced %d bindings, wanted one", len(bindings))
	}
	out := bindings[0].Function()
	if out != now {
		t.Errorf("now() got %v, wanted %v", out, now)
	}
}

func TestFunctionSingletonBinding(t *testing.T) {
	size, err := NewFunction("size",
		// Since the singleton requires that the operand is a traits.Sizer, the type guards can be
		// disabled for a minor boost in performance, and because the argument type-checking
		// doesn't actually give much additional benefit. The drawback is that invalid signatures
		// at type-check might be valid at runtime.
		DisableTypeGuards(true),
		Overload("size_map",
			[]*types.Type{types.NewMapType(types.NewTypeParamType("K"), types.NewTypeParamType("V"))}, types.IntType),
		Overload("size_list",
			[]*types.Type{types.NewListType(types.NewTypeParamType("V"))}, types.IntType),
		Overload("size_string",
			[]*types.Type{types.StringType}, types.IntType),
		MemberOverload("map_size",
			[]*types.Type{types.NewMapType(types.NewTypeParamType("K"), types.NewTypeParamType("V"))}, types.IntType),
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("V"))}, types.IntType),
		MemberOverload("string_size",
			[]*types.Type{types.StringType}, types.IntType),
		SingletonUnaryBinding(func(arg ref.Val) ref.Val {
			return arg.(traits.Sizer).Size()
		}, traits.SizerType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if !size.HasSingletonBinding() {
		t.Error("size.HasSingletonBinding() got false, wanted true")
	}
	bindings, err := size.Bindings()
	if err != nil {
		t.Fatalf("size.Bindings() failed: %v", err)
	}
	if len(bindings) != 1 {
		t.Errorf("size.Bindings() got %d bindings, wanted 1", len(bindings))
	}
	if bindings[0].Unary == nil {
		t.Fatalf("size.Bindings() missing singleton unary binding")
	}
	result := bindings[0].Unary(types.String("hello"))
	if result.Equal(types.Int(5)) != types.True {
		t.Errorf("size('hello') got %v, wanted 5", result)
	}
	// Invalid at type-check, but valid since type guard checks have been disabled
	result = bindings[0].Unary(types.Bytes("hello"))
	if result.Equal(types.Int(5)) != types.True {
		t.Errorf("size(b'hello') got %v, wanted 5", result)
	}
}

func TestVariableDocumentation(t *testing.T) {
	v := NewVariableWithDoc("var", types.StringType, "string variable")
	doc := v.Documentation()
	if doc.Description != v.Description() {
		t.Errorf("doc.Description got %s, wanted %s", doc.Description, v.Description())
	}
	if doc.Name != v.Name() {
		t.Errorf("doc.Name got %s, wanted %s", doc.Name, v.Name())
	}
	if doc.Type != "string" {
		t.Errorf("doc.Type got %s, wanted string", doc.Type)
	}
}

func TestFunctionDocumentation(t *testing.T) {
	tests := []struct {
		name       string
		fn         *FunctionDecl
		signatures []string
		examples   []string
	}{
		{
			name: "function",
			fn: testFunction(t, "size",
				FunctionDocs(`compute the number of entries in a list or map`),
				MemberOverload("list_size",
					[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType,
					OverloadExamples(`[].size() // 0`, `[1, 2, 3].size() // 3`)),
				Overload("size_list",
					[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType,
					OverloadExamples(`size([]) // 0`, `size([1, 2, 3]) // 3`))),
			signatures: []string{
				"list(<T>).size() -> int",
				"size(list(<T>)) -> int",
			},
			examples: []string{
				`[].size() // 0`,
				`[1, 2, 3].size() // 3`,
				`size([]) // 0`,
				`size([1, 2, 3]) // 3`,
			},
		},
		{
			name: "type",
			fn: testFunction(t, overloads.TypeConvertType,
				Overload(overloads.TypeConvertType,
					[]*types.Type{types.NewTypeTypeWithParam(types.NewTypeParamType("T"))},
					types.TypeType,
					OverloadExamples(`type(int) // type`))),
			signatures: []string{"type(type) -> type"},
			examples:   []string{`type(int) // type`},
		},
		{
			name: "unary operator",
			fn: testFunction(t, operators.Negate,
				FunctionDocs(`negate a numeric value`),
				Overload(overloads.NegateInt64, []*types.Type{types.IntType}, types.IntType,
					OverloadExamples(`-(1) // -1`))),
			signatures: []string{"-int -> int"},
			examples:   []string{`-(1) // -1`},
		},
		{
			name: "binary operator",
			fn: testFunction(t, operators.Add,
				FunctionDocs(`add two numeric values`),
				Overload(overloads.AddInt64, []*types.Type{types.IntType, types.IntType}, types.IntType,
					OverloadExamples(`1 + 2 // 3`))),
			signatures: []string{"int + int -> int"},
			examples:   []string{`1 + 2 // 3`},
		},
		{
			name: "index operator",
			fn: testFunction(t, operators.Index,
				FunctionDocs(`access a list by numeric index, zero-based`),
				Overload(overloads.IndexList, []*types.Type{types.NewListType(types.NewTypeParamType("T")), types.IntType}, types.NewTypeParamType("T"),
					OverloadExamples(`[1, 2, 3, 4][2] // 3`))),
			signatures: []string{"list(<T>)[int] -> <T>"},
			examples:   []string{`[1, 2, 3, 4][2] // 3`},
		},
		{
			name: "conditional",
			fn: testFunction(t, operators.Conditional,
				FunctionDocs(`ternary operator`),
				Overload(overloads.Conditional,
					[]*types.Type{types.BoolType, types.NewTypeParamType("T"), types.NewTypeParamType("T")},
					types.NewTypeParamType("T"),
					OverloadExamples(`true ? 1 : 2 // 1`))),
			signatures: []string{"bool ? <T> : <T> -> <T>"},
			examples:   []string{`true ? 1 : 2 // 1`},
		},
	}
	for _, tst := range tests {
		tc := tst
		t.Run(tc.name, func(t *testing.T) {
			doc := tc.fn.Documentation()
			if doc.Kind != common.DocFunction {
				t.Errorf("fn.Documentation() kind got %v, wanted common.DocFunction", doc.Kind)
			}
			if doc.Description != tc.fn.Description() {
				t.Errorf("doc.Description got %s, wanted %s", doc.Description, tc.fn.Description())
			}
			if len(doc.Children) != len(tc.fn.OverloadDecls()) {
				t.Fatalf("doc.Children count was %d, wanted %d", len(doc.Children), len(tc.fn.OverloadDecls()))
			}
			for _, child := range doc.Children {
				found := false
				for _, sig := range tc.signatures {
					if child.Signature == sig {
						found = true
						break
					}
				}
				if !found {
					t.Errorf("unable to find signature: %s", child.Signature)
				}
				for _, childEx := range child.Children {
					found = false
					for _, ex := range tc.examples {
						if strings.Contains(childEx.Description, ex) {
							found = true
							break
						}
					}
					if !found {
						t.Errorf("unable to find example: %v", childEx.Description)
					}
				}
			}
		})
	}
}

func TestFunctionMerge(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		FunctionDocs(`compute the number of entries in a list or map`),
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
		MemberOverload("map_size",
			[]*types.Type{types.NewMapType(types.NewTypeParamType("K"), types.NewTypeParamType("V"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	out, err := sizeFunc.Merge(sizeFunc)
	if err != nil {
		t.Errorf("sizeFunc.Merge(sizeFunc) failed: %v", err)
	}
	if out != sizeFunc {
		t.Errorf("sizeFunc.Merge(sizeFunc) != sizeFunc: %v", out)
	}
	sizeVecFunc, err := NewFunction("size",
		MemberOverload("vector_size",
			[]*types.Type{types.NewOpaqueType("vector", types.NewTypeParamType("T"))}, types.IntType),
		SingletonUnaryBinding(func(sizer ref.Val) ref.Val {
			return sizer.(traits.Sizer).Size()
		}, traits.SizerType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeMerged, err := sizeFunc.Merge(sizeVecFunc)
	if err != nil {
		t.Fatalf("Merge() failed: %v", err)
	}
	if sizeMerged.Name() != "size" {
		t.Errorf("Merge() produced a function with name %v, wanted 'size'", sizeMerged.Name())
	}
	if len(sizeMerged.overloads) != 3 {
		t.Errorf("Merge() produced %d overloads, wanted 3", len(sizeFunc.overloads))
	}
	if sizeMerged.Description() != "compute the number of entries in a list or map" {
		t.Errorf("Description() got %s, wanted %s", sizeMerged.Description(),
			"compute the number of entries in a list or map")
	}
	overloads := map[string]bool{
		"list_size":   true,
		"map_size":    true,
		"vector_size": true,
	}
	for _, o := range sizeMerged.overloads {
		delete(overloads, o.ID())
	}
	if len(overloads) != 0 {
		t.Errorf("Merge() did not include overloads: %v", overloads)
	}
}

func TestFunctionMergeWrongName(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeVecFunc, err := NewFunction("sizeN",
		MemberOverload("vector_size",
			[]*types.Type{types.NewOpaqueType("vector", types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = sizeFunc.Merge(sizeVecFunc)
	if err == nil || !strings.Contains(err.Error(), "unrelated functions") {
		t.Fatalf("Merge() expected to fail, got: %v", err)
	}
}

func TestFunctionMergeOverloadCollision(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeVecFunc, err := NewFunction("size",
		MemberOverload("list_size2",
			[]*types.Type{types.NewListType(types.NewTypeParamType("K"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = sizeFunc.Merge(sizeVecFunc)
	if err == nil || !strings.Contains(err.Error(), "declaration merge failed") {
		t.Fatalf("Merge() expected to fail, got: %v", err)
	}
}

func TestFunctionMergeOverloadArgCountRedefinition(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeVecFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T")), types.IntType}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = sizeFunc.Merge(sizeVecFunc)
	if err == nil || !strings.Contains(err.Error(), "redefinition") {
		t.Fatalf("Merge() expected to fail, got: %v", err)
	}
}

func TestFunctionMergeOverloadArgTypeRedefinition(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("arg_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeVecFunc, err := NewFunction("size",
		MemberOverload("arg_size",
			[]*types.Type{types.NewMapType(types.IntType, types.StringType)}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = sizeFunc.Merge(sizeVecFunc)
	if err == nil || !strings.Contains(err.Error(), "redefinition") {
		t.Fatalf("Merge() expected to fail, got: %v", err)
	}
}

func TestFunctionMergeSingletonRedefinition(t *testing.T) {
	sizeFunc, err := NewFunction("size",
		MemberOverload("list_size",
			[]*types.Type{types.NewListType(types.NewTypeParamType("T"))}, types.IntType),
		SingletonUnaryBinding(func(ref.Val) ref.Val {
			return types.IntZero
		}),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	sizeVecFunc, err := NewFunction("size",
		MemberOverload("string_size",
			[]*types.Type{types.StringType}, types.IntType),
		SingletonUnaryBinding(func(ref.Val) ref.Val {
			return types.IntZero
		}),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = sizeFunc.Merge(sizeVecFunc)
	if err == nil || !strings.Contains(err.Error(), "already has a singleton") {
		t.Fatalf("Merge() expected to fail, got: %v", err)
	}
}

func TestFunctionAddDuplicateOverloads(t *testing.T) {
	_, err := NewFunction("max",
		Overload("max_int", []*types.Type{types.IntType}, types.IntType),
		Overload("max_int", []*types.Type{types.IntType}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() with duplicate overload signature failed: %v", err)
	}
}

func TestFunctionAddDuplicateOverloadsPreservesBinding(t *testing.T) {
	f, err := NewFunction("max",
		Overload("max_int", []*types.Type{types.IntType}, types.IntType),
		Overload("max_int", []*types.Type{types.IntType}, types.IntType,
			UnaryBinding(func(v ref.Val) ref.Val {
				return v
			})),
		Overload("max_int", []*types.Type{types.IntType}, types.IntType),
	)
	if err != nil {
		t.Fatalf("NewFunction() with duplicate overload signature failed: %v", err)
	}
	if len(f.overloads) != 1 {
		t.Fatal("Duplicate overloads were not merged")
	}
	o := f.overloads["max_int"]
	if o.unaryOp == nil {
		t.Error("Duplicate overloads purged the overload binding")
	}
}

func TestFunctionAddCollidingOverloads(t *testing.T) {
	_, err := NewFunction("max",
		Overload("max_int", []*types.Type{types.IntType}, types.IntType),
		Overload("max_int2", []*types.Type{types.IntType}, types.IntType),
	)
	if err == nil || !strings.Contains(err.Error(), "max_int collides with max_int2") {
		t.Fatalf("NewFunction() got %v, wanted collision error", err)
	}
}

func TestFunctionNoOverloads(t *testing.T) {
	_, err := NewFunction("right", SingletonBinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
		return arg2
	}))
	if err == nil || !strings.Contains(err.Error(), "must have at least one overload") {
		t.Errorf("NewFunction() got %v, wanted 'must have at least one overload'", err)
	}
}

func TestSingletonOverloadCollision(t *testing.T) {
	fn, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType,
			UnaryBinding(func(arg ref.Val) ref.Val {
				return arg
			}),
		),
		SingletonUnaryBinding(func(arg ref.Val) ref.Val {
			return arg
		}),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = fn.Bindings()
	if err == nil || !strings.Contains(err.Error(), "incompatible with specialized overloads") {
		t.Errorf("NewFunction() got %v, wanted incompatible with specialized overloads", err)
	}
}

func TestSingletonOverloadLateBindingCollision(t *testing.T) {
	fn, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType,
			LateFunctionBinding(),
		),
		SingletonUnaryBinding(func(arg ref.Val) ref.Val {
			return arg
		}),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	_, err = fn.Bindings()
	if err == nil || !strings.Contains(err.Error(), "incompatible with late bindings") {
		t.Errorf("NewFunction() got %v, wanted incompatible with late bindings", err)
	}
}

func TestSingletonUnaryBindingRedefinition(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType),
		SingletonUnaryBinding(func(arg ref.Val) ref.Val {
			return arg
		}),
		SingletonUnaryBinding(func(arg ref.Val) ref.Val {
			return arg
		}),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a singleton binding") {
		t.Errorf("NewFunction() got %v, wanted already has a singleton binding", err)
	}
}

func TestSingletonBinaryBindingRedefinition(t *testing.T) {
	_, err := NewFunction("right",
		Overload("right_double_double", []*types.Type{types.DoubleType, types.DoubleType}, types.DoubleType),
		SingletonBinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
			return arg2
		}, traits.ComparerType),
		SingletonBinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
			return arg2
		}),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a singleton binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a singleton binding", err)
	}
}

func TestSingletonFunctionBindingRedefinition(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType),
		SingletonFunctionBinding(func(args ...ref.Val) ref.Val {
			return args[0]
		}, traits.ComparerType),
		SingletonFunctionBinding(func(args ...ref.Val) ref.Val {
			return args[0]
		}, traits.ComparerType),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a singleton binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a singleton binding", err)
	}
}

func TestOverloadUnaryBindingRedefinition(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType,
			UnaryBinding(func(arg ref.Val) ref.Val {
				return arg
			}),
			UnaryBinding(func(arg ref.Val) ref.Val {
				return arg
			}),
		),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a binding") {
		t.Errorf("NewFunction() got %v, wanted already has a binding", err)
	}
}

func TestOverloadUnaryBindingArgCountMismatch(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{}, types.AnyType,
			UnaryBinding(func(arg ref.Val) ref.Val {
				return arg
			}),
		),
	)
	if err == nil || !strings.Contains(err.Error(), "non-unary overload") {
		t.Errorf("NewFunction() got %v, wanted non-unary overload", err)
	}
}

func TestOverloadBinaryBindingArgCountMismatch(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{}, types.AnyType,
			BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
				return lhs
			}),
		),
	)
	if err == nil || !strings.Contains(err.Error(), "non-binary overload") {
		t.Errorf("NewFunction() got %v, wanted non-binary overload", err)
	}
}

func TestOverloadBinaryBindingRedefinition(t *testing.T) {
	_, err := NewFunction("right",
		Overload("right_double_double", []*types.Type{types.DoubleType, types.DoubleType}, types.DoubleType,
			BinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
				return arg2
			}),
			BinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
				return arg2
			}),
		),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a binding", err)
	}
}

func TestOverloadFunctionBindingRedefinition(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_any", []*types.Type{types.AnyType}, types.AnyType,
			FunctionBinding(func(args ...ref.Val) ref.Val {
				return args[0]
			}),
			FunctionBinding(func(args ...ref.Val) ref.Val {
				return args[0]
			}),
		),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a binding", err)
	}
}

func TestOverloadFunctionLateBinding(t *testing.T) {
	function, err := NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType}, types.AnyType, LateFunctionBinding(), LateFunctionBinding()),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if len(function.OverloadDecls()) != 1 {
		t.Fatalf("NewFunction() got %v, wanted 1 overload", function.OverloadDecls())
	}
	if !function.OverloadDecls()[0].HasLateBinding() {
		t.Errorf("overload %v did not have a late binding", function.OverloadDecls()[0])
	}
}

func TestOverloadFunctionMixLateAndNonLateBinding(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType}, types.AnyType, LateFunctionBinding()),
		Overload("id_int", []*types.Type{types.IntType}, types.AnyType),
	)
	if err == nil || !strings.Contains(err.Error(), "cannot mix late and non-late bindings") {
		t.Errorf("NewCustomEnv() got %v, wanted cannot mix late and non-late bindings", err)
	}
}

func TestOverloadFunctionBindingWithLateBinding(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType}, types.AnyType, FunctionBinding(func(args ...ref.Val) ref.Val {
			return args[0]
		}), LateFunctionBinding()),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a binding", err)
	}
}

func TestOverloadFunctionLateBindingWithBinding(t *testing.T) {
	_, err := NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType}, types.AnyType, LateFunctionBinding(),
			FunctionBinding(func(args ...ref.Val) ref.Val {
				return args[0]
			})),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a late binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a late binding", err)
	}

	_, err = NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType}, types.AnyType, LateFunctionBinding(),
			UnaryBinding(func(arg ref.Val) ref.Val {
				return arg
			})),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a late binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a late binding", err)
	}

	_, err = NewFunction("id",
		Overload("id_bool", []*types.Type{types.BoolType, types.BoolType}, types.AnyType, LateFunctionBinding(),
			BinaryBinding(func(arg1 ref.Val, arg2 ref.Val) ref.Val {
				return arg1
			})),
	)
	if err == nil || !strings.Contains(err.Error(), "already has a late binding") {
		t.Errorf("NewCustomEnv() got %v, wanted already has a late binding", err)
	}
}

func TestOverloadIsNonStrict(t *testing.T) {
	fn, err := NewFunction("getOrDefault",
		MemberOverload("get",
			[]*types.Type{types.NewMapType(
				types.NewTypeParamType("K"), types.NewTypeParamType("V")),
				types.NewTypeParamType("K"),
				types.NewTypeParamType("V"),
			},
			types.NewTypeParamType("V"),
			OverloadOperandTrait(traits.ContainerType|traits.IndexerType),
			OverloadIsNonStrict(),
			FunctionBinding(func(args ...ref.Val) ref.Val {
				container := args[0].(traits.Container)
				key := args[1]
				orValue := args[2]
				if types.Bool(types.IsUnknownOrError(key)) {
					return orValue
				}
				if container.Contains(key) == types.True {
					return args[0].(traits.Indexer).Get(key)
				}
				return orValue
			}),
		),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	bindings, err := fn.Bindings()
	if err != nil {
		t.Fatalf("fn.Binding() failed: %v", err)
	}
	m := types.DefaultTypeAdapter.NativeToValue(map[string]string{"hello": "world"})
	out := bindings[0].Function(m, types.String("hello"), types.String("goodbye"))
	if out.Equal(types.String("world")) != types.True {
		t.Errorf("function got %v, wanted 'world'", out)
	}
	out = bindings[0].Function(m, types.String("missing"), types.String("goodbye"))
	if out.Equal(types.String("goodbye")) != types.True {
		t.Errorf("function got %v, wanted 'goodbye'", out)
	}
	out = bindings[0].Function(m, types.NewErr("no such key"), types.String("goodbye"))
	if out.Equal(types.String("goodbye")) != types.True {
		t.Errorf("function got %v, wanted 'goodbye'", out)
	}
}

func TestOverloadOperandTrait(t *testing.T) {
	fn, err := NewFunction("getOrDefault",
		MemberOverload("get",
			[]*types.Type{types.NewMapType(
				types.NewTypeParamType("K"), types.NewTypeParamType("V")),
				types.NewTypeParamType("K"),
				types.NewTypeParamType("V"),
			},
			types.NewTypeParamType("V"),
			OverloadOperandTrait(traits.ContainerType|traits.IndexerType),
			FunctionBinding(func(args ...ref.Val) ref.Val {
				container := args[0].(traits.Container)
				key := args[1]
				orValue := args[2]
				if container.Contains(key) == types.True {
					return args[0].(traits.Indexer).Get(key)
				}
				return orValue
			}),
		),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	bindings, err := fn.Bindings()
	if err != nil {
		t.Fatalf("fn.Binding() failed: %v", err)
	}
	m := types.DefaultTypeAdapter.NativeToValue(map[string]string{"hello": "world"})
	out := bindings[0].Function(m, types.String("hello"), types.String("goodbye"))
	if out.Equal(types.String("world")) != types.True {
		t.Errorf("function got %v, wanted 'world'", out)
	}
	out = bindings[0].Function(m, types.String("missing"), types.String("goodbye"))
	if out.Equal(types.String("goodbye")) != types.True {
		t.Errorf("function got %v, wanted 'goodbye'", out)
	}
	noSuchKey := types.NewErr("no such key")
	out = bindings[0].Function(m, noSuchKey, types.String("goodbye"))
	if out != noSuchKey {
		t.Errorf("function got %v, wanted 'no such key'", out)
	}
}

func TestFunctionGetTypeParams(t *testing.T) {
	fn, err := NewFunction("deep_type_params",
		Overload("no_type_params", []*types.Type{}, types.DynType),
		Overload("one_type_param", []*types.Type{types.BoolType}, types.NewTypeParamType("K")),
		Overload("deep_type_params",
			[]*types.Type{types.NewTypeParamType("E1"),
				types.NewMapType(types.NewTypeParamType("K"), types.NewTypeParamType("V"))},
			types.NewTypeParamType("V"),
		),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if len(fn.OverloadDecls()) != 3 {
		t.Fatal("fn.OverloadDecls() not equal to 3")
	}
	o1 := fn.OverloadDecls()[0]
	o2 := fn.OverloadDecls()[1]
	o3 := fn.OverloadDecls()[2]
	if len(o1.TypeParams()) != 0 {
		t.Errorf("overload %v did not have zero type-params", o1)
	}
	if len(o2.TypeParams()) != 1 && !reflect.DeepEqual(o2.TypeParams(), []string{"K"}) {
		t.Errorf("overload %v did not have a single type param", o2)
	}
	if len(o3.TypeParams()) != 3 {
		t.Errorf("overload %v did not have three type params", o3)
	}
}

func TestFunctionDisableDeclaration(t *testing.T) {
	fn, err := NewFunction("in",
		DisableDeclaration(true),
		Overload("in_list",
			[]*types.Type{types.NewListType(types.NewTypeParamType("K")), types.NewTypeParamType("K")},
			types.BoolType,
		),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if !fn.IsDeclarationDisabled() {
		t.Error("got declaration enabled, wanted disabled")
	}
}

func TestFunctionEnableDeclaration(t *testing.T) {
	fn, err := NewFunction("in",
		DisableDeclaration(false),
		Overload("in_list",
			[]*types.Type{types.NewListType(types.NewTypeParamType("K")), types.NewTypeParamType("K")},
			types.BoolType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if fn.IsDeclarationDisabled() {
		t.Error("got declaration disabled, wanted enabled")
	}
	fn2, err := NewFunction("in",
		DisableDeclaration(true),
		Overload("in_list",
			[]*types.Type{types.NewListType(types.NewTypeParamType("K")), types.NewTypeParamType("K")},
			types.BoolType),
	)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	if !fn2.IsDeclarationDisabled() {
		t.Error("got declaration enabled, wanted disabled")
	}

	// enabled -> disabled
	merged, err := fn.Merge(fn2)
	if err != nil {
		t.Fatalf("fn.Merge(fn2) failed: %v", err)
	}
	if !merged.IsDeclarationDisabled() {
		t.Error("got declaration enabled, wanted disabled")
	}
	// disabled -> enabled
	merged2, err := fn2.Merge(fn)
	if err != nil {
		t.Fatalf("fn.Merge(fn2) failed: %v", err)
	}
	if merged2.IsDeclarationDisabled() {
		t.Error("got declaration disabled, wanted enabled")
	}
}

func TestFunctionDeclToExprDecl(t *testing.T) {
	tests := []struct {
		fn     *FunctionDecl
		exDecl *exprpb.Decl
	}{
		{
			fn: testFunction(t, "equals",
				Overload("equals_value_value",
					[]*types.Type{types.NewTypeParamType("T"), types.NewTypeParamType("T")}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId: "equals_value_value",
								Params: []*exprpb.Type{
									chkdecls.NewTypeParamType("T"),
									chkdecls.NewTypeParamType("T"),
								},
								TypeParams: []string{"T"},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		{
			fn: testFunction(t, "equals",
				MemberOverload("value_equals_value",
					[]*types.Type{types.NewTypeParamType("T"), types.NewTypeParamType("T")}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId:         "value_equals_value",
								IsInstanceFunction: true,
								Params: []*exprpb.Type{
									chkdecls.NewTypeParamType("T"),
									chkdecls.NewTypeParamType("T"),
								},
								TypeParams: []string{"T"},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		{
			fn: testFunction(t, "equals",
				Overload("equals_int_uint",
					[]*types.Type{types.IntType, types.UintType}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId: "equals_int_uint",
								Params: []*exprpb.Type{
									chkdecls.Int,
									chkdecls.Uint,
								},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		{
			fn: testFunction(t, "equals",
				MemberOverload("int_equals_uint",
					[]*types.Type{types.IntType, types.UintType}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId:         "int_equals_uint",
								IsInstanceFunction: true,
								Params: []*exprpb.Type{
									chkdecls.Int,
									chkdecls.Uint,
								},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		{
			fn: testFunction(t, "equals",
				MemberOverload("list_optional_value_equals_list_optional_value", []*types.Type{
					types.NewListType(types.NewOptionalType(types.NewTypeParamType("T"))),
					types.NewListType(types.NewOptionalType(types.NewTypeParamType("T"))),
				}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId:         "list_optional_value_equals_list_optional_value",
								IsInstanceFunction: true,
								Params: []*exprpb.Type{
									chkdecls.NewListType(chkdecls.NewOptionalType(chkdecls.NewTypeParamType("T"))),
									chkdecls.NewListType(chkdecls.NewOptionalType(chkdecls.NewTypeParamType("T"))),
								},
								TypeParams: []string{"T"},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		{
			fn: testFunction(t, "equals",
				MemberOverload("int_equals_uint",
					[]*types.Type{types.IntType, types.UintType}, types.BoolType),
				MemberOverload("uint_equals_int",
					[]*types.Type{types.UintType, types.IntType}, types.BoolType)),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId:         "int_equals_uint",
								IsInstanceFunction: true,
								Params: []*exprpb.Type{
									chkdecls.Int,
									chkdecls.Uint,
								},
								ResultType: chkdecls.Bool,
							},
							{
								OverloadId:         "uint_equals_int",
								IsInstanceFunction: true,
								Params: []*exprpb.Type{
									chkdecls.Uint,
									chkdecls.Int,
								},
								ResultType: chkdecls.Bool,
							},
						},
					},
				},
			},
		},
		// Test to make sure the overloads are in stable order: int_uint | uint_int | int_int | uint_uint
		{
			fn: testMerge(t,
				testFunction(t, "equals",
					FunctionDocs(`test equality between an int and uint only`),
					Overload("int_equals_uint", []*types.Type{types.IntType, types.UintType}, types.BoolType,
						OverloadExamples(`1 == 1u // true`)),
					Overload("uint_equals_int", []*types.Type{types.UintType, types.IntType}, types.BoolType,
						OverloadExamples(`1u == -1 // false`))),
				testFunction(t, "equals",
					FunctionDocs(`test equality between two int-like values`),
					Overload("int_equals_int", []*types.Type{types.IntType, types.IntType}, types.BoolType,
						OverloadExamples(`1 == 1 // true`, `1 == 2 // false`)),
					Overload("int_equals_uint", []*types.Type{types.IntType, types.UintType}, types.BoolType),
					Overload("uint_equals_uint", []*types.Type{types.UintType, types.UintType}, types.BoolType,
						OverloadExamples(`1u == 1u // true`)))),
			exDecl: &exprpb.Decl{
				Name: "equals",
				DeclKind: &exprpb.Decl_Function{
					Function: &exprpb.Decl_FunctionDecl{
						Overloads: []*exprpb.Decl_FunctionDecl_Overload{
							{
								OverloadId: "int_equals_uint",
								Params: []*exprpb.Type{
									chkdecls.Int,
									chkdecls.Uint,
								},
								ResultType: chkdecls.Bool,
								Doc:        "1 == 1u // true",
							},
							{
								OverloadId: "uint_equals_int",
								Params: []*exprpb.Type{
									chkdecls.Uint,
									chkdecls.Int,
								},
								ResultType: chkdecls.Bool,
								Doc:        "1u == -1 // false",
							},
							{
								OverloadId: "int_equals_int",
								Params: []*exprpb.Type{
									chkdecls.Int,
									chkdecls.Int,
								},
								ResultType: chkdecls.Bool,
								Doc:        "1 == 1 // true\n1 == 2 // false",
							},
							{
								OverloadId: "uint_equals_uint",
								Params: []*exprpb.Type{
									chkdecls.Uint,
									chkdecls.Uint,
								},
								ResultType: chkdecls.Bool,
								Doc:        "1u == 1u // true",
							},
						},
					},
				},
			},
		},
	}
	for _, tst := range tests {
		exDecl, err := FunctionDeclToExprDecl(tst.fn)
		if err != nil {
			t.Fatalf("FunctionDeclToExprDecl(%v) failed: %v", tst.fn, err)
		}
		if !proto.Equal(exDecl, tst.exDecl) {
			t.Errorf("got not equal, wanted %v == %v", exDecl, tst.exDecl)
		}
	}
}

func TestNewVariable(t *testing.T) {
	a := NewVariable("a", types.BoolType)
	if !a.DeclarationIsEquivalent(a) {
		t.Error("NewVariable(a, bool) does not equal itself")
	}
	if !a.DeclarationIsEquivalent(NewVariable("a", types.BoolType)) {
		t.Error("NewVariable(a, bool) does not equal itself")
	}
	a1 := NewVariable("a", types.IntType)
	if a.DeclarationIsEquivalent(a1) {
		t.Error("NewVariable(a, int).DeclarationEquals(NewVariable(a, bool))")
	}
}

func TestNewConstant(t *testing.T) {
	a := NewConstant("a", types.IntType, types.Int(42))
	if !a.DeclarationIsEquivalent(a) {
		t.Error("NewConstant(a, int) does not equal itself")
	}
	if !a.DeclarationIsEquivalent(NewVariable("a", types.IntType)) {
		t.Error("NewConstant(a, int) is not declaration equivalent to int variable")
	}
}

func TestTypeVariable(t *testing.T) {
	tests := []struct {
		t *types.Type
		v *VariableDecl
	}{
		{
			t: types.AnyType,
			v: NewVariable("google.protobuf.Any", types.NewTypeTypeWithParam(types.AnyType)),
		},
		{
			t: types.DynType,
			v: NewVariable("dyn", types.NewTypeTypeWithParam(types.DynType)),
		},
		{
			t: types.NewObjectType("google.protobuf.Int32Value"),
			v: NewVariable("int", types.NewTypeTypeWithParam(types.NewNullableType(types.IntType))),
		},
		{
			t: types.NewObjectType("google.protobuf.Int32Value"),
			v: NewVariable("int", types.NewTypeTypeWithParam(types.NewNullableType(types.IntType))),
		},
	}
	for _, tst := range tests {
		if !TypeVariable(tst.t).DeclarationIsEquivalent(tst.v) {
			t.Errorf("got not equal %v.Equals(%v)", TypeVariable(tst.t), tst.v)
		}
	}
}

func TestVariableDeclToExprDecl(t *testing.T) {
	a, err := VariableDeclToExprDecl(NewVariable("a", types.BoolType))
	if err != nil {
		t.Fatalf("VariableDeclToExprDecl() failed: %v", err)
	}
	if !proto.Equal(a, chkdecls.NewVar("a", chkdecls.Bool)) {
		t.Error("proto.Equal() returned false, wanted true")
	}

	docVar := NewVariableWithDoc("a", types.BoolType, "doc")
	a, err = VariableDeclToExprDecl(docVar)
	if err != nil {
		t.Fatalf("VariableDeclToExprDecl() failed: %v", err)
	}
	if docVar.Description() != a.GetIdent().GetDoc() {
		t.Errorf("docVar.Description() got %s, wanted %s", docVar.Description(), a.GetIdent().GetDoc())
	}
	if !proto.Equal(a, chkdecls.NewVarWithDoc("a", chkdecls.Bool, "doc")) {
		t.Error("proto.Equal() returned false, wanted true")
	}
}

func TestVariableDeclToExprDeclInvalid(t *testing.T) {
	out, err := VariableDeclToExprDecl(NewVariable("bad", &types.Type{}))
	if err == nil {
		t.Fatalf("VariableDeclToExprDecl() succeeded: %v, wanted error", out)
	}
}

func TestNilFunction(t *testing.T) {
	var f *FunctionDecl
	if f.Name() != "" {
		t.Errorf("f.Name() got %s, wanted ''", f.Name())
	}
	if f.Description() != "" {
		t.Errorf("f.Description() got %s, wanted ''", f.Description())
	}
	if f.Documentation() != nil {
		t.Errorf("f.Documentation() got %v, wanted nil", f.Documentation())
	}
	if !f.IsDeclarationDisabled() {
		t.Errorf("f.IsDeclarationDisabled() got %t, wanted true", f.IsDeclarationDisabled())
	}
	if len(f.OverloadDecls()) != 0 {
		t.Errorf("f.OverloadDecls() got %d overloads, wanted 0", len(f.OverloadDecls()))
	}
	if b, err := f.Bindings(); err != nil || len(b) != 0 {
		t.Error("f.Bindings() got non-empty result")
	}
	other := &FunctionDecl{}
	if fn, err := f.Merge(other); err == nil {
		t.Errorf("f.Merge(nil) wanted error, got %v", fn)
	}
	if err := f.AddOverload(nil); err == nil {
		t.Error("f.AddOverload(nil) did not error")
	}
	if err := other.AddOverload(nil); err == nil {
		t.Error("f.AddOverload(nil) did not error")
	}
}

func TestNilVariable(t *testing.T) {
	var v *VariableDecl
	if v.Name() != "" {
		t.Errorf("v.Name() got %s, wanted ''", v.Name())
	}
	if v.Type() != nil {
		t.Errorf("v.Type() got %v, wanted nil", v.Type())
	}
	if v.Value() != nil {
		t.Errorf("v.Type() got %v, wanted nil", v.Value())
	}
	if v.Description() != "" {
		t.Errorf("v.Description() got %s, wanted ''", v.Description())
	}
	if v.Documentation() != nil {
		t.Errorf("v.Documentation() got %v, wanted nil", v.Documentation())
	}
}

func testMerge(t *testing.T, funcs ...*FunctionDecl) *FunctionDecl {
	t.Helper()
	fn := funcs[0]
	var err error
	for i := 1; i < len(funcs); i++ {
		fn, err = fn.Merge(funcs[i])
		if err != nil {
			t.Fatalf("%v.Merge(%v) failed: %v", fn, funcs[i], err)
		}
	}
	return fn
}

func testFunction(t *testing.T, name string, opts ...FunctionOpt) *FunctionDecl {
	t.Helper()
	fn, err := NewFunction(name, opts...)
	if err != nil {
		t.Fatalf("NewFunction() failed: %v", err)
	}
	return fn
}
