// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

//go:build !root

package solvers

import (
	"context"
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/purpleidea/mgmt/engine"
	_ "github.com/purpleidea/mgmt/engine/resources" // import so the resources register
	engineUtil "github.com/purpleidea/mgmt/engine/util"
	"github.com/purpleidea/mgmt/lang/ast"
	"github.com/purpleidea/mgmt/lang/funcs/operators"
	"github.com/purpleidea/mgmt/lang/funcs/vars"
	"github.com/purpleidea/mgmt/lang/interfaces"
	"github.com/purpleidea/mgmt/lang/types"
	"github.com/purpleidea/mgmt/lang/unification"
	"github.com/purpleidea/mgmt/util"
)

func TestUnification1(t *testing.T) {
	type test struct { // an individual test
		name      string
		ast       interfaces.Stmt // raw AST
		fail      bool
		expect    map[interfaces.Expr]*types.Type
		experr    error  // expected error if fail == true (nil ignores it)
		experrstr string // expected error prefix
	}
	testCases := []test{}

	// this causes a panic, so it can't be used
	//{
	//	testCases = append(testCases, test{
	//		"nil",
	//		nil,
	//		true, // expect error
	//		nil,  // no AST
	//	})
	//}
	{
		expr := &ast.ExprStr{V: "hello"}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "str",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "one res",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				expr: types.TypeStr,
			},
		})
	}
	{
		v1 := &ast.ExprStr{}
		v2 := &ast.ExprStr{}
		v3 := &ast.ExprStr{}
		expr := &ast.ExprList{
			Elements: []interfaces.Expr{
				v1,
				v2,
				v3,
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "test"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "slicestring",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "list of strings",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				v1:   types.TypeStr,
				v2:   types.TypeStr,
				v3:   types.TypeStr,
				expr: types.TypeListStr,
			},
		})
	}
	{
		k1 := &ast.ExprInt{V: 1}
		k2 := &ast.ExprInt{V: 2}
		k3 := &ast.ExprInt{V: 3}
		v1 := &ast.ExprFloat{V: 1.0}
		v2 := &ast.ExprFloat{V: 2.0}
		v3 := &ast.ExprFloat{V: 3.0}
		expr := &ast.ExprMap{
			KVs: []*ast.ExprMapKV{
				{Key: k1, Val: v1},
				{Key: k2, Val: v2},
				{Key: k3, Val: v3},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "test"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "mapintfloat",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "map of int->float",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				k1:   types.TypeInt,
				k2:   types.TypeInt,
				k3:   types.TypeInt,
				v1:   types.TypeFloat,
				v2:   types.TypeFloat,
				v3:   types.TypeFloat,
				expr: types.NewType("map{int: float}"),
			},
		})
	}
	{
		b := &ast.ExprBool{}
		s := &ast.ExprStr{}
		i := &ast.ExprInt{}
		f := &ast.ExprFloat{}
		expr := &ast.ExprStruct{
			Fields: []*ast.ExprStructField{
				{Name: "somebool", Value: b},
				{Name: "somestr", Value: s},
				{Name: "someint", Value: i},
				{Name: "somefloat", Value: f},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "test"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "mixedstruct",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "simple struct",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				b:    types.TypeBool,
				s:    types.TypeStr,
				i:    types.TypeInt,
				f:    types.TypeFloat,
				expr: types.NewType("struct{somebool bool; somestr str; someint int; somefloat float}"),
			},
		})
	}
	{
		// test "n1" {
		//	int64ptr => 13 + 42,
		//}
		expr := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "+",
				},
				&ast.ExprInt{
					V: 13,
				},

				&ast.ExprInt{
					V: 42,
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{
						V: "n1",
					},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "int64ptr",
							Value: expr, // func
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "func call",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				expr: types.NewType("int"),
			},
		})
	}
	{
		//test "n1" {
		//	int64ptr => 13 + 42 - 4,
		//}
		innerFunc := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "-",
				},
				&ast.ExprInt{
					V: 42,
				},
				&ast.ExprInt{
					V: 4,
				},
			},
		}
		expr := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "+",
				},
				&ast.ExprInt{
					V: 13,
				},
				innerFunc, // nested func, can we unify?
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{
						V: "n1",
					},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "int64ptr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "func call, multiple ints",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				innerFunc: types.NewType("int"),
				expr:      types.NewType("int"),
			},
		})
	}
	{
		//test "n1" {
		//	float32 => -25.38789 + 32.6 + 13.7,
		//}
		innerFunc := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "+",
				},
				&ast.ExprFloat{
					V: 32.6,
				},
				&ast.ExprFloat{
					V: 13.7,
				},
			},
		}
		expr := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "+",
				},
				&ast.ExprFloat{
					V: -25.38789,
				},
				innerFunc, // nested func, can we unify?
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{
						V: "n1",
					},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "float32",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "func call, multiple floats",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				innerFunc: types.NewType("float"),
				expr:      types.NewType("float"),
			},
		})
	}
	{
		//$x = 42 - 13
		//test "t1" {
		//	int64 => $x,
		//}
		innerFunc := &ast.ExprCall{
			Name: operators.OperatorFuncName,
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "-",
				},
				&ast.ExprInt{
					V: 42,
				},
				&ast.ExprInt{
					V: 13,
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtBind{
					Ident: "x",
					Value: innerFunc,
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{
						V: "t1",
					},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "int64",
							Value: &ast.ExprVar{
								Name: "x",
							},
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "assign from func call or two ints",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				innerFunc: types.NewType("int"),
			},
		})
	}
	{
		//$x = golang.template("hello", 42)
		//test "t1" {
		//	anotherstr => $x,
		//}
		innerFunc := &ast.ExprCall{
			Name: "golang.template",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "hello",
				},
				&ast.ExprInt{
					V: 42,
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "golang",
				},
				&ast.StmtBind{
					Ident: "x",
					Value: innerFunc,
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{
						V: "t1",
					},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "anotherstr",
							Value: &ast.ExprVar{
								Name: "x",
							},
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "simple template",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				innerFunc: types.NewType("str"),
			},
		})
	}
	{
		// import "datetime"
		//test "t1" {
		//	stringptr => datetime.now(),	# bad (str vs. int)
		//}
		expr := &ast.ExprCall{
			Name: "datetime.now",
			Args: []interfaces.Expr{},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "datetime",
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "stringptr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "single func unification",
			ast:  stmt,
			fail: true,
		})
	}
	{
		//import "sys"
		//test "t1" {
		//	stringptr => sys.getenv("GOPATH", "bug"),	# bad (two args vs. one)
		//}
		expr := &ast.ExprCall{
			Name: "sys.getenv",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "GOPATH",
				},
				&ast.ExprStr{
					V: "bug",
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "sys",
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "stringptr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "function, wrong arg count",
			ast:  stmt,
			fail: true,
		})
	}
	// XXX: add these tests when we fix the bug!
	//{
	//	//import "fmt"
	//	//test "t1" {
	//	//	stringptr => fmt.printf("hello %s and %s", "one"),	# bad
	//	//}
	//	expr := &ast.ExprCall{
	//		Name: "fmt.printf",
	//		Args: []interfaces.Expr{
	//			&ast.ExprStr{
	//				V: "hello %s and %s",
	//			},
	//			&ast.ExprStr{
	//				V: "one",
	//			},
	//		},
	//	}
	//	stmt := &ast.StmtProg{
	//		Body: []interfaces.Stmt{
	//			&ast.StmtImport{
	//				Name: "fmt",
	//			},
	//			&ast.StmtRes{
	//				Kind: "test",
	//				Name: &ast.ExprStr{V: "t1"},
	//				Contents: []ast.StmtResContents{
	//					&ast.StmtResField{
	//						Field: "stringptr",
	//						Value: expr,
	//					},
	//				},
	//			},
	//		},
	//	}
	//	testCases = append(testCases, test{
	//		name: "function, missing arg for printf",
	//		ast:  stmt,
	//		fail: true,
	//	})
	//}
	//{
	//	//import "fmt"
	//	//test "t1" {
	//	//	stringptr => fmt.printf("hello %s and %s", "one", "two", "three"),	# bad
	//	//}
	//	expr := &ast.ExprCall{
	//		Name: "fmt.printf",
	//		Args: []interfaces.Expr{
	//			&ast.ExprStr{
	//				V: "hello %s and %s",
	//			},
	//			&ast.ExprStr{
	//				V: "one",
	//			},
	//			&ast.ExprStr{
	//				V: "two",
	//			},
	//			&ast.ExprStr{
	//				V: "three",
	//			},
	//		},
	//	}
	//	stmt := &ast.StmtProg{
	//		Body: []interfaces.Stmt{
	//			&ast.StmtImport{
	//				Name: "fmt",
	//			},
	//			&ast.StmtRes{
	//				Kind: "test",
	//				Name: &ast.ExprStr{V: "t1"},
	//				Contents: []ast.StmtResContents{
	//					&ast.StmtResField{
	//						Field: "stringptr",
	//						Value: expr,
	//					},
	//				},
	//			},
	//		},
	//	}
	//	testCases = append(testCases, test{
	//		name: "function, extra arg for printf",
	//		ast:  stmt,
	//		fail: true,
	//	})
	//}
	{
		//import "fmt"
		//test "t1" {
		//	stringptr => fmt.printf("hello %s and %s", "one", "two"),
		//}
		expr := &ast.ExprCall{
			Name: "fmt.printf",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "hello %s and %s",
				},
				&ast.ExprStr{
					V: "one",
				},
				&ast.ExprStr{
					V: "two",
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "fmt",
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "stringptr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name: "function, regular printf unification",
			ast:  stmt,
			fail: false,
			expect: map[interfaces.Expr]*types.Type{
				expr: types.NewType("str"),
			},
		})
	}
	{
		//import "fmt"
		//$x str = if true {	# should fail unification
		//	42
		//} else {
		//	13
		//}
		//test "t1" {
		//	stringptr => fmt.printf("hello %s", $x),
		//}
		cond := &ast.ExprIf{
			Condition:  &ast.ExprBool{V: true},
			ThenBranch: &ast.ExprInt{V: 42},
			ElseBranch: &ast.ExprInt{V: 13},
		}
		// setting this (wrong) type should later fail unification
		if err := cond.SetType(types.TypeStr); err != nil {
			t.Fatalf("func SetType: %v", err)
		}
		expr := &ast.ExprCall{
			Name: "fmt.printf",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "hello %s",
				},
				&ast.ExprVar{
					Name: "x", // the var
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "fmt",
				},
				&ast.StmtBind{
					Ident: "x", // the var
					Value: cond,
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "anotherstr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name:      "typed if expr",
			ast:       stmt,
			fail:      true,
			experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != int",
		})
	}
	{
		//import "fmt"
		//$w = true
		//$x str = $w	# should fail unification
		//test "t1" {
		//	stringptr => fmt.printf("hello %s", $x),
		//}
		wvar := &ast.ExprBool{V: true}
		xvar := &ast.ExprVar{Name: "w"}
		// setting this (wrong) type should later fail unification
		if err := xvar.SetType(types.TypeStr); err != nil {
			t.Fatalf("func SetType: %v", err)
		}
		expr := &ast.ExprCall{
			Name: "fmt.printf",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "hello %s",
				},
				&ast.ExprVar{
					Name: "x", // the var
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "fmt",
				},
				&ast.StmtBind{
					Ident: "w",
					Value: wvar,
				},
				&ast.StmtBind{
					Ident: "x", // the var
					Value: xvar,
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "anotherstr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name:      "typed var expr",
			ast:       stmt,
			fail:      true,
			experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != bool",
		})
	}
	{
		//import "fmt"
		//$w = true
		//$x str = $w	# should fail unification
		//test "t1" {
		//	stringptr => fmt.printf("hello %v", $x),
		//}
		wvar := &ast.ExprBool{V: true}
		xvar := &ast.ExprVar{Name: "w"}
		// setting this (wrong) type should later fail unification
		if err := xvar.SetType(types.TypeStr); err != nil {
			t.Fatalf("func SetType: %v", err)
		}
		expr := &ast.ExprCall{
			Name: "fmt.printf",
			Args: []interfaces.Expr{
				&ast.ExprStr{
					V: "hello %s",
				},
				&ast.ExprVar{
					Name: "x", // the var
				},
			},
		}
		stmt := &ast.StmtProg{
			Body: []interfaces.Stmt{
				&ast.StmtImport{
					Name: "fmt",
				},
				&ast.StmtBind{
					Ident: "w",
					Value: wvar,
				},
				&ast.StmtBind{
					Ident: "x", // the var
					Value: xvar,
				},
				&ast.StmtRes{
					Kind: "test",
					Name: &ast.ExprStr{V: "t1"},
					Contents: []ast.StmtResContents{
						&ast.StmtResField{
							Field: "anotherstr",
							Value: expr,
						},
					},
				},
			},
		}
		testCases = append(testCases, test{
			name:      "typed var expr again",
			ast:       stmt,
			fail:      true,
			experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != bool",
		})
	}

	names := []string{}
	for index, tc := range testCases { // run all the tests
		if tc.name == "" {
			t.Errorf("test #%d: not named", index)
			continue
		}
		if util.StrInList(tc.name, names) {
			t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
			continue
		}
		names = append(names, tc.name)
		t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
			xast, fail, expect, experr, experrstr := tc.ast, tc.fail, tc.expect, tc.experr, tc.experrstr

			//str := strings.NewReader(code)
			//xast, err := parser.LexParse(str)
			//if err != nil {
			//	t.Errorf("test #%d: lex/parse failed with: %+v", index, err)
			//	return
			//}
			// TODO: print out the AST's so that we can see the types
			t.Logf("\n\ntest #%d: AST (before): %+v\n", index, xast)

			data := &interfaces.Data{
				// TODO: add missing fields here if/when needed
				Metadata: &interfaces.Metadata{},

				Debug: testing.Verbose(), // set via the -test.v flag to `go test`
				Logf: func(format string, v ...interface{}) {
					t.Logf(fmt.Sprintf("test #%d", index)+": ast: "+format, v...)
				},
			}
			// some of this might happen *after* interpolate in SetScope or Unify...
			if err := xast.Init(data); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not init and validate AST: %+v", index, err)
				return
			}

			// skip interpolation in this test so that the node pointers
			// aren't changed and so we can compare directly to expected
			//astInterpolated, err := ast.Interpolate() // interpolate strings in ast
			//if err != nil {
			//	t.Errorf("test #%d: interpolate failed with: %+v", index, err)
			//	return
			//}
			//t.Logf("test #%d: astInterpolated: %+v", index, astInterpolated)

			variables := map[string]interfaces.Expr{
				"purpleidea": &ast.ExprStr{V: "hello world!"}, // james says hi
				//"hostname": &ast.ExprStr{V: obj.Hostname},
			}
			consts := ast.VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix!
			addback := vars.ConstNamespace + interfaces.ModuleSep        // add it back...
			var err error
			variables, err = ast.MergeExprMaps(variables, consts, addback)
			if err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: couldn't merge in consts: %+v", index, err)
				return
			}

			// top-level, built-in, initial global scope
			scope := &interfaces.Scope{
				Variables: variables,
				// all the built-in top-level, core functions enter here...
				Functions: ast.FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix
			}
			// propagate the scope down through the AST...
			if err := xast.SetScope(scope); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: set scope failed with: %+v", index, err)
				return
			}

			// apply type unification
			debug := testing.Verbose()
			logf := func(format string, v ...interface{}) {
				t.Logf(fmt.Sprintf("test #%d", index)+": unification: "+format, v...)
			}

			solver, err := unification.LookupDefault()
			if err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: solver lookup failed with: %+v", index, err)
				return
			}
			unifier := &unification.Unifier{
				AST:          xast,
				Solver:       solver,
				UnifiedState: types.NewUnifiedState(),
				Debug:        debug,
				Logf:         logf,
			}
			err = unifier.Unify(context.TODO())

			// TODO: print out the AST's so that we can see the types
			t.Logf("\n\ntest #%d: AST (after): %+v\n", index, xast)

			if !fail && err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: unification failed with: %+v", index, err)
				return
			}
			if fail && err == nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: unification passed, expected fail", index)
				return
			}
			if fail && experr != nil && err != experr { // test for specific error!
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected fail, got wrong error", index)
				t.Errorf("test #%d: got error: %+v", index, err)
				t.Errorf("test #%d: exp error: %+v", index, experr)
				return
			}

			if fail && err != nil {
				t.Logf("test #%d: err: %+v", index, err)
			}
			// test for specific error string!
			if fail && experrstr != "" && !strings.HasPrefix(err.Error(), experrstr) {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected fail, got wrong error", index)
				t.Errorf("test #%d: got error: %s", index, err.Error())
				t.Errorf("test #%d: exp error: %s", index, experrstr)
				return
			}

			if expect == nil { // test done early
				return
			}
			// TODO: do this in sorted order
			var failed bool
			for expr, exptyp := range expect {
				typ, err := expr.Type() // lookup type
				if err != nil {
					t.Errorf("test #%d: type lookup of %+v failed with: %+v", index, expr, err)
					failed = true
					break
				}

				if err := typ.Cmp(exptyp); err != nil {
					t.Errorf("test #%d: type cmp failed with: %+v", index, err)
					t.Logf("test #%d: got: %+v", index, typ)
					t.Logf("test #%d: exp: %+v", index, exptyp)
					failed = true
					break
				}
			}
			if failed {
				return
			}
		})
	}
}

func TestLangFieldNameToStructType1(t *testing.T) {
	for _, kind := range engine.RegisteredResourcesNames() {
		t.Logf("res.Kind(): %s", kind)

		res, err := engineUtil.LangFieldNameToStructType(kind)
		if err != nil {
			t.Errorf("error trying to inspect kind %s: %s", kind, err.Error())
			continue
		}
		if res == nil {
			t.Errorf("got nil when trying to inspect kind: %s", kind)
			continue
		}
	}
}

func TestLangFieldNameToStructType2(t *testing.T) {
	// This tests that we don't infinitely recurse inside of this function.
	k := "consul:kv"
	res, err := engineUtil.LangFieldNameToStructType(k)

	expected := map[string]*types.Type{
		"address": types.TypeStr,
		"key":     types.TypeStr,
		"scheme":  types.TypeStr,
		"token":   types.TypeStr,
		"value":   types.TypeStr,
	}
	if err != nil {
		t.Errorf("error trying to get the field name type map: %s", err.Error())
		return
	}
	if !reflect.DeepEqual(res, expected) {
		t.Errorf("unexpected result: %+v", res)
		return
	}
}
