// Copyright 2017 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package ast

import (
	"encoding/json"
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"reflect"
	"slices"
	"strings"
	"testing"

	"github.com/open-policy-agent/opa/v1/types"
	"github.com/open-policy-agent/opa/v1/util"
)

func TestCheckInference(t *testing.T) {
	builtinMap := make(map[string]*Builtin, len(BuiltinMap)+3)
	maps.Copy(builtinMap, BuiltinMap)

	// fake_builtin_1([str1,str2])
	builtinMap["fake_builtin_1"] = &Builtin{
		Name: "fake_builtin_1",
		Decl: types.NewFunction(
			nil,
			types.NewArray(
				[]types.Type{types.S, types.S}, nil,
			),
		),
	}

	// fake_builtin_2({"a":str1,"b":str2})
	builtinMap["fake_builtin_2"] = &Builtin{
		Name: "fake_builtin_2",
		Decl: types.NewFunction(
			nil,
			types.NewObject(
				[]*types.StaticProperty{
					{Key: "a", Value: types.S},
					{Key: "b", Value: types.S},
				}, nil,
			),
		),
	}

	// fake_builtin_3({str1,str2,...})
	builtinMap["fake_builtin_3"] = &Builtin{
		Name: "fake_builtin_3",
		Decl: types.NewFunction(
			nil,
			types.NewSet(types.S),
		),
	}

	tests := []struct {
		note     string
		query    string
		expected map[Var]types.Type
	}{
		{"trivial", `x = 1`, map[Var]types.Type{
			Var("x"): types.N,
		}},
		{"one-level", "y = 1; x = y", map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.N,
		}},
		{"two-level", "z = 1; y = z; x = y", map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.N,
			Var("z"): types.N,
		}},
		{"array-nested", "[x, 1] = [true, y]", map[Var]types.Type{
			Var("x"): types.B,
			Var("y"): types.N,
		}},
		{"array-transitive", "y = [[2], 1]; [[x], 1] = y", map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.NewArray(
				[]types.Type{
					types.NewArray([]types.Type{types.N}, nil),
					types.N,
				}, nil),
		}},
		{"array-embedded", `[1, "2", x] = data.foo`, map[Var]types.Type{
			Var("x"): types.A,
		}},
		{"object-nested", `{"a": "foo", "b": {"c": x}} = {"a": y, "b": {"c": 2}}`, map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.S,
		}},
		{"object-transitive", `y = {"a": "foo", "b": 2}; {"a": z, "b": x} = y`, map[Var]types.Type{
			Var("x"): types.N,
			Var("z"): types.S,
		}},
		{"object-embedded", `{"1": "2", "2": x} = data.foo`, map[Var]types.Type{
			Var("x"): types.A,
		}},
		{"object-numeric-key", `x = {1: 2}; y = 1; x[y]`, map[Var]types.Type{
			Var("x"): types.NewObject([]*types.StaticProperty{{Key: json.Number("1"), Value: types.N}}, nil),
			Var("y"): types.N,
		}},
		{"object-object-key", `x = {{{}: 1}: 1}`, map[Var]types.Type{
			Var("x"): types.NewObject(
				[]*types.StaticProperty{types.NewStaticProperty(
					map[string]any{
						"{}": json.Number("1"),
					},
					types.N,
				)},
				nil,
			),
		}},
		{"object-composite-ref-operand", `x = {{}: 1}; x[{}] = y`, map[Var]types.Type{
			Var("x"): types.NewObject(
				[]*types.StaticProperty{types.NewStaticProperty(
					map[string]any{},
					types.N,
				)},
				nil,
			),
			Var("y"): types.N,
		}},
		{"sets", `x = {1, 2}; y = {{"foo", 1}, x}`, map[Var]types.Type{
			Var("x"): types.NewSet(types.N),
			Var("y"): types.NewSet(
				types.NewAny(
					types.NewSet(
						types.NewAny(types.N, types.S),
					),
					types.NewSet(
						types.N,
					),
				),
			),
		}},
		{"sets-nested", `{"a", 1, 2} = {1,2,3}`, nil},
		{"sets-composite-ref-operand", `s = {[1, 2], [3, 4]}; s[[x, y]]`, map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.N,
			Var("s"): types.NewSet(types.NewArray([]types.Type{types.N, types.N}, nil)),
		}},
		{"empty-composites", `
				obj = {};
				arr = [];
				set = set();
				obj[i] = v1;
				arr[j] = v2;
				set[v3];
				obj = {"foo": "bar"};
				arr = [1];
				set = {1,2,3}
				`, map[Var]types.Type{
			Var("obj"): types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
			Var("i"):   types.A,
			Var("v1"):  types.A,
			Var("arr"): types.NewArray(nil, types.A),
			Var("j"):   types.N,
			Var("v2"):  types.A,
			Var("set"): types.NewSet(types.A),
			Var("v3"):  types.A,
		}},
		{"empty-composite-property", `
			obj = {};
			obj.foo = x;
			obj[i].foo = y
		`, map[Var]types.Type{
			Var("x"): types.A,
			Var("y"): types.A,
		}},
		{"local-reference", `
			a = [
				1,
				{
					"foo": [
						{"bar": null},
						-1,
						{"bar": true}
					]
				},
				3];

			x = a[1].foo[_].bar`, map[Var]types.Type{
			Var("x"): types.NewAny(types.Nl, types.B),
		}},
		{"local-reference-var", `

			a = [
				{
					"a": null,
					"b": {
						"foo": {
							"c": {1,},
						},
						"bar": {
							"c": {"hello",},
						},
					},
				},
				{
					"a": null,
					"b": {
						"foo": {
							"c": {1,},
						},
						"bar": {
							"c": {true,},
						},
					},
				},
			];
			x = a[i].b[j].c[k]
			`, map[Var]types.Type{
			Var("i"): types.N,
			Var("j"): types.S,
			Var("k"): types.NewAny(types.S, types.N, types.B),
			Var("x"): types.NewAny(types.S, types.N, types.B),
		}},
		{"local-reference-var-any", `
			a = [[], {}];
			a[_][i]
		`, map[Var]types.Type{
			Var("i"): types.A,
		}},
		{"local-reference-nested", `
			a = [["foo"], 0, {"bar": "baz"}, 2];
			b = [0,1,2,3];
			a[b[_]][k] = v
			`, map[Var]types.Type{
			Var("k"): types.NewAny(types.S, types.N),
		}},
		{"simple-built-in", "plus(1,2,x)", map[Var]types.Type{
			Var("x"): types.N,
		}},
		{"simple-built-in-exists", "plus(1,2,x); plus(x,2,y)", map[Var]types.Type{
			Var("x"): types.N,
			Var("y"): types.N,
		}},
		{"array-builtin", `fake_builtin_1([x,"foo"])`, map[Var]types.Type{
			Var("x"): types.S,
		}},
		{"object-builtin", `fake_builtin_2({"a": "foo", "b": x})`, map[Var]types.Type{
			Var("x"): types.S,
		}},
		{"set-builtin", `fake_builtin_3({"foo", x})`, map[Var]types.Type{
			Var("x"): types.S,
		}},
		{"array-comprehension-ref-closure", `a = [1,"foo",3]; x = [ i | a[_] = i ]`, map[Var]types.Type{
			Var("x"): types.NewArray(nil, types.NewAny(types.N, types.S)),
		}},
		{"array-comprehension-var-closure", `x = 1; y = [ i | x = i ]`, map[Var]types.Type{
			Var("y"): types.NewArray(nil, types.N),
		}},
		{"dynamic-object-value", `q = {"a": "b", "c": "d"}; {k: [v]} = {k: [q[k]]}`, map[Var]types.Type{
			Var("k"): types.S,
			Var("v"): types.A,
		}},
		{
			note:  "type unioning: arrays",
			query: `x = [[1], ["foo"]]; x[_] = [y]`,
			expected: map[Var]types.Type{
				Var("y"): types.NewAny(
					types.N, types.S,
				),
			},
		},
		{
			note:  "type unioning: sets",
			query: `x = {[1], ["foo"]}; x[[y]]`,
			expected: map[Var]types.Type{
				Var("y"): types.NewAny(
					types.N, types.S,
				),
			},
		},
		{
			note:  "type unioning: object values",
			query: `x = {"a": [1], "b": ["foo"]}; x[_] = [y]`,
			expected: map[Var]types.Type{
				Var("y"): types.NewAny(
					types.N, types.S,
				),
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			body := MustParseBody(tc.query)
			checker := newTypeChecker()
			env := checker.Env(builtinMap)
			env, err := checker.CheckBody(env, body)
			if len(err) != 0 {
				t.Fatalf("Unexpected error: %v", err)
			}
			for k, tpe := range tc.expected {
				result := env.Get(k)
				if tpe == nil {
					if result != nil {
						t.Errorf("Expected %v type to be unset but got: %v", k, result)
					}
				} else {
					if result == nil {
						t.Errorf("Expected to infer %v => %v but got nil", k, tpe)
					} else if types.Compare(tpe, result) != 0 {
						t.Errorf("Expected to infer %v => %v but got %v", k, tpe, result)
					}
				}
			}
		})
	}
}

func TestCheckInferenceRules(t *testing.T) {

	// Rules must have refs resolved, safe ordering, etc. Each pair is a
	// (package path, rule) tuple. The test constructs the Rule objects to
	// run the inference on from these inputs.
	ruleset1 := [][2]string{
		{`a`, `trivial = true { true }`},
		{`a`, `complete = [{"foo": x}] { x = 1 }`},
		{`a`, `partialset[{"foo": x}] { y = "bar"; x = y }`},
		{`a`, `partialobj[x] = {"foo": y} { y = "bar"; x = y }`},
		{`b`, `trivial_ref = x { x = data.a.trivial }`},
		{`b`, `transitive_ref = [x] { y = data.b.trivial_ref; x = y }`},
		{`c`, `else_kw = null { false } else = 100 { true } else = "foo" { true }`},
		{`iteration`, `arr = [[1], ["two"], {"x": true}, ["four"]] { true }`},
		{`iteration`, `values[x] { data.iteration.arr[_][_] = x } `},
		{`iteration`, `keys[i] { data.iteration.arr[_][i] = _ } `},
		{`disjunction`, `partialset[1] { true }`},
		{`disjunction`, `partialset[x] { x = "foo" }`},
		{`disjunction`, `partialset[3] { true }`},
		{`disjunction`, `partialobj[x] = y { y = "bar"; x = "foo" }`},
		{`disjunction`, `partialobj[x] = y { y = 100; x = "foo" }`},
		{`disjunction`, `complete = 1 { true }`},
		{`disjunction`, `complete = x { x = "foo" }`},
		{`prefix.a.b.c`, `d = true { true }`},
		{`prefix.i.j.k`, `p = 1 { true }`},
		{`prefix.i.j.k`, `p = "foo" { true }`},
		{`default_rule`, `default x = 1`},
		{`default_rule`, `x = "foo" { true }`},
		{`unknown_type`, `p = [x] { x = data.deadbeef }`},
		{`nested_ref`, `inner = {"a": 0, "b": "1"} { true }`},
		{`nested_ref`, `middle = [[1, true], ["foo", false]] { true }`},
		{`nested_ref`, `p = x { data.nested_ref.middle[data.nested_ref.inner.a][0] = x }`},
		{`number_key`, `q[x] = y { a = ["a", "b"]; y = a[x] }`},
		{`non_leaf`, `p[x] { data.prefix.i[x][_] }`},
	}
	ruleset2 := [][2]string{
		{`ref_rule_single`, `p.q.r { true }`},
		{`ref_rule_single_with_number_key`, `p.q[3] { true }`},
		{`ref_regression_array_key`,
			`walker[[p, v]] = o { l = input; walk(l, k); [p, v] = k; o = {} }`},
		{`overlap`, `p.q[r] = y { x = ["a", "b"]; y = x[r] }`},
		{`overlap`, `p.q.r = false { true }`},
		{`overlap`, `p.q.r = "false" { true }`},
		{`overlap`, `p.q[42] = 1337 { true }`},
		{`overlap`, `p.q2.a = input.a { true }`},
		{`overlap`, `p.q2[56] = input.a { true }`},
	}
	ruleset3 := [][2]string{
		{`simple`, `p.q[r][s] = 42 { x = ["a", "b"]; r = x[s] }`},
		{`mixed`, `p.q[r].s[t] = 42 { x = ["a", "b"]; r = x[t] }`},
		{`overrides`, `p.q[r] = "foo" { x = ["a", "b"]; r = x[_] }`},
		{`overrides`, `p.q.r[s] = 42 { x = ["a", "b"]; x[s] }`},
		{`overrides`, `p.q[r].s = true { x = [true, false]; r = x[_] }`},
		{`overrides_static`, `p.q[r].a = "foo" { r = "bar"; s = "baz" }`},
		{`overrides_static`, `p.q[r].b = 42 { r = "bar" }`},
		{`overrides_static`, `p.q[r].c = true { r = "bar" }`},
	}

	tests := []struct {
		note     string
		rules    [][2]string
		ref      string
		expected types.Type
	}{
		{"trivial", ruleset1, `data.a.trivial`, types.B},

		{"complete-doc", ruleset1, `data.a.complete`, types.NewArray(
			[]types.Type{types.NewObject(
				[]*types.StaticProperty{{
					Key: "foo", Value: types.N,
				}},
				nil,
			)},
			nil,
		)},

		{"complete-doc-suffix", ruleset1, `data.a.complete[0].foo`, types.N},

		{"else-kw", ruleset1, "data.c.else_kw", types.NewAny(types.Nl, types.N, types.S)},

		{"partial-set-doc", ruleset1, `data.a.partialset`, types.NewSet(
			types.NewObject(
				[]*types.StaticProperty{{
					Key: "foo", Value: types.S,
				}},
				nil,
			),
		)},

		{"partial-object-doc", ruleset1, "data.a.partialobj", types.NewObject(
			nil,
			types.NewDynamicProperty(types.S, types.NewObject(
				[]*types.StaticProperty{{
					Key: "foo", Value: types.S,
				}},
				nil,
			)),
		)},

		{"partial-object-doc-suffix", ruleset1, `data.a.partialobj.somekey.foo`, types.S},

		{"partial-object-doc-number-suffix", ruleset1, "data.number_key.q[1]", types.S},

		{"iteration", ruleset1, "data.iteration.values", types.NewSet(
			types.NewAny(
				types.S,
				types.N,
				types.B),
		)},

		{"iteration-keys", ruleset1, "data.iteration.keys", types.NewSet(
			types.NewAny(
				types.S,
				types.N,
			),
		)},

		{"disj-complete-doc", ruleset1, "data.disjunction.complete", types.NewAny(
			types.S,
			types.N,
		)},

		{"disj-partial-set-doc", ruleset1, "data.disjunction.partialset", types.NewSet(
			types.NewAny(
				types.S,
				types.N),
		)},

		{"disj-partial-obj-doc", ruleset1, "data.disjunction.partialobj", types.NewObject(
			nil,
			types.NewDynamicProperty(types.S, types.NewAny(types.S, types.N)),
		)},

		{"ref", ruleset1, "data.b.trivial_ref", types.B},

		{"ref-transitive", ruleset1, "data.b.transitive_ref", types.NewArray(
			[]types.Type{
				types.B,
			},
			nil,
		)},

		{"prefix", ruleset1, `data.prefix.a.b`, types.NewObject(
			[]*types.StaticProperty{{
				Key: "c", Value: types.NewObject(
					[]*types.StaticProperty{{Key: "d", Value: types.B}},
					types.NewDynamicProperty(types.S, types.A),
				),
			}},
			types.NewDynamicProperty(types.S, types.A),
		)},

		// Check that prefixes that iterate fallback to any.
		{"prefix-iter", ruleset1, `data.prefix.i.j[k]`, types.A},

		// Check that iteration targeting a rule (but nonetheless prefixed) falls back to any.
		{"prefix-iter-2", ruleset1, `data.prefix.i.j[k].p`, types.A},

		{"default-rule", ruleset1, "data.default_rule.x", types.NewAny(
			types.S,
			types.N,
		)},

		{"unknown-type", ruleset1, "data.unknown_type.p", types.NewArray(
			[]types.Type{
				types.A,
			},
			nil,
		)},

		{"nested-ref", ruleset1, "data.nested_ref.p", types.NewAny(
			types.S,
			types.N,
		)},

		{"non-leaf", ruleset1, "data.non_leaf.p", types.NewSet(
			types.S,
		)},

		{"ref-rules single value, full ref", ruleset2, "data.ref_rule_single.p.q.r", types.B},
		{"ref-rules single value, prefix", ruleset2, "data.ref_rule_single.p",
			types.NewObject(
				[]*types.StaticProperty{{
					Key: "q", Value: types.NewObject(
						[]*types.StaticProperty{{Key: "r", Value: types.B}},
						types.NewDynamicProperty(types.S, types.A),
					),
				}},
				types.NewDynamicProperty(types.S, types.A),
			)},

		{"ref-rules single value, number key, full ref", ruleset2, "data.ref_rule_single_with_number_key.p.q[3]", types.B},
		{"ref-rules single value, number key, prefix", ruleset2, "data.ref_rule_single_with_number_key.p",
			types.NewObject(
				[]*types.StaticProperty{{
					Key: "q", Value: types.NewObject(
						[]*types.StaticProperty{{Key: json.Number("3"), Value: types.B}},
						types.NewDynamicProperty(types.S, types.A),
					),
				}},
				types.NewDynamicProperty(types.S, types.A),
			)},

		{"ref_regression_array_key", ruleset2, "data.ref_regression_array_key.walker",
			types.NewObject(
				nil,
				types.NewDynamicProperty(types.NewArray([]types.Type{types.NewArray(types.NewAny(), types.A), types.A}, nil),
					types.NewObject(nil, types.NewDynamicProperty(types.A, types.A))),
			)},
		{
			note:     "ref-rules single value, full ref to known leaf",
			rules:    ruleset2,
			ref:      "data.overlap.p.q.r",
			expected: types.NewAny(types.B, types.S),
		},
		{
			note:     "ref-rules single value, full ref to known leaf (same key type as dynamic, different value type)",
			rules:    ruleset2,
			ref:      "data.overlap.p.q[42]",
			expected: types.N,
		},
		{
			note:     "ref-rules single value, full ref to known leaf (any type)",
			rules:    ruleset2,
			ref:      "data.overlap.p.q2.a",
			expected: types.A,
		},
		{
			note:     "ref-rules single value, full ref to known leaf (same key type as dynamic, any type)",
			rules:    ruleset2,
			ref:      "data.overlap.p.q2[56]",
			expected: types.A,
		},
		{
			note:     "ref-rules single value, full ref to dynamic leaf",
			rules:    ruleset2,
			ref:      "data.overlap.p.q[1]",
			expected: types.Any{types.B, types.N, types.S}, // key type cannot be tied to specific dynamic value type, so we get all of them
		},
		{
			note:  "ref-rules single value, prefix ref to partial object root",
			rules: ruleset2,
			ref:   "data.overlap.p.q",
			expected: types.NewObject(
				nil,
				types.NewDynamicProperty(types.Any{types.N, types.S}, types.Any{types.B, types.N, types.S}),
			),
		},
		{
			note:  "general ref-rules, only vars in obj-path, complete obj access",
			rules: ruleset3,
			ref:   "data.simple.p.q",
			expected: types.NewObject(
				[]*types.StaticProperty{},
				types.NewDynamicProperty(types.S,
					types.NewObject(
						[]*types.StaticProperty{},
						types.NewDynamicProperty(types.N, types.N),
					),
				),
			),
		},
		{
			note:  "general ref-rules, only vars in obj-path, intermediate obj access",
			rules: ruleset3,
			ref:   "data.simple.p.q.b",
			expected: types.NewObject(
				[]*types.StaticProperty{},
				types.NewDynamicProperty(types.N, types.N),
			),
		},
		{
			note:     "general ref-rules, only vars in obj-path, leaf access",
			rules:    ruleset3,
			ref:      "data.simple.p.q.b[1]",
			expected: types.N,
		},
		{
			note:  "general ref-rules, vars and constants in obj-path, complete obj access",
			rules: ruleset3,
			ref:   "data.mixed.p.q",
			expected: types.NewObject(
				[]*types.StaticProperty{},
				types.NewDynamicProperty(types.S,
					types.NewObject(nil,
						types.NewDynamicProperty(types.S, types.NewObject(nil,
							types.NewDynamicProperty(types.N, types.N))),
					),
				),
			),
		},
		{
			note:  "general ref-rules, key overrides, complete obj access",
			rules: ruleset3,
			ref:   "data.overrides.p.q",
			expected: types.NewObject(nil, types.NewDynamicProperty(
				types.Or(types.B, types.S),
				types.Any{
					types.S,
					types.NewObject(nil, types.NewDynamicProperty(
						types.Any{types.N, types.S},
						types.Any{types.B, types.N})),
				},
			),
			),
		},
		{
			note:  "general ref-rules, multiple static key overrides, complete obj access",
			rules: ruleset3,
			ref:   "data.overrides_static.p.q",
			expected: types.NewObject(
				[]*types.StaticProperty{},
				types.NewDynamicProperty(types.S,
					types.NewObject(
						nil,
						types.NewDynamicProperty(types.S, types.Any{types.B, types.N, types.S}),
					),
				),
			),
		},
		{
			note:  "general ref-rules, multiple static key overrides, intermediate obj access",
			rules: ruleset3,
			ref:   "data.overrides_static.p.q.foo",
			expected: types.NewObject(nil,
				types.NewDynamicProperty(types.S, types.Any{types.B, types.N, types.S}),
			),
		},
		{
			note:     "general ref-rules, multiple static key overrides, leaf access (a)",
			rules:    ruleset3,
			ref:      "data.overrides_static.p.q.foo.a",
			expected: types.Any{types.B, types.N, types.S}, // Dynamically build object types don't have static properties, so even though we "know" the 'a' key has a string value, we've lost this information.
		},
		{
			note:     "general ref-rules, multiple static key overrides, leaf access (b)",
			rules:    ruleset3,
			ref:      "data.overrides_static.p.q.bar.b",
			expected: types.Any{types.B, types.N, types.S},
		},
		{
			note:     "general ref-rules, multiple static key overrides, leaf access (c)",
			rules:    ruleset3,
			ref:      "data.overrides_static.p.q.baz.c",
			expected: types.Any{types.B, types.N, types.S},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			var elems []util.T

			// Convert test rules into rule slice for call.
			for i := range tc.rules {
				pkg := MustParsePackage(`package ` + tc.rules[i][0])
				rule := MustParseRule(tc.rules[i][1])
				module := &Module{
					Package: pkg,
					Rules:   []*Rule{rule},
				}
				rule.Module = module
				elems = append(elems, rule)
				for next := rule.Else; next != nil; next = next.Else {
					next.Module = module
					elems = append(elems, next)
				}
			}

			ref := MustParseRef(tc.ref)
			checker := newTypeChecker()
			env, err := checker.CheckTypes(newTypeChecker().Env(map[string]*Builtin{"walk": BuiltinMap["walk"]}), elems, nil)

			if err != nil {
				t.Fatalf("Unexpected error %v:", err)
			}

			result := env.Get(ref)
			if tc.expected == nil {
				if result != nil {
					t.Errorf("Expected %v type to be unset but got: %v", ref, result)
				}
			} else {
				if result == nil {
					t.Errorf("Expected to infer %v => %v but got nil", ref, tc.expected)
				} else if types.Compare(tc.expected, result) != 0 {
					t.Errorf("Expected to infer %v => %v but got %v", ref, tc.expected, result)
				}
			}
		})
	}

}

func TestCheckInferenceOverlapWithRules(t *testing.T) {
	ruleset1 := [][2]string{
		{`prefix.i.j.k`, `p = 1 { true }`},
		{`prefix.i.j.k`, `p = "foo" { true }`},
	}
	tests := []struct {
		note     string
		rules    [][2]string
		ref      string
		expected types.Type // ref's type
		query    string
		extra    map[Var]types.Type
	}{
		{
			note:     "non-leaf, extra vars",
			rules:    ruleset1,
			ref:      "data.prefix.i.j[k]",
			expected: types.A,
			query:    "data.prefix.i.j[k][b]",
			extra: map[Var]types.Type{
				Var("k"): types.S,
				Var("b"): types.S,
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			var elems []util.T

			// Convert test rules into rule slice for "warmup" call.
			for i := range tc.rules {
				pkg := MustParsePackage(`package ` + tc.rules[i][0])
				rule := MustParseRule(tc.rules[i][1])
				module := &Module{
					Package: pkg,
					Rules:   []*Rule{rule},
				}
				rule.Module = module
				elems = append(elems, rule)
				for next := rule.Else; next != nil; next = next.Else {
					next.Module = module
					elems = append(elems, next)
				}
			}

			ref := MustParseRef(tc.ref)
			checker := newTypeChecker()
			env, err := checker.CheckTypes(nil, elems, nil)
			if err != nil {
				t.Fatalf("Unexpected error %v:", err)
			}

			result := env.Get(ref)
			if tc.expected == nil {
				if result != nil {
					t.Errorf("Expected %v type to be unset but got: %v", ref, result)
				}
			} else {
				if result == nil {
					t.Errorf("Expected to infer %v => %v but got nil", ref, tc.expected)
				} else if types.Compare(tc.expected, result) != 0 {
					t.Errorf("Expected to infer %v => %v but got %v", ref, tc.expected, result)
				}
			}

			body := MustParseBody(tc.query)
			env, err = checker.CheckBody(env, body)
			if len(err) != 0 {
				t.Fatalf("Unexpected error: %v", err)
			}
			for ex, exp := range tc.extra {
				act := env.Get(ex)
				if types.Compare(act, exp) != 0 {
					t.Errorf("Expected to infer extra %v => %v but got %v", ex, exp, act)
				}
			}
		})
	}
}

func TestCheckErrorSuppression(t *testing.T) {

	query := `arr = [1,2,3]; arr[0].deadbeef = 1`

	_, errs := newTypeChecker().CheckBody(nil, MustParseBody(query))
	if len(errs) != 1 {
		t.Fatalf("Expected exactly one error but got: %v", errs)
	}

	_, ok := errs[0].Details.(*RefErrUnsupportedDetail)
	if !ok {
		t.Fatalf("Expected ref error but got: %v", errs)
	}

	query = `_ = [true | count(1)]`

	_, errs = newTypeChecker().CheckBody(newTypeChecker().Env(BuiltinMap), MustParseBody(query))
	if len(errs) != 1 {
		t.Fatalf("Expected exactly one error but got: %v", errs)
	}

	_, ok = errs[0].Details.(*ArgErrDetail)
	if !ok {
		t.Fatalf("Expected arg error but got: %v", errs)
	}

}

func TestCheckBadCardinality(t *testing.T) {
	tests := []struct {
		body string
		exp  []types.Type
	}{
		{
			body: "plus(1)",
			exp:  []types.Type{types.N},
		},
		{
			body: "plus(1, 2, 3, 4)",
			exp:  []types.Type{types.N, types.N, types.N, types.N},
		},
	}
	for _, test := range tests {
		body := MustParseBody(test.body)
		tc := newTypeChecker()
		env := tc.Env(BuiltinMap)
		_, err := tc.CheckBody(env, body)
		if len(err) != 1 || err[0].Code != TypeErr {
			t.Fatalf("Expected 1 type error from %v but got: %v", body, err)
		}
		detail, ok := err[0].Details.(*ArgErrDetail)
		if !ok {
			t.Fatalf("Expected argument error details but got: %v", err)
		}
		if len(test.exp) != len(detail.Have) {
			t.Fatalf("Expected arg types %v but got: %v", test.exp, detail.Have)
		}
		for i := range test.exp {
			if types.Compare(test.exp[i], detail.Have[i]) != 0 {
				t.Fatalf("Expected types for %v to be %v but got: %v", body[0], test.exp, detail.Have)
			}
		}
	}
}

func TestCheckMatchErrors(t *testing.T) {
	tests := []struct {
		note  string
		query string
	}{
		{"null", "null = true"},
		{"boolean", "true = null"},
		{"number", "1 = null"},
		{"string", `"hello" = null`},
		{"array", "[1,2,3] = null"},
		{"array-nested", `[1,2,3] = [1,2,"3"]`},
		{"array-nested-2", `[1,2] = [1,2,3]`},
		{"array-dynamic", `[ true | true ] = [x | a = [1, "foo"]; x = a[_]]`},
		{"object", `{"a": 1, "b": 2} = null`},
		{"object-nested", `{"a": 1, "b": "2"} = {"a": 1, "b": 2}`},
		{"object-nested-2", `{"a": 1} = {"a": 1, "b": "2"}`},
		{"set", "{1,2,3} = null"},
		{"any", `x = ["str", 1]; x[_] = null`},
	}
	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			body := MustParseBody(tc.query)
			checker := newTypeChecker()
			_, err := checker.CheckBody(nil, body)
			if len(err) != 1 {
				t.Fatalf("Expected exactly one error from %v, but got:\n%v", body, err)
			}
		})
	}
}

func TestCheckBuiltinErrors(t *testing.T) {
	builtinMap := make(map[string]*Builtin, len(BuiltinMap)+3)
	maps.Copy(builtinMap, BuiltinMap)

	builtinMap["fake_builtin_2"] = &Builtin{
		Name: "fake_builtin_2",
		Decl: types.NewFunction(
			types.Args(
				types.NewAny(types.NewObject(
					[]*types.StaticProperty{
						{Key: "a", Value: types.S},
						{Key: "b", Value: types.S},
					}, nil),
				),
			),
			types.NewObject(
				[]*types.StaticProperty{
					{Key: "b", Value: types.S},
					{Key: "c", Value: types.S},
				}, nil,
			),
		),
	}

	tests := []struct {
		note  string
		query string
	}{
		{"trivial", "plus(true, 1, x)"},
		{"refs", "x = [null]; plus(x[0], 1, y)"},
		{"array comprehensions", `sum([null | true], x)`},
		{"arrays-any", `sum([1,2,"3",4], x)`},
		{"arrays-bad-input", `contains([1,2,3], "x")`},
		{"objects-any", `fake_builtin_2({"a": a, "c": c})`},
		{"objects-bad-input", `sum({"a": 1, "b": 2}, x)`},
		{"sets-any", `sum({1,2,"3",4}, x)`},
		{"virtual-ref", `plus(data.test.p, data.coffee, 0)`},
	}

	env := newTestEnv([]string{
		`p = "foo" { true }`,
		`f(x) = x { true }`,
	}, builtinMap)

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			body := MustParseBody(tc.query)
			checker := newTypeChecker()
			_, err := checker.CheckBody(env, body)
			if len(err) != 1 {
				t.Fatalf("Expected exactly one error from %v but got:\n%v", body, err)
			}
		})
	}
}

func TestVoidBuiltins(t *testing.T) {
	builtinMap := make(map[string]*Builtin, len(BuiltinMap)+1)
	maps.Copy(builtinMap, BuiltinMap)

	// Void builtin is used in test cases.
	builtinMap["fake_void_builtin"] = &Builtin{
		Name: "fake_void_builtin",
		Decl: types.NewFunction(
			types.Args(types.N),
			nil,
		),
	}

	tests := []struct {
		query   string
		wantErr bool
	}{
		{"fake_void_builtin(1)", false},
		{"fake_void_builtin()", true},
		{"fake_void_builtin(1,2)", true},
		{"fake_void_builtin(true)", true},
	}

	for _, tc := range tests {
		body := MustParseBody(tc.query)
		checker := newTypeChecker()
		_, errs := checker.CheckBody(newTestEnv(nil, builtinMap), body)
		if len(errs) != 0 && !tc.wantErr {
			t.Fatal(errs)
		} else if len(errs) == 0 && tc.wantErr {
			t.Fatal("Expected error")
		}
	}
}

func TestVariadicBuiltins(t *testing.T) {

	// Ensure type checking allows variadic arguments.
	env := newTypeChecker().Env(map[string]*Builtin{
		"println": {
			Name: "println",
			Decl: types.NewVariadicFunction([]types.Type{}, types.A, nil),
		},
	})

	_, errs := newTypeChecker().CheckBody(env, MustParseBody(`println("hello", "world")`))
	if len(errs) != 0 {
		t.Fatal(errs)
	}

	// Test error checking on positional arguments on a variadic function.
	env = newTypeChecker().Env(map[string]*Builtin{
		"println": {
			Name: "println",
			Decl: types.NewVariadicFunction([]types.Type{types.N}, types.A, nil),
		},
	})

	_, errs = newTypeChecker().CheckBody(env, MustParseBody(`println("hello", 7)`))
	if len(errs) != 1 {
		t.Fatal("expected one error but got:", errs)
	}

	detail := errs[0].Details.(*ArgErrDetail)

	if len(detail.Have) != 2 || len(detail.Want.Args) != 1 || types.Compare(detail.Want.Args[0], types.N) != 0 {
		t.Fatal("unexpected detail:", detail)
	}

	// Test error checking on variadic arguments.
	env = newTypeChecker().Env(map[string]*Builtin{
		"println": {
			Name: "println",
			Decl: types.NewVariadicFunction([]types.Type{types.N}, types.N, nil),
		},
	})

	_, errs = newTypeChecker().CheckBody(env, MustParseBody(`println(7, "world")`))
	if len(errs) != 1 {
		t.Fatal("expected one error but got:", errs)
	}

	detail = errs[0].Details.(*ArgErrDetail)

	if len(detail.Have) != 2 || len(detail.Want.Args) != 1 || types.Compare(detail.Want.Args[0], types.N) != 0 || types.Compare(detail.Want.Variadic, types.N) != 0 {
		t.Fatal("unexpected detail:", detail)
	}

}

func TestCheckRefErrUnsupported(t *testing.T) {

	query := `arr = [[1,2],[3,4]]; arr[1][0].deadbeef`

	_, errs := newTypeChecker().CheckBody(nil, MustParseBody(query))
	if len(errs) != 1 {
		t.Fatalf("Expected exactly one error but got: %v", errs)
	}

	details, ok := errs[0].Details.(*RefErrUnsupportedDetail)
	if !ok {
		t.Fatalf("Expected ref err unsupported but got: %v", errs)
	}

	wantRef := MustParseRef(`arr[1][0].deadbeef`)
	wantPos := 2
	wantHave := types.N

	if !wantRef.Equal(details.Ref) ||
		wantPos != details.Pos ||
		types.Compare(wantHave, details.Have) != 0 {
		t.Fatalf("Expected (%v, %v, %v) but got: (%v, %v, %v)", wantRef, wantPos, wantHave, details.Ref, details.Pos, details.Have)
	}

}

func TestCheckRefErrInvalid(t *testing.T) {

	env := newTestEnv([]string{
		`p { true }`,
		`q = {"foo": 1, "bar": 2} { true }`,
		`a.b.c[3] = x { x = {"x": {"y": 2}} }`,
	}, BuiltinMap)

	tests := []struct {
		note  string
		query string
		ref   string
		pos   int
		have  types.Type
		want  types.Type
		oneOf []Value
	}{
		{
			note:  "bad non-leaf var",
			query: `x = 1; data.test[x]`,
			ref:   `data.test[x]`,
			pos:   2,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("a"), String("p"), String("q")},
		},
		{
			note:  "bad non-leaf ref",
			query: `arr = [1]; data.test[arr[0]]`,
			ref:   `data.test[arr[0]]`,
			pos:   2,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("a"), String("p"), String("q")},
		},
		{
			note:  "bad leaf ref",
			query: `arr = [1]; data.test.q[arr[0]]`,
			ref:   `data.test.q[arr[0]]`,
			pos:   3,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("bar"), String("foo")},
		},
		{
			note:  "bad ref hitting last term",
			query: `x = true; data.test.a.b.c[x][_]`,
			ref:   `data.test.a.b.c[x][_]`,
			pos:   5,
			have:  types.B,
			want:  types.Any{types.N, types.S},
			oneOf: []Value{Number("3")},
		},
		{
			note:  "bad ref hitting dynamic part",
			query: `s = true; data.test.a.b.c[3].x[s][_] = _`,
			ref:   `data.test.a.b.c[3].x[s][_]`,
			pos:   7,
			have:  types.B,
			want:  types.S,
			oneOf: []Value{String("y")},
		},
		{
			note:  "bad leaf var",
			query: `x = 1; data.test.q[x]`,
			ref:   `data.test.q[x]`,
			pos:   3,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("bar"), String("foo")},
		},
		{
			note:  "bad array index value",
			query: "arr = [[1,2],[3],[4]]; arr[0].dead.beef = x",
			ref:   "arr[0].dead.beef",
			pos:   2,
			want:  types.N,
		},
		{
			note:  "bad set element value",
			query: `s = {{1,2},{3,4}}; x = {1,2}; s[x].deadbeef`,
			ref:   "s[x].deadbeef",
			pos:   2,
			want:  types.N,
		},
		{
			note:  "bad object key value",
			query: `arr = [{"a": 1, "c": 3}, {"b": 2}]; arr[0].b`,
			ref:   "arr[0].b",
			pos:   2,
			want:  types.S,
			oneOf: []Value{String("a"), String("c")},
		},
		{
			// NOTE(sr): Thins one and the next are special: it cannot work with ref heads, either, since we need at
			// least ONE string term after data.test: a module needs a package line, and the shortest head ref
			// possible is thus data.x.y.
			note:  "bad non-leaf value",
			query: `data.test[1]`,
			ref:   "data.test[1]",
			pos:   2,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("a"), String("p"), String("q")},
		},
		{
			note:  "bad non-leaf value (package)", // See note above ^^
			query: `data[1]`,
			ref:   "data[1]",
			pos:   1,
			have:  types.N,
			want:  types.S,
			oneOf: []Value{String("test")},
		},
		{
			note:  "composite ref operand",
			query: `data.test.q[[1, 2]]`,
			ref:   "data.test.q[[1, 2]]",
			pos:   3,
			have:  types.NewArray([]types.Type{types.N, types.N}, nil),
			want:  types.S,
		},
		{
			note:  "composite ref type error 1",
			query: `a = {[1], [2], [3]}; a[["foo"]]`,
			ref:   `a[["foo"]]`,
			pos:   1,
			have:  types.NewArray([]types.Type{types.S}, nil),
			want:  types.NewArray([]types.Type{types.N}, nil),
		},
		{
			note:  "composite ref type error 2",
			query: `a = {{"a": 2}}; a[{"a": "foo"}]`,
			ref:   `a[{"a": "foo"}]`,
			pos:   1,
			have:  types.NewObject([]*types.StaticProperty{types.NewStaticProperty("a", types.S)}, nil),
			want:  types.NewObject([]*types.StaticProperty{types.NewStaticProperty("a", types.N)}, nil),
		},
		{
			note:  "composite ref type error 3 - array",
			query: `a = [1,2,3]; a[{}] = b`,
			ref:   `a[{}]`,
			pos:   1,
			have:  types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
			want:  types.N,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {

			_, errs := newTypeChecker().CheckBody(env, MustParseBody(tc.query))
			if len(errs) != 1 {
				t.Fatalf("Expected exactly one error but got: %v", errs)
			}

			details, ok := errs[0].Details.(*RefErrInvalidDetail)
			if !ok {
				t.Fatalf("Expected ref error invalid but got: %v", errs)
			}

			wantRef := MustParseRef(tc.ref)

			if details.Pos != tc.pos ||
				!details.Ref.Equal(wantRef) ||
				types.Compare(details.Want, tc.want) != 0 ||
				types.Compare(details.Have, tc.have) != 0 ||
				!reflect.DeepEqual(details.OneOf, tc.oneOf) {
				t.Fatalf("Expected (%v, %v, %v, %v, %v) but got: (%v, %v, %v, %v, %v)", wantRef, tc.pos, tc.have, tc.want, tc.oneOf, details.Ref, details.Pos, details.Have, details.Want, details.OneOf)
			}
		})
	}
}

func TestFunctionsTypeInference(t *testing.T) {
	functions := []string{
		`foo([a, b]) = y if { split(a, b, y) }`,
		`bar(x) = y if { count(x, y) }`,
		`baz([x, y]) = z if { sprintf("%s%s", [x, y], z) }`,
		`qux({"bar": x, "foo": y}) = {a: b} if { upper(y, a); json.unmarshal(x, b) }`,
		`corge(x) = y if { qux({"bar": x, "foo": x}, a); baz([a["{5: true}"], "BUZ"], y) }`,
	}
	body := strings.Join(functions, "\n")
	base := "package base\n" + body

	popts := ParserOptions{AllFutureKeywords: true}

	c := NewCompiler()
	if c.Compile(map[string]*Module{"base": MustParseModuleWithOpts(base, popts)}); c.Failed() {
		t.Fatalf("Failed to compile base module: %v", c.Errors)
	}

	tests := []struct {
		body    string
		wantErr bool
	}{
		{
			`fn(_) = y if { data.base.foo(["hello", 5], y) }`,
			true,
		},
		{
			`fn(_) = y if { data.base.foo(["hello", "ll"], y) }`,
			false,
		},
		{
			`fn(_) = y if { data.base.baz(["hello", "ll"], y) }`,
			false,
		},
		{
			`fn(_) = y if { data.base.baz([5, ["foo", "bar", true]], y) }`,
			false,
		},
		{
			`fn(_) = y if { data.base.baz(["hello", {"a": "b", "c": 3}], y) }`,
			false,
		},
		{
			`fn(_) = y if { data.base.corge("this is not json", y) }`,
			false,
		},
		{
			`fn(x) = y if { data.non_existent(x, a); y = a[0] }`,
			true,
		},
		{
			`fn(x) = y if { y = [x] }`,
			false,
		},
		{
			`f(x) = y if { [x] = y }`,
			false,
		},
		{
			`fn(x) = y if { y = {"k": x} }`,
			false,
		},
		{
			`f(x) = y if { {"k": x} = y }`,
			false,
		},
		{
			`p if { [data.base.foo] }`,
			true,
		},
		{
			`p if { x = data.base.foo }`,
			true,
		},
		{
			`p if { data.base.foo(data.base.bar) }`,
			true,
		},
	}

	for n, test := range tests {
		t.Run(fmt.Sprintf("Test Case %d", n), func(t *testing.T) {
			mod := MustParseModuleWithOpts("package test\n"+test.body, popts)
			c := NewCompiler()
			c.Compile(map[string]*Module{"base": MustParseModuleWithOpts(base, popts), "mod": mod})
			if test.wantErr && !c.Failed() {
				t.Errorf("Expected error but got success")
			} else if !test.wantErr && c.Failed() {
				t.Errorf("Expected success but got error: %v", c.Errors)
			}
		})
	}
}

func TestFunctionTypeInferenceUnappliedWithObjectVarKey(t *testing.T) {

	// Run type inference on a function that constructs an object with a key
	// from args in the head.
	module := MustParseModule(`
		package test
		import rego.v1

		f(x) := y if { y = {x: 1} }
	`)

	elems := []util.T{
		module.Rules[0],
	}

	env, err := newTypeChecker().CheckTypes(newTypeChecker().Env(BuiltinMap), elems, nil)

	if len(err) > 0 {
		t.Fatal(err)
	}

	// Check inferred type for reference to function.
	tpe := env.Get(MustParseRef("data.test.f"))
	exp := types.NewFunction([]types.Type{types.A}, types.NewObject(nil, types.NewDynamicProperty(types.A, types.N)))

	if types.Compare(tpe, exp) != 0 {
		t.Fatalf("Expected %v but got %v", exp, tpe)
	}
}

func TestCheckValidErrors(t *testing.T) {

	module := MustParseModule(`
		package test
		import rego.v1

		p if {
			concat("", 1)  # type error
		}

		q if {
			r(1)
		}

		r(x) = x`)

	module2 := MustParseModule(`
		package test
		import rego.v1

		b if {
			a(1)		# call erroneous function
		}

		a(x) if {
			max("foo")  # max requires an array
		}

		m if {
			1 / "foo"	# type error
		}

		n if {
			m			# call erroneous rule
		}`)

	module3 := MustParseModule(`
		package test
		import rego.v1

		x := {"a" : 1}

		y if {
			z
		}

		z if {
			x[1] == 1	# undefined reference error
		}`)

	tests := map[string]struct {
		module *Module
		numErr int
		query  []string
	}{
		"single_type_error":         {module: module, numErr: 1, query: []string{`data.test.p`}},
		"multiple_type_error":       {module: module2, numErr: 2, query: []string{`data.test.a`, `data.test.m`}},
		"undefined_reference_error": {module: module3, numErr: 1, query: []string{`data.test.z`}},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			c := NewCompiler()
			c.Compile(map[string]*Module{"test": tc.module})

			if !c.Failed() {
				t.Errorf("Expected error but got success")
			}

			if len(c.Errors) != tc.numErr {
				t.Fatalf("Expected %v error(s) but got: %v", tc.numErr, c.Errors)
			}

			// check type of the rule/function that contains an error
			for _, q := range tc.query {
				tpe := c.TypeEnv.Get(MustParseRef(q))

				if types.Compare(tpe, types.NewAny()) != 0 {
					t.Fatalf("Expected Any type but got %v", tpe)
				}
			}
		})
	}
}

func TestCheckErrorDetails(t *testing.T) {

	tests := []struct {
		detail   ErrorDetails
		expected []string
	}{
		{
			detail: &RefErrUnsupportedDetail{
				Ref:  MustParseRef("data.foo[x]"),
				Pos:  1,
				Have: types.N,
			},
			expected: []string{
				"data.foo[x]",
				"^^^^^^^^",
				"have: number",
			},
		},
		{
			detail: &RefErrInvalidDetail{
				Ref:  MustParseRef("data.foo[x]"),
				Pos:  2,
				Have: types.N,
				Want: types.S,
			},
			expected: []string{
				"data.foo[x]",
				"         ^",
				"         have (type): number",
				"         want (type): string",
			},
		},
		{
			detail: &RefErrInvalidDetail{
				Ref:  MustParseRef("data.foo[100]"),
				Pos:  2,
				Want: types.S,
				OneOf: []Value{
					String("a"),
					String("b"),
				},
			},
			expected: []string{
				"data.foo[100]",
				"         ^",
				"         have: 100",
				`         want (one of): ["a" "b"]`,
			},
		},
		{
			detail: &ArgErrDetail{
				Have: []types.Type{
					types.N,
					types.S,
				},
				Want: types.FuncArgs{
					Args: []types.Type{
						types.S,
						types.S,
					},
				},
			},
			expected: []string{
				"have: (number, string)",
				"want: (string, string)",
			},
		},
	}

	for _, tc := range tests {
		if !slices.Equal(tc.detail.Lines(), tc.expected) {
			t.Errorf("Expected %v for %v but got: %v", tc.expected, tc.detail, tc.detail.Lines())
		}
	}
}

func TestCheckErrorOrdering(t *testing.T) {

	mod := MustParseModule(`
		package test
		import rego.v1

		q = true

		p if { data.test.q = 1 }  # type error: bool = number
		p if { data.test.q = 2 }  # type error: bool = number
	`)

	input := make([]util.T, len(mod.Rules))
	inputReversed := make([]util.T, len(mod.Rules))

	for i := range input {
		input[i] = mod.Rules[i]
		inputReversed[i] = mod.Rules[i]
	}

	inputReversed[1], inputReversed[2] = inputReversed[2], inputReversed[1]

	_, errs1 := newTypeChecker().CheckTypes(nil, input, nil)
	_, errs2 := newTypeChecker().CheckTypes(nil, inputReversed, nil)

	if errs1.Error() != errs2.Error() {
		t.Fatalf("Expected error slices to be equal. errs1:\n\n%v\n\nerrs2:\n\n%v\n\n", errs1, errs2)
	}
}

func TestRewrittenVarsInErrors(t *testing.T) {

	_, errs := newTypeChecker().WithVarRewriter(rewriteVarsInRef(map[Var]Var{
		"__local0__": "foo",
		"__local1__": "bar",
	})).CheckBody(nil, MustParseBody(`__local0__ = [[1]]; __local1__ = "bar"; __local0__[0][__local1__]`))

	if len(errs) != 1 {
		t.Fatal("expected exactly one error but got:", len(errs))
	}

	detail, ok := errs[0].Details.(*RefErrInvalidDetail)
	if !ok {
		t.Fatal("expected invalid ref detail but got:", errs[0].Details)
	}

	if !detail.Ref.Equal(MustParseRef("foo[0][bar]")) {
		t.Fatal("expected ref to be foo[0][bar] but got:", detail.Ref)
	}

}

func newTestEnv(rs []string, builtinMap map[string]*Builtin) *TypeEnv {
	module := MustParseModule(`
		package test
	`)

	// We preallocate enough for at least the base rules.
	// Else cases will cause reallocs, but that's okay.
	elems := make([]util.T, 0, len(rs))

	for i := range rs {
		rule := MustParseRule(rs[i])
		rule.Module = module
		elems = append(elems, rule)
		for next := rule.Else; next != nil; next = next.Else {
			next.Module = module
			elems = append(elems, next)
		}
	}

	env, err := newTypeChecker().CheckTypes(newTypeChecker().Env(builtinMap), elems, nil)
	if len(err) > 0 {
		panic(err)
	}

	return env
}

const inputSchema = `{
	"$schema": "http://json-schema.org/draft-07/schema",
	"$id": "http://example.com/example.json",
	"type": "object",
	"title": "The root schema",
	"description": "The root schema comprises the entire JSON document.",
	"default": {},
	"examples": [
		{
			"user": "alice",
			"operation": "write"
		}
	],
	"required": [
		"user",
		"operation"
	],
	"properties": {
		"user": {
			"$id": "#/properties/user",
			"type": "string",
			"title": "The user schema",
			"description": "An explanation about the purpose of this instance.",
			"default": "",
			"examples": [
				"alice"
			]
		},
		"operation": {
			"$id": "#/properties/operation",
			"type": "string",
			"title": "The operation schema",
			"description": "An explanation about the purpose of this instance.",
			"default": "",
			"examples": [
				"write"
			]
		}
	},
	"additionalProperties": true
}`

const inputSchema2 = `{
    "$schema": "http://json-schema.org/draft-07/schema",
    "$id": "http://example.com/example.json",
    "type": "object",
    "title": "The root schema",
    "description": "The root schema comprises the entire JSON document.",
    "default": {},
    "examples": [
        {
            "operation": "read"
        }
    ],
    "required": [
        "operation"
    ],
    "properties": {
        "operation": {
            "$id": "#/properties/operation",
            "type": "string",
            "title": "The operation schema",
            "description": "An explanation about the purpose of this instance.",
            "default": "",
            "examples": [
                "read"
            ]
        }
    },
    "additionalProperties": true
}`

const dataSchema = `{
    "$schema": "http://json-schema.org/draft-07/schema",
    "$id": "http://example.com/example.json",
    "type": "object",
    "title": "The root schema",
    "description": "The root schema comprises the entire JSON document.",
    "default": {},
    "examples": [
        {
            "alice": [
                "read",
                "write"
            ],
            "bob": [
                "read"
            ]
        }
    ],
    "required": [
        "alice",
        "bob"
    ],
    "properties": {
        "alice": {
            "$id": "#/properties/alice",
            "type": "array",
            "title": "The alice schema",
            "description": "An explanation about the purpose of this instance.",
            "default": [],
            "examples": [
                [
                    "read",
                    "write"
                ]
            ],
            "additionalItems": false,
            "items": {
                "$id": "#/properties/alice/items",
                "type": "string",
                "title": "The items schema",
                "description": "An explanation about the purpose of this instance.",
                "default": "",
                "examples": [
                    [
                        "read",
                        "write"
                    ]
                ]
            }
        },
        "bob": {
            "$id": "#/properties/bob",
            "type": "array",
            "title": "The bob schema",
            "description": "An explanation about the purpose of this instance.",
            "default": [],
            "examples": [
                [
                    "read"
                ]
            ],
            "additionalItems": false,
            "items": {
                "$id": "#/properties/bob/items",
                "type": "string",
                "title": "The items schema",
                "description": "An explanation about the purpose of this instance.",
                "default": "",
                "examples": [
                    [
                        "read"
                    ]
                ]
            }
        }
    },
    "additionalProperties": true
}`

func TestCheckAnnotationRules(t *testing.T) {

	ischema := util.MustUnmarshalJSON([]byte(inputSchema))
	ischema2 := util.MustUnmarshalJSON([]byte(inputSchema2))
	dschema := util.MustUnmarshalJSON([]byte(dataSchema))
	numberSchema := util.MustUnmarshalJSON([]byte(`{"type": "number"}`))
	stringSchema := util.MustUnmarshalJSON([]byte(`{"type": "string"}`))

	module1 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
}`

	module2 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input: schema["whocan-input-schema"]
#   - data.acl: schema["acl-schema"]
whocan[user] {
		access = acl[user]
		access[_] == input.operation
}`

	module3 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input: schema["whocan-input-schema"]
#   - data.acl: schema["acl-schema"]
allow {
	access = data.acl[input.user]
	access[_] == input.operation
}`

	module4 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema.missing
whocan[user] {
		access = acl[user]
		access[_] == input.operation
}`

	module8 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
#   - input.apple.orange: schema["input"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.banana
}`

	module9 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
#   - input.apple.orange: schema["input"]
#   - input.apple.orange.banana: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.orange.banana
}`

	module10 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input.apple.orange: schema["input"]
#   - input.apple.orange.banana: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.orange.banana.fruit
}`

	module11 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input.apple.orange: schema["input"]
#   - input.apple.orange: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.orange.bob
		input.apple.orange.user
}`

	module12 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input: schema["acl-schema"]
allow {
		access = data.acl[input.user]
}`

	module13 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input.apple["orange"]: schema["input"]
allow {
		access = data.acl[input.user]
		input.apple.orange.fruit
}`

	module14 := `
package policy

import data.acl
import input

# METADATA
# scope: rule
# schemas:
#   - input.request.object: schema["acl-schema"]
deny[msg] {
	input.request.kind.kind == "Pod"
	image := input.request.object.spec.typo.containers[_].image
	not startswith(image, "hooli.com/")
}`

	module15 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   -  data.acl: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.orange.banana
}`

	module16 := `
package policy

import data.acl
import input

# METADATA
# scope: rule
# schemas:
#   - data.acl: schema["acl-schema"]
deny[msg] {
	input.request.kind.kinds == "Pod"
	image := input.request.object.spec.containers[_].image
	not startswith(image, "hooli.com/")
	data.blah
}`

	module17 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["acl-schema"]
allow {
		input.alice
}

deny[msg] {
	input.foo
}`

	module18 := `
package policy

import data.acl
import input

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - input.apple.banana: schema["input"]
deny[msg] {
	input.apple.banana
}

deny1[msg] {
	input.apple.banana.foo
}`

	module19 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - data.acl: schema["acl-schema"]
#   - data.acl.foo: schema["input"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		input.apple.orange.banana
		data.acl.foo.blah
}`

	module20 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		data.acl.foo
}`

	module21 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
}

# METADATA for whocan rule
# scope: rule
# schemas:
#   - input: schema["whocan-input-schema"]
#   - data.acl: schema["acl-schema"]
whocan[user] {
		access = acl[user]
		access[_] == input.operation
}`

	module22 := `
package policy

import data.acl
import input

default allow = false

# METADATA
# scope: rule
# schemas:
#   - input: schema["input"]
#   - data.acl: schema["acl-schema"]
allow {
		access = data.acl[input.user]
		access[_] == input.operation
		data.foo
		data.acl.foo
}

# METADATA for whocan rule
# scope: rule
# schemas:
#   - input: schema["whocan-input-schema"]
#   - data.acl: schema["acl-schema"]
whocan[user] {
		access = acl[user]
		access[_] == input.operation.foo

}`

	schemaSet := NewSchemaSet()
	schemaSet.Put(MustParseRef("schema.number"), numberSchema)
	schemaSet.Put(MustParseRef("schema.string"), stringSchema)
	schemaSet.Put(MustParseRef("schema.input"), ischema)
	schemaSet.Put(MustParseRef(`schema["whocan-input-schema"]`), ischema2)
	schemaSet.Put(MustParseRef(`schema["acl-schema"]`), dschema)

	tests := []struct {
		note    string
		modules []string
		err     string
	}{
		{note: "data and input annotations", modules: []string{module1}},
		{note: "correct data override", modules: []string{module2}},
		{note: "incorrect data override", modules: []string{module3}, err: "undefined ref: input.user"},
		{note: "missing schema", modules: []string{module4}, err: "undefined schema: schema.missing"},
		{note: "overriding ref with length greater than one and not existing", modules: []string{module8}, err: "undefined ref: input.apple.banana"},
		{note: "overriding ref with length greater than one and existing prefix", modules: []string{module9}},
		{note: "overriding ref with length greater than one and existing prefix with type error", modules: []string{module10}, err: "undefined ref: input.apple.orange.banana.fruit"},
		{note: "overriding ref with length greater than one and existing ref", modules: []string{module11}, err: "undefined ref: input.apple.orange.user"},
		{note: "overriding ref of size one", modules: []string{module12}, err: "undefined ref: input.user"},
		{note: "overriding annotation written with brackets", modules: []string{module13}, err: "undefined ref: input.apple.orange.fruit"},
		{note: "overriding strict", modules: []string{module14}, err: "undefined ref: input.request.object.spec.typo"},
		{note: "data annotation but no input schema", modules: []string{module15}},
		{note: "data schema annotation does not overly restrict data expression", modules: []string{module16}},
		{note: "correct defer annotation on another rule has no effect base case", modules: []string{module17}},
		{note: "correct defer annotation on another rule has no effect", modules: []string{module18}},
		{note: "overriding ref with data prefix", modules: []string{module19}, err: "data.acl.foo.blah"},
		{note: "data annotation type error", modules: []string{module20}, err: "data.acl.foo"},
		{note: "more than one rule with metadata", modules: []string{module21}},
		{note: "more than one rule with metadata with type error", modules: []string{module22}, err: "undefined ref"},
		{note: "document scope", err: "test1.rego:8: rego_type_error: match error", modules: []string{`package test
# METADATA
# scope: document
# schemas:
# - input.foo: schema.number
p { input.foo = 7 }

p { input.foo = [] }`}},

		{note: "rule scope overrides document scope", modules: []string{`package test

# METADATA
# scope: document
# schemas:
# - input.foo: schema.number
p { input.foo = 7 }

# METADATA
# scope: rule
# schemas:
# - input.foo: schema.string
p { input.foo = "str" }`}},

		{note: "rule scope merges with document scope", err: "test1.rego:15: rego_type_error: match error", modules: []string{`package test

# METADATA
# scope: document
# schemas:
# - input.bar: schema.number
p { input.bar = 7 }

# METADATA
# scope: rule
# schemas:
# - input.foo: schema.string
p {
	input.foo = "str"
	input.bar = "str"
}`}},

		{note: "document scope conflict", err: "test1.rego:9: rego_type_error: document annotation redeclared: test1.rego:3", modules: []string{`package test

# METADATA
# scope: document
# schemas:
# - input.foo: schema.number
p { input.foo = 7 }

# METADATA
# scope: document
# schemas:
# - input.foo: schema.string
p { input.foo = "str" }`}},

		{note: "package scope in other module", modules: []string{`# METADATA
# scope: package
# schemas:
# - input.foo: schema.number
package test`, `package test

p { input.foo = 7 }`}},

		{note: "package scope in other module type conflict", err: "test2.rego:3: rego_type_error: match error", modules: []string{`# METADATA
# scope: package
# schemas:
# - input.foo: schema.string
package test`, `package test

p { input.foo = 7 }`}},

		{note: "package scope conflict", err: "test2.rego:1: rego_type_error: package annotation redeclared: test1.rego:1", modules: []string{`# METADATA
# scope: package
# schemas:
# - input.foo: schema.string
package test`, `# METADATA
# scope: package
# schemas:
# - input.foo: schema.number
package test

p { input.foo = 7 }`}},

		{note: "subpackages scope", err: "test1.rego:7: rego_type_error: match error", modules: []string{`# METADATA
# scope: subpackages
# schemas:
# - input: schema.number
package test

p { input = "str" }`}},

		{note: "document scope overrides subpackages scope", modules: []string{`# METADATA
# scope: subpackages
# schemas:
# - input: schema.number
package test

# METADATA
# scope: document
# schemas:
# - input: schema.string
p { input = "str" }`}},

		{note: "document scope overrides subpackages scope and finds error", err: "test1.rego:11: rego_type_error: match error", modules: []string{`# METADATA
# scope: subpackages
# schemas:
# - input: schema.string
package test

# METADATA
# scope: rule
# schemas:
# - input: schema.number
p { input = "str" }`}},

		{note: "package scope", err: "test1.rego:7: rego_type_error: match error", modules: []string{`# METADATA
# scope: package
# schemas:
# - input: schema.string
package test

p { input = 7 }`}},

		{note: "rule scope overrides package scope", modules: []string{`# METADATA
# scope: package
# schemas:
# - input: schema.string
package test

# METADATA
# scope: rule
# schemas:
# - input: schema.number
p { input = 7 }`}},

		{note: "inline definition", err: "test1.rego:7: rego_type_error: match error", modules: []string{`package test

# METADATA
# scope: rule
# schemas:
# - input: {"type": "string"}
p { input = 7 }`}},
		{note: "document scope is unordered", err: "test1.rego:3: rego_type_error: match error", modules: []string{`package test

p { input = 7 }

# METADATA
# scope: document
# schemas:
# - input: schema.string
p { input = "foo" }`}},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			var modules []*Module
			var elems []util.T

			for i, module := range tc.modules {
				mod, err := ParseModuleWithOpts(fmt.Sprintf("test%d.rego", i+1), module, ParserOptions{
					ProcessAnnotation: true,
					RegoVersion:       RegoV0,
				})
				if err != nil {
					t.Fatal(err)
				}
				modules = append(modules, mod)

				for _, rule := range mod.Rules {
					elems = append(elems, rule)
					for next := rule.Else; next != nil; next = next.Else {
						elems = append(elems, next)
					}
				}
			}

			oldTypeEnv := newTypeChecker().WithSchemaSet(schemaSet).Env(BuiltinMap)
			as, errors := BuildAnnotationSet(modules)
			typeenv, checkErrors := newTypeChecker().WithSchemaSet(schemaSet).CheckTypes(oldTypeEnv, elems, as)
			errors = append(errors, checkErrors...)
			if len(errors) > 0 {
				for _, e := range errors {
					if tc.err == "" || !strings.Contains(e.Error(), tc.err) {
						t.Fatalf("Unexpected check rule error when processing annotations: %v", e)
					}
				}
				return
			} else if tc.err != "" {
				t.Fatalf("Expected error %q but got success", tc.err)
			}

			if oldTypeEnv.tree.children != nil && typeenv.next.tree.children != nil && (typeenv.next.tree.children.Len() != oldTypeEnv.tree.children.Len()) {
				t.Fatalf("Unexpected type env")
			}

		})
	}
}

func TestCheckAnnotationInference(t *testing.T) {

	tests := []struct {
		note    string
		modules map[string]string
		schemas map[string]string
		exp     map[string]types.Type
	}{
		{
			note: "rule scope",
			modules: map[string]string{
				"test.rego": `
package test
import rego.v1

# METADATA
# scope: rule
# schemas:
# - input: schema.foo
p = x if { input = x }

q = p`,
			},
			schemas: map[string]string{
				"schema.foo": `{"type": "number"}`,
			},
			exp: map[string]types.Type{
				"data.test.p": types.N,
				"data.test.q": types.N,
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {

			modules := map[string]*Module{}
			for k, v := range tc.modules {
				var err error
				modules[k], err = ParseModuleWithOpts(k, v, ParserOptions{ProcessAnnotation: true})
				if err != nil {
					t.Fatal(err)
				}

				ss := NewSchemaSet()
				for k, v := range tc.schemas {

					ref := MustParseRef(k)
					var schema any
					err = util.Unmarshal([]byte(v), &schema)
					if err != nil {
						t.Fatal(err)
					}

					ss.Put(ref, schema)
				}

				compiler := NewCompiler().
					WithSchemas(ss).
					WithUseTypeCheckAnnotations(true)
				compiler.Compile(modules)
				if compiler.Failed() {
					t.Fatal("unexpected error:", compiler.Errors)
				}

				for k, v := range tc.exp {
					ref := MustParseRef(k)
					result := compiler.TypeEnv.Get(ref)
					if types.Compare(result, v) != 0 {
						t.Errorf("expected %v => %v but got %v", ref, v, result)
					}
				}
			}

		})
	}

}

// TestSchemaCache is a regression test for https://github.com/open-policy-agent/opa/issues/7679
func TestInlinedSchemaAnnotationIgnoredByCache(t *testing.T) {
	policy := `
package test

# METADATA
# schemas:
#   - input.x: {"type": "boolean"}
p if {
	input.x == 42
}

# METADATA
# schemas:
#   - input.x: {"type": "object"}
q if {
	input.x == "foo"
}`

	m, err := ParseModuleWithOpts("policy.rego", policy, ParserOptions{ProcessAnnotation: true})
	if err != nil {
		t.Fatalf("failed to parse module: %v", err)
	}

	ms := map[string]*Module{"policy.rego": m}
	compiler := NewCompiler().WithUseTypeCheckAnnotations(true)
	compiler.Compile(ms)

	if !compiler.Failed() {
		t.Fatal("compiler failures expected, got none")
	}

	expErrs := []string{
		`policy.rego:8: rego_type_error: match error
	left  : boolean
	right : number`,
		`policy.rego:15: rego_type_error: match error
	left  : object[any: any]
	right : string`,
	}

	for _, expErr := range expErrs {
		found := false
		for _, err := range compiler.Errors {
			if strings.Contains(err.Error(), expErr) {
				found = true
				continue
			}
		}
		if !found {
			t.Errorf("expected error:\n\n%s\n\ngot:\n\n%s", expErr, compiler.Errors.Error())
		}
	}
}

func TestRemoteSchema(t *testing.T) {
	schema := `{"type": "boolean"}`

	schemaCalled := false
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		schemaCalled = true
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(schema))
	}))
	defer server.Close()

	policy := fmt.Sprintf(`
package test
import rego.v1

# METADATA
# schemas:
# - input: {$ref: "%s"}
p if {
	input == 42
}`, server.URL)

	module, err := ParseModuleWithOpts("policy.rego", policy, ParserOptions{ProcessAnnotation: true})
	if err != nil {
		t.Fatal(err)
	}
	modules := map[string]*Module{"policy.rego": module}

	compiler := NewCompiler().
		WithUseTypeCheckAnnotations(true)
	compiler.Compile(modules)

	if !compiler.Failed() {
		t.Fatal("expected error, got none")
	}

	expectedTypeError := "rego_type_error: match error"
	if !strings.Contains(compiler.Errors.Error(), expectedTypeError) {
		t.Fatalf("expected error:\n\n%s\n\ngot:\n\n%s",
			expectedTypeError, compiler.Errors.Error())
	}

	if !schemaCalled {
		t.Fatal("expected schema server to be called, was not")
	}
}

func TestRemoteSchemaHostNotAllowed(t *testing.T) {
	capabilities := CapabilitiesForThisVersion()
	capabilities.AllowNet = []string{}
	schema := `{"type": "boolean"}`

	schemaCalled := false
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		schemaCalled = true
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(schema))
	}))
	defer server.Close()

	policy := fmt.Sprintf(`
package test
import rego.v1

# METADATA
# schemas:
# - input: {$ref: "%s"}
p if {
	input == 42
}`, server.URL)

	module, err := ParseModuleWithOpts("policy.rego", policy, ParserOptions{ProcessAnnotation: true})
	if err != nil {
		t.Fatal(err)
	}
	modules := map[string]*Module{"policy.rego": module}

	compiler := NewCompiler().
		WithUseTypeCheckAnnotations(true).
		WithCapabilities(capabilities)
	compiler.Compile(modules)

	if !compiler.Failed() {
		t.Fatal("expected error, got none")
	}

	expectedTypeError := "rego_type_error: unable to compile the schema: remote reference loading disabled"
	if !strings.Contains(compiler.Errors.Error(), expectedTypeError) {
		t.Fatalf("expected error:\n\n%s\n\ngot:\n\n%s",
			expectedTypeError, compiler.Errors.Error())
	}

	if schemaCalled {
		t.Fatal("expected schema server to not be called, was")
	}
}

func TestCheckExpectedErrors(t *testing.T) {
	tests := []struct {
		name          string
		policy        string
		expectedError string
	}{
		{
			name: "value with multiple assignments",
			policy: `package p

default f := ""

f := 1
r := f()`,
			expectedError: "rego_type_error: undefined function data.p.f",
		},
		{
			name: "value with one assignments",
			policy: `package p

f := 1
r := f()`,
			expectedError: "rego_type_error: undefined function data.p.f",
		},
		{
			name: "value with no assignments",
			policy: `package p

r := f()`,
			expectedError: "rego_type_error: undefined function f",
		},
		{
			name: "function used as reference",
			policy: `package play

import rego.v1

f(x) := x

allow if {
    some x in f
}`,
			expectedError: "policy.rego:8: rego_type_error: function data.play.f used as reference, not called",
		},
		{
			name: "function used as reference",
			policy: `package play

p if { [data.base.foo] }`,
			expectedError: "policy.rego:3: rego_type_error: function data.base.foo used as reference, not called",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			module, err := ParseModuleWithOpts("policy.rego", tc.policy, ParserOptions{ProcessAnnotation: true})
			if err != nil {
				t.Fatal(err)
			}

			capabilities := CapabilitiesForThisVersion()
			compiler := NewCompiler().
				WithUseTypeCheckAnnotations(true).
				WithCapabilities(capabilities)
			pOpts := ParserOptions{AllFutureKeywords: true}

			functions := []string{
				`foo([a, b]) = y if { split(a, b, y) }`,
			}
			body := strings.Join(functions, "\n")
			base := "package base\n" + body
			compiler.Compile(map[string]*Module{"base": MustParseModuleWithOpts(base, pOpts), "policy.rego": module})

			if !compiler.Failed() {
				t.Fatal("expected error, got none")
			}

			if !strings.Contains(compiler.Errors.Error(), tc.expectedError) {
				t.Fatalf("expected error:\n\n%s\n\ngot:\n\n%s",
					tc.expectedError, compiler.Errors.Error())
			}
		})
	}
}
