// Copyright 2024 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 ext

import (
	"fmt"
	"strings"
	"testing"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/checker"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/interpreter"
)

func TestTwoVarComprehensions(t *testing.T) {
	compreTests := []struct {
		expr string
	}{
		// list.all()
		{expr: "[1, 2, 3, 4].all(i, v, i < 5 && v > 0)"},
		{expr: "[1, 2, 3, 4].all(i, v, i < v)"},
		{expr: "[1, 2, 3, 4].all(i, v, i > v) == false"},
		{expr: `
		cel.bind(listA, [1, 2, 3, 4],
		cel.bind(listB, [1, 2, 3, 4, 5],
		   listA.all(i, v, listB[?i].hasValue() && listB[i] == v)
		))
		`},
		{expr: `
		cel.bind(listA, [1, 2, 3, 4, 5, 6],
		cel.bind(listB, [1, 2, 3, 4, 5],
		   listA.all(i, v, listB[?i].hasValue() && listB[i] == v)
		)) == false
		`},
		// list.exists()
		{expr: `
		cel.bind(l, ['hello', 'world', 'hello!', 'worlds'],
		  l.exists(i, v,
		    v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false)
		  )
		)
		`},
		// list.existsOne()
		{expr: `
		cel.bind(l, ['hello', 'world', 'hello!', 'worlds'],
		  l.existsOne(i, v,
		    v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false)
		  )
		)
		`},
		{expr: `
		cel.bind(l, ['hello', 'goodbye', 'hello!', 'goodbye'],
		  l.exists_one(i, v,
		    v.startsWith('hello') && l[?(i+1)].optMap(next, next == "goodbye").orValue(false)
		  )
		) == false
		`},
		// list.transformList()
		{expr: `
		['Hello', 'world'].transformList(i, v, "[%d]%s".format([i, v.lowerAscii()])) == ["[0]hello", "[1]world"]
		`},
		{expr: `
		['hello', 'world'].transformList(i, v, v.startsWith('greeting'), "[%d]%s".format([i, v])) == []
		`},
		{expr: `
		[1, 2, 3].transformList(indexVar, valueVar, (indexVar * valueVar) + valueVar) == [1, 4, 9]
		`},
		{expr: `
		[1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar) + valueVar) == [1, 9]
		`},
		// list.transformMap()
		{expr: `
		['Hello', 'world'].transformMap(i, v, [v.lowerAscii()]) == {0: ['hello'], 1: ['world']}
		`},
		{expr: `
		// round-tripping example
		['world', 'Hello'].transformMap(i, v, [v.lowerAscii()])
		  .transformList(k, v, v) // extract the list back form the map
		  .flatten()
		  .sort() == ['hello', 'world']
		`},
		{expr: `
		[1, 2, 3].transformMap(indexVar, valueVar,
	      (indexVar * valueVar) + valueVar) == {0: 1, 1: 4, 2: 9}
        `},
		{expr: `
		[1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0,
	  	  (indexVar * valueVar) + valueVar) == {0: 1, 2: 9}
		`},
		// list.transformMapEntry()
		{expr: `
		"key1:value1 key2:value2 key3:value3".split(" ")
		.transformMapEntry(i, v,
		  cel.bind(entry, v.split(":"),
		    entry.size() == 2 ? {entry[0]: entry[1]} : {}
		  )
		) == {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
		`},
		{expr: `
		"key1:value1:extra key2:value2 key3".split(" ")
		.transformMapEntry(i, v,
		  cel.bind(entry, v.split(":"), {?entry[0]: entry[?1]})
		) == {'key1': 'value1', 'key2': 'value2'}
		`},
		// map.all()
		{expr: `
		{'hello': 'world', 'hello!': 'world'}.all(k, v, k.startsWith('hello') && v == 'world')
		`},
		{expr: `
		{'hello': 'world', 'hello!': 'worlds'}.all(k, v, k.startsWith('hello') && v.endsWith('world')) == false
		`},
		// map.exists()
		{expr: `
		{'hello': 'world', 'hello!': 'worlds'}.exists(k, v, k.startsWith('hello') && v.endsWith('world'))
		`},
		// map.existsOne()
		{expr: `
		{'hello': 'world', 'hello!': 'worlds'}.existsOne(k, v, k.startsWith('hello') && v.endsWith('world'))
		`},
		// map.exists_one()
		{expr: `
		{'hello': 'world', 'hello!': 'worlds'}.exists_one(k, v, k.startsWith('hello') && v.endsWith('world'))
		`},
		{expr: `
		{'hello': 'world', 'hello!': 'wow, world'}.exists_one(k, v, k.startsWith('hello') && v.endsWith('world')) == false
		`},
		// map.transformList()
		{expr: `
		{'Hello': 'world'}.transformList(k, v, "%s=%s".format([k.lowerAscii(), v])) == ["hello=world"]
		`},
		{expr: `
		dyn({'Hello': 'world'}).transformList(k, v, "%s=%s".format([k.lowerAscii(), v])) == ["hello=world"]
		`},
		{expr: `
		{'hello': 'world'}.transformList(k, v, k.startsWith('greeting'), "%s=%s".format([k, v])) == []
		`},
		{expr: `
		{'greeting': 'hello', 'farewell': 'goodbye'}
		  .transformList(k, _, k).sort() == ['farewell', 'greeting']
		`},
		{expr: `
		{'greeting': 'hello', 'farewell': 'goodbye'}
		  .transformList(_, v, v).sort() == ['goodbye', 'hello']
		`},
		// map.transformMap()
		{expr: `
		{'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, "%s, %s!".format([k, v]))
		   == {'hello': 'hello, world!', 'goodbye': 'goodbye, cruel world!'}
		`},
		{expr: `
		dyn({'hello': 'world', 'goodbye': 'cruel world'}).transformMap(k, v, "%s, %s!".format([k, v]))
		   == {'hello': 'hello, world!', 'goodbye': 'goodbye, cruel world!'}
		`},
		{expr: `
		{'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, v.startsWith('world'), "%s, %s!".format([k, v]))
		   == {'hello': 'hello, world!'}
		`},
		// map.transformMapEntry()
		{expr: `
		{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {k.reverse(): v.reverse()})
		   == {'olleh': 'dlrow', 'sgniteerg': 'tacocat'}
		`},
		{expr: `
		{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, v.reverse() == v, {k.reverse(): v.reverse()})
		   == {'sgniteerg': 'tacocat'}
		`},
		{expr: `
		{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {}) == {}
		`},
	}

	env := testCompreEnv(t)
	for i, tst := range compreTests {
		tc := tst
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			var asts []*cel.Ast
			pAst, iss := env.Parse(tc.expr)
			if iss.Err() != nil {
				t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
			}
			asts = append(asts, pAst)
			cAst, iss := env.Check(pAst)
			if iss.Err() != nil {
				t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err())
			}
			asts = append(asts, cAst)

			for _, ast := range asts {
				prg, err := env.Program(ast)
				if err != nil {
					t.Fatalf("env.Program() failed: %v", err)
				}
				out, _, err := prg.Eval(cel.NoVars())
				if err != nil {
					t.Fatalf("prg.Eval() failed: %v", err)
				}
				if out.Value() != true {
					t.Errorf("prg.Eval() got %v, wanted true for expr: %s", out.Value(), tc.expr)
				}
			}
		})
	}
}

func TestTwoVarComprehensionsCost(t *testing.T) {
	tests := []struct {
		name          string
		expr          string
		vars          []cel.EnvOption
		in            map[string]any
		hints         map[string]uint64
		estimatedCost checker.CostEstimate
		actualCost    uint64
	}{
		{
			name:          "all list literal",
			expr:          `[1, 2, 3, 4].all(i, v, i < 5 && v > 0)`,
			estimatedCost: checker.CostEstimate{Min: 23, Max: 39},
			actualCost:    39,
		},
		{
			name:          "all map literal - true",
			expr:          `{1: 1, 2: 2, 3: 3}.all(i, v, i < 5 && v > 0)`,
			estimatedCost: checker.CostEstimate{Min: 40, Max: 52},
			actualCost:    52,
		},
		{
			name:          "all map literal - false",
			expr:          `!{0: 0}.all(i, v, i < 5 && v > 0)`,
			estimatedCost: checker.CostEstimate{Min: 35, Max: 39},
			actualCost:    39,
		},
		{
			name: "all map(int,int) variable",
			expr: `m.all(i, v, i < 5 && v > 0)`,
			vars: []cel.EnvOption{cel.Variable("m", cel.MapType(cel.IntType, cel.IntType))},
			hints: map[string]uint64{
				"m": 3,
			},
			in: map[string]any{
				"m": map[int]int{1: 1, 2: 2},
			},
			estimatedCost: checker.CostEstimate{Min: 2, Max: 23},
			actualCost:    16,
		},
		{
			name: "all map(string,string) variable",
			expr: `m.all(k, v, k < v)`,
			vars: []cel.EnvOption{cel.Variable("m", cel.MapType(cel.StringType, cel.StringType))},
			hints: map[string]uint64{
				"m":         3,
				"m.@keys":   16,
				"m.@values": 128,
			},
			in: map[string]any{
				"m": map[string]string{"he": "hello", "go": "goodbye"},
			},
			estimatedCost: checker.CostEstimate{Min: 2, Max: 23},
			actualCost:    14,
		},
		{
			name:          "transformList empty",
			expr:          `[].transformList(i, v, v) == []`,
			estimatedCost: checker.FixedCostEstimate(31),
			actualCost:    31,
		},
		{
			name:          "transformList single element",
			expr:          `[1].transformList(i, v, i) == [0]`,
			estimatedCost: checker.FixedCostEstimate(45),
			actualCost:    45,
		},
		{
			name:          "transformList with filter",
			expr:          `[3, 2, 1].transformList(i, v, v > i, v) == [3, 2]`,
			estimatedCost: checker.CostEstimate{Min: 44, Max: 80},
			actualCost:    67,
		},
		{
			name:          "transformMap empty list",
			expr:          `[].transformMap(k, v, v + 1) == {}`,
			estimatedCost: checker.FixedCostEstimate(71),
			actualCost:    71,
		},
		{
			name:          "transformMap empty map",
			expr:          `{}.transformMap(k, v, v + 1) == {}`,
			estimatedCost: checker.FixedCostEstimate(91),
			actualCost:    91,
		},
		{
			name:          "transformMap literal scalar map",
			expr:          `{1: 2}.transformMap(k, v, v + 1) == {1: 3}`,
			estimatedCost: checker.FixedCostEstimate(97),
			actualCost:    97,
		},
		{
			name: "transformMap local bind",
			expr: `cel.bind(m, {"hello": "hello"},
			                m.transformMap(k, v, v + "world")) == {"hello": "helloworld"}`,
			estimatedCost: checker.FixedCostEstimate(108),
			actualCost:    108,
		},
		{
			name:          "transformMap filter map",
			expr:          `{1: 2, 3: 4, 5: 6}.transformMap(k, v, k % 3 == 0, v + 1) == {3: 5}`,
			estimatedCost: checker.CostEstimate{Min: 104, Max: 116},
			actualCost:    106,
		},
		{
			name: "transformMap variable input",
			expr: `m.transformMap(k, v, k.startsWith('legacy') && v.size() == 1, v + [2]) == {'legacy-solo': [1, 2]}`,
			vars: []cel.EnvOption{
				cel.Variable("m", cel.MapType(cel.StringType, cel.ListType(cel.IntType))),
			},
			in: map[string]any{
				"m": map[string][]int{
					"legacy-solo": {1},
					"legacy-pair": {3, 2},
				},
			},
			hints: map[string]uint64{
				"m":                5,
				"m.@keys":          16,
				"m.@values":        10,
				"m.@values.@items": 2,
			},
			estimatedCost: checker.CostEstimate{Min: 73, Max: 173},
			actualCost:    100,
		},
		{
			name:          "transformMapEntry literal input",
			expr:          `{1: 2}.transformMapEntry(k, v, {v: k}) == {2: 1}`,
			estimatedCost: checker.FixedCostEstimate(126),
			actualCost:    126,
		},
		{
			name: "transformMapEntry variable input",
			expr: `m.transformMapEntry(k, v, {v: k}) == m.transformMapEntry(k, v, {v: k})`,
			vars: []cel.EnvOption{
				cel.Variable("m", cel.MapType(cel.StringType, cel.IntType)),
			},
			in: map[string]any{
				"m": map[string]int{
					"legacy-solo": 1,
					"legacy-pair": 2,
				},
			},
			hints: map[string]uint64{
				"m":         5,
				"m.@keys":   16,
				"m.@values": 10,
			},
			estimatedCost: checker.CostEstimate{Min: 65, Max: 405},
			actualCost:    201,
		},
	}

	for _, tst := range tests {
		tc := tst
		t.Run(tc.name, func(t *testing.T) {
			env := testCompreEnv(t, tc.vars...)
			var asts []*cel.Ast
			pAst, iss := env.Parse(tc.expr)
			if iss.Err() != nil {
				t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
			}
			asts = append(asts, pAst)
			cAst, iss := env.Check(pAst)
			if iss.Err() != nil {
				t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err())
			}

			testCheckCost(t, env, cAst, tc.hints, tc.estimatedCost)
			asts = append(asts, cAst)
			for _, ast := range asts {
				testEvalWithCost(t, env, ast, tc.in, tc.actualCost)
			}
		})
	}
}

func TestTwoVarComprehensionsStaticErrors(t *testing.T) {
	tests := []struct {
		expr string
		err  string
	}{
		{
			expr: "[].all(i, i, i < i)",
			err:  "duplicate variable name: i",
		},
		{
			expr: "[].all(__result__, i, __result__ < i)",
			err:  "iteration variable overwrites accumulator variable",
		},
		{
			expr: "[].all(j, __result__, __result__ < j)",
			err:  "iteration variable overwrites accumulator variable",
		},
		{
			expr: "[].all(i.j, k, i.j < k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "[].all(j, i.k, j < i.k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "1.all(j, k, j < k)",
			err:  "cannot be range",
		},
		{
			expr: "[].exists(i.j, k, i.j < k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "[].exists(j, i.k, j < i.k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "''.exists(j, k, j < k)",
			err:  "cannot be range",
		},
		{
			expr: "[].exists_one(i.j, k, i.j < k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "[].existsOne(j, i.k, j < i.k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "[].exists_one(i.j, k, i.j < k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "''.existsOne(j, k, j < k)",
			err:  "cannot be range",
		},
		{
			expr: "[].transformList(i.j, k, i.j + k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "[].transformList(j, i.k, j + i.k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "{}.transformMap(i.j, k, i.j + k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "{}.transformMap(j, i.k, j + i.k)",
			err:  "argument must be a simple name",
		},
		{
			expr: "{}.transformMapEntry(j, i.k, {j: i.k})",
			err:  "argument must be a simple name",
		},
		{
			expr: "{}.transformMapEntry(i.j, k, {k: i.j})",
			err:  "argument must be a simple name",
		},
		{
			expr: "{}.transformMapEntry(j, k, 'bad filter', {k: j})",
			err:  "no matching overload",
		},
		{
			expr: "[1, 2].transformList(i, v, v % 2 == 0 ? [v] : v)",
			err:  "no matching overload",
		},
		{
			expr: `{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, []) == {}`,
			err:  "no matching overload"},
	}
	env := testCompreEnv(t)
	for i, tst := range tests {
		tc := tst
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			_, iss := env.Compile(tc.expr)
			if iss.Err() == nil || !strings.Contains(iss.Err().Error(), tc.err) {
				t.Errorf("env.Compile(%q) got %v, wanted error %v", tc.expr, iss.Err(), tc.err)
			}
		})
	}
}

func TestTwoVarComprehensionsRuntimeErrors(t *testing.T) {
	tests := []struct {
		expr string
		err  string
	}{
		{
			expr: "[1, 1].transformMapEntry(i, v, {v: i})",
			err:  "insert failed: key 1 already exists",
		},
		{
			expr: `[0, 0u].transformMapEntry(i, v, {v: i})`,
			err:  "insert failed: key 0 already exists",
		},
	}
	env := testCompreEnv(t)
	for i, tst := range tests {
		tc := tst
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			ast, iss := env.Compile(tc.expr)
			if iss.Err() != nil {
				t.Fatalf("env.Compile(%q) failed with error %v", tc.expr, iss.Err())
			}
			prg, err := env.Program(ast)
			if err != nil {
				t.Fatalf("env.Program(ast) failed: %v", err)
			}
			in := cel.NoVars()
			_, _, err = prg.Eval(in)
			if err == nil || !strings.Contains(err.Error(), tc.err) {
				t.Errorf("prg.Eval() got %v, wanted %v", err, tc.err)
			}
		})
	}
}

func TestTwoVarComprehensionsVersion(t *testing.T) {
	_, err := cel.NewEnv(TwoVarComprehensions(TwoVarComprehensionsVersion(0)))
	if err != nil {
		t.Fatalf("TwoVarComprehensionVersion(0) failed: %v", err)
	}
}

func TestTwoVarComprehensionsUnparse(t *testing.T) {
	tests := []struct {
		name     string
		expr     string
		unparsed string
	}{
		{
			name:     "transform map entry",
			expr:     `[0, 0u].transformMapEntry(i, v, {v: i})`,
			unparsed: `[0, 0u].transformMapEntry(i, v, {v: i})`,
		},
		{
			name:     "transform map",
			expr:     `{'a': 'world', 'b': 'hello'}.transformMap(i, v, i == 'a' ? v.upperAscii() : v)`,
			unparsed: `{"a": "world", "b": "hello"}.transformMap(i, v, (i == "a") ? v.upperAscii() : v)`,
		},
		{
			name:     "transform list",
			expr:     `[1.0, 2.0, 2.0].transformList(i, v, i / 2.0 == 1.0)`,
			unparsed: `[1.0, 2.0, 2.0].transformList(i, v, i / 2.0 == 1.0)`,
		},
		{
			name:     "existsOne",
			expr:     `{'a': 'b', 'c': 'd'}.existsOne(k, v, k == 'b' || v == 'b')`,
			unparsed: `{"a": "b", "c": "d"}.existsOne(k, v, k == "b" || v == "b")`,
		},
		{
			name:     "exists",
			expr:     `{'a': 'b', 'c': 'd'}.exists(k, v, k == 'b' || v == 'b')`,
			unparsed: `{"a": "b", "c": "d"}.exists(k, v, k == "b" || v == "b")`,
		},
		{
			name:     "all",
			expr:     `[null, null, 'hello', string].all(i, v, i == 0 || type(v) != int)`,
			unparsed: `[null, null, "hello", string].all(i, v, i == 0 || type(v) != int)`,
		},
	}
	env := testCompreEnv(t)
	for _, tst := range tests {
		tc := tst
		t.Run(tc.name, func(t *testing.T) {
			ast, iss := env.Parse(tc.expr)
			if iss.Err() != nil {
				t.Fatalf("env.Parse(%q) failed: %v", tc.expr, iss.Err())
			}
			unparsed, err := cel.AstToString(ast)
			if err != nil {
				t.Fatalf("cel.AstToString() failed: %v", err)
			}
			if unparsed != tc.unparsed {
				t.Errorf("cel.AstToString() got %q, wanted %q", unparsed, tc.unparsed)
			}
		})
	}
}

func TestTwoVarComprehensionsResidualAST(t *testing.T) {
	tests := []struct {
		name     string
		in       map[string]any
		varOpts  []cel.EnvOption
		unks     []*interpreter.AttributePattern
		expr     string
		residual string
	}{
		{
			name: "transform map entry residual compare",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.DynType)),
				cel.Variable("y", cel.IntType),
			},
			in: map[string]any{
				"x": []any{0, uint(1)},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y")},
			expr:     `x.transformMapEntry(i, v, {v: i}).size() < y`,
			residual: `2 < y`,
		},
		{
			name: "transform map entry residual transform",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.DynType)),
				cel.Variable("y", cel.IntType),
			},
			in: map[string]any{
				"x": []any{0, uint(1)},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y")},
			expr:     `x.transformMapEntry(i, v, i < y, {v: i})`,
			residual: `[0, 1u].transformMapEntry(i, v, i < y, {v: i})`,
		},
		{
			name: "nested exists unknown inner range",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.IntType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.DynType)),
			},
			in: map[string]any{
				"x": []any{1, 2, 3},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y")},
			expr:     `x.exists(val, y.exists(key, _, key == val))`,
			residual: `[1, 2, 3].exists(val, y.exists(key, _, key == val))`,
		},
		{
			name: "nested exists unknown inner range",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.IntType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.DynType)),
			},
			in: map[string]any{
				"y": map[int]string{1: "hi", 2: "hello", 3: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("x")},
			expr:     `x.exists(val, y.exists(key, _, key == val))`,
			residual: `x.exists(val, y.exists(key, _, key == val))`,
		},
		{
			name: "nested exists unknown outer range with extra predicate",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.IntType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.DynType)),
			},
			in: map[string]any{
				"y": map[int]string{1: "hi", 2: "hello", 3: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("x")},
			expr:     `x.exists(val, y.exists(key, _, key == val)) && y.all(key, val, val.startsWith('h'))`,
			residual: `x.exists(val, y.exists(key, _, key == val))`,
		},
		{
			name: "nested exists partial unknown outer range",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.IntType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.DynType)),
			},
			in: map[string]any{
				"x": []int{42, 0, 43},
				"y": map[int]string{1: "hi", 2: "hello", 3: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("x").QualInt(1)},
			expr:     `x.exists(val, y.exists(key, _, key == val)) || x[0] == 0 || x[1] == 1 || x[2] == 2`,
			residual: `x.exists(val, y.exists(key, _, key == val)) || x[1] == 1`,
		},
		{
			name: "nested exists partial unknown outer range with optionals",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.IntType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.DynType)),
			},
			in: map[string]any{
				"x": []int{42, 0, 43},
				"y": map[int]string{1: "hi", 2: "hello", 3: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("x").QualInt(1)},
			expr:     `x.exists(val, y.exists(key, _, key == val)) || (x[?0].hasValue() && x[?1].hasValue())`,
			residual: `x.exists(val, y.exists(key, _, key == val)) || x[?1].hasValue()`,
		},
		{
			name: "inner value partial unknown two-var",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.StringType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.StringType)),
			},
			in: map[string]any{
				"x": []string{"howdy", "hello", "hi"},
				"y": map[int]string{0: "hi", 1: "hello", 2: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y").QualInt(1)},
			expr:     `x.exists(key, val, y[?key] == optional.of(val))`,
			residual: `["howdy", "hello", "hi"].exists(key, val, y[?key] == optional.of(val))`,
		},
		{
			name: "inner value partial unknown one-var",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.StringType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.StringType)),
			},
			in: map[string]any{
				"x": []string{"howdy"},
				"y": map[int]string{0: "hello"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("x").QualInt(0)},
			expr:     `y.exists(key, y[?key] == x[?key])`,
			residual: `{0: "hello"}.exists(key, y[?key] == x[?key])`,
		},
		{
			name: "simple bind",
			varOpts: []cel.EnvOption{
				cel.Variable("y", cel.MapType(cel.IntType, cel.StringType)),
			},
			in: map[string]any{
				"y": map[int]string{0: "hi", 1: "hello", 2: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y").QualInt(1)},
			expr:     `cel.bind(z, y[0], z + y[1])`,
			residual: `cel.bind(z, "hi", "hi" + y[1])`,
		},
		{
			name: "bind with comprehension",
			varOpts: []cel.EnvOption{
				cel.Variable("x", cel.ListType(cel.StringType)),
				cel.Variable("y", cel.MapType(cel.IntType, cel.StringType)),
			},
			in: map[string]any{
				"x": []string{"hi", "hello", "howdy"},
				"y": map[int]string{0: "hi", 1: "hello", 2: "howdy"},
			},
			unks:     []*interpreter.AttributePattern{cel.AttributePattern("y").QualInt(1)},
			expr:     `cel.bind(z, y[0], x.all(i, val, val == z || optional.of(val) == y[?i]))`,
			residual: `cel.bind(z, "hi", ["hi", "hello", "howdy"].all(i, val, val == z || optional.of(val) == y[?i]))`,
		},
	}
	for _, tst := range tests {
		tc := tst
		t.Run(tc.name, func(t *testing.T) {
			env := testCompreEnv(t, tc.varOpts...)
			ast, iss := env.Compile(tc.expr)
			if iss.Err() != nil {
				t.Fatalf("env.Compile(%q) failed: %v", tc.expr, iss.Err())
			}
			prg, err := env.Program(ast,
				cel.EvalOptions(cel.OptTrackState, cel.OptPartialEval))
			if err != nil {
				t.Fatalf("env.Program() failed: %v", err)
			}
			unkVars, err := cel.PartialVars(tc.in, tc.unks...)
			if err != nil {
				t.Fatalf("PartialVars() failed: %v", err)
			}
			out, det, err := prg.Eval(unkVars)
			if !types.IsUnknown(out) {
				t.Fatalf("got %v, expected unknown", out)
			}
			if err != nil {
				t.Fatalf("prg.Eval() failed: %v", err)
			}
			residual, err := env.ResidualAst(ast, det)
			if err != nil {
				t.Fatalf("env.ResidualAst() failed: %v", err)
			}
			expr, err := cel.AstToString(residual)
			if err != nil {
				t.Fatalf("cel.AstToString() failed: %v", err)
			}
			if expr != tc.residual {
				t.Errorf("got expr: %s, wanted %s", expr, tc.residual)
			}
		})
	}
}

func testCompreEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
	t.Helper()
	baseOpts := []cel.EnvOption{
		TwoVarComprehensions(),
		Bindings(),
		Lists(),
		Strings(),
		cel.OptionalTypes(),
		cel.EnableMacroCallTracking(),
		cel.EnableHiddenAccumulatorName(true),
	}
	env, err := cel.NewEnv(append(baseOpts, opts...)...)
	if err != nil {
		t.Fatalf("cel.NewEnv(TwoVarComprehensions()) failed: %v", err)
	}
	return env
}
