// Copyright 2020 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 interpreter

import (
	"fmt"
	"testing"

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

// attr describes a simplified format for specifying common Attribute and Qualifier values for
// use in pattern matching tests.
type attr struct {
	// unchecked indicates whether the attribute has not been type-checked and thus not gone
	// the variable and function resolution step.
	unchecked bool
	// container simulates the expression container and is only relevant on 'unchecked' test inputs
	// as the container is used to resolve the potential fully qualified variable names represented
	// by an identifier or select expression.
	container string
	// variable name, fully qualified unless the attr is marked as unchecked=true
	name string
	// quals contains a list of static qualifiers.
	quals []any
}

// patternTest describes a pattern, and a set of matches and misses for the pattern to highlight
// what the pattern will and will not match.
type patternTest struct {
	pattern *AttributePattern
	matches []attr
	misses  []attr
}

var patternTests = map[string]patternTest{
	"var": {
		pattern: NewAttributePattern("var"),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{"field"}},
		},
		misses: []attr{
			{name: "ns.var"},
		},
	},
	"var_namespace": {
		pattern: NewAttributePattern("ns.app.var"),
		matches: []attr{
			{name: "ns.app.var"},
			{name: "ns.app.var", quals: []any{int64(0)}},
			{
				name:      "ns",
				quals:     []any{"app", "var", "foo"},
				container: "ns.app",
				unchecked: true,
			},
		},
		misses: []attr{
			{name: "ns.var"},
			{
				name:      "ns",
				quals:     []any{"var"},
				container: "ns.app",
				unchecked: true,
			},
		},
	},
	"var_field": {
		pattern: NewAttributePattern("var").QualString("field"),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{"field"}},
			{name: "var", quals: []any{"field"}, unchecked: true},
			{name: "var", quals: []any{"field", uint64(1)}},
		},
		misses: []attr{
			{name: "var", quals: []any{"other"}},
		},
	},
	"var_index": {
		pattern: NewAttributePattern("var").QualInt(0),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{int64(0)}},
			{name: "var", quals: []any{float64(0)}},
			{name: "var", quals: []any{int64(0), false}},
			{name: "var", quals: []any{uint64(0)}},
		},
		misses: []attr{
			{name: "var", quals: []any{int64(1), false}},
		},
	},
	"var_index_uint": {
		pattern: NewAttributePattern("var").QualUint(1),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{uint64(1)}},
			{name: "var", quals: []any{uint64(1), true}},
			{name: "var", quals: []any{int64(1), false}},
		},
		misses: []attr{
			{name: "var", quals: []any{uint64(0)}},
		},
	},
	"var_index_bool": {
		pattern: NewAttributePattern("var").QualBool(true),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{true}},
			{name: "var", quals: []any{true, "name"}},
		},
		misses: []attr{
			{name: "var", quals: []any{false}},
			{name: "none"},
		},
	},
	"var_wildcard": {
		pattern: NewAttributePattern("ns.var").Wildcard(),
		matches: []attr{
			{name: "ns.var"},
			// The unchecked attributes consider potential namespacing and field selection
			// when testing variable names.
			{
				name:      "var",
				quals:     []any{true},
				container: "ns",
				unchecked: true,
			},
			{
				name:      "var",
				quals:     []any{"name"},
				container: "ns",
				unchecked: true,
			},
			{
				name:      "var",
				quals:     []any{"name"},
				container: "ns",
				unchecked: true,
			},
		},
		misses: []attr{
			{name: "var", quals: []any{false}},
			{name: "none"},
		},
	},
	"var_wildcard_field": {
		pattern: NewAttributePattern("var").Wildcard().QualString("field"),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{true}},
			{name: "var", quals: []any{int64(10), "field"}},
		},
		misses: []attr{
			{name: "var", quals: []any{int64(10), "other"}},
		},
	},
	"var_wildcard_wildcard": {
		pattern: NewAttributePattern("var").Wildcard().Wildcard(),
		matches: []attr{
			{name: "var"},
			{name: "var", quals: []any{true}},
			{name: "var", quals: []any{int64(10), "field"}},
		},
		misses: []attr{
			{name: "none"},
		},
	},
}

func TestAttributePattern_UnknownResolution(t *testing.T) {
	reg := newTestRegistry(t)
	for nm, tc := range patternTests {
		tst := tc
		t.Run(nm, func(t *testing.T) {
			for i, match := range tst.matches {
				m := match
				t.Run(fmt.Sprintf("match[%d]", i), func(t *testing.T) {
					var err error
					cont := containers.DefaultContainer
					if m.unchecked {
						cont, err = containers.NewContainer(containers.Name(m.container))
						if err != nil {
							t.Fatal(err)
						}
					}
					fac := NewPartialAttributeFactory(cont, reg, reg)
					attr := genAttr(fac, m)
					partVars, _ := NewPartialActivation(EmptyActivation(), tst.pattern)
					val, err := attr.Resolve(partVars)
					if err != nil {
						t.Fatalf("Got error: %s, wanted unknown", err)
					}
					_, isUnk := val.(*types.Unknown)
					if !isUnk {
						t.Fatalf("Got value %v, wanted unknown", val)
					}
				})
			}
			for i, miss := range tst.misses {
				m := miss
				t.Run(fmt.Sprintf("miss[%d]", i), func(t *testing.T) {
					cont := containers.DefaultContainer
					if m.unchecked {
						var err error
						cont, err = containers.NewContainer(containers.Name(m.container))
						if err != nil {
							t.Fatal(err)
						}
					}
					fac := NewPartialAttributeFactory(cont, reg, reg)
					attr := genAttr(fac, m)
					partVars, _ := NewPartialActivation(EmptyActivation(), tst.pattern)
					val, err := attr.Resolve(partVars)
					if err == nil {
						t.Fatalf("Got value: %s, wanted error", val)
					}
				})
			}
		})
	}
}

func TestAttributePattern_CrossReference(t *testing.T) {
	reg := newTestRegistry(t)
	fac := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg)
	a := fac.AbsoluteAttribute(1, "a")
	b := fac.AbsoluteAttribute(2, "b")
	a.AddQualifier(b)

	// Ensure that var a[b], the dynamic index into var 'a' is the unknown value
	// returned from attribute resolution.
	partVars, _ := NewPartialActivation(
		map[string]any{"a": []int64{1, 2}},
		NewAttributePattern("b"))
	val, err := a.Resolve(partVars)
	if err != nil {
		t.Fatal(err)
	}
	if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) {
		t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val)
	}

	// Ensure that a[b], the dynamic index into var 'a' is the unknown value
	// returned from attribute resolution. Note, both 'a' and 'b' have unknown attribute
	// patterns specified. This changes the evaluation behavior slightly, but the end
	// result is the same.
	partVars, _ = NewPartialActivation(
		map[string]any{"a": []int64{1, 2}},
		NewAttributePattern("a").QualInt(0),
		NewAttributePattern("b"))
	val, err = a.Resolve(partVars)
	if err != nil {
		t.Fatal(err)
	}
	if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) {
		t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val)
	}

	// Note, that only 'a[0].c' will result in an unknown result since both 'a' and 'b'
	// have values. However, since the attribute being pattern matched is just 'a.b',
	// the outcome will indicate that 'a[b]' is unknown.
	partVars, _ = NewPartialActivation(
		map[string]any{"a": []int64{1, 2}, "b": 0},
		NewAttributePattern("a").QualInt(0).QualString("c"))
	val, err = a.Resolve(partVars)
	if err != nil {
		t.Fatal(err)
	}
	unkAttr := types.NewAttributeTrail("a")
	types.QualifyAttribute[int64](unkAttr, 0)
	wantUnk := types.NewUnknown(2, unkAttr)
	if !wantUnk.Contains(val.(*types.Unknown)) {
		t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk)
	}

	// Test a positive case that returns a valid value even though the attribugte factory
	// is the partial attribute factory.
	partVars, _ = NewPartialActivation(
		map[string]any{"a": []int64{1, 2}, "b": 0})
	val, err = a.Resolve(partVars)
	if err != nil {
		t.Fatal(err)
	}
	if val != int64(1) {
		t.Errorf("Got %v, wanted 1 for a[b]", val)
	}

	// Ensure the unknown attribute id moves when the attribute becomes more specific.
	partVars, _ = NewPartialActivation(
		map[string]any{"a": []int64{1, 2}, "b": 0},
		NewAttributePattern("a").QualInt(0).QualString("c"))
	// Qualify a[b] with 'c', a[b].c
	c, _ := fac.NewQualifier(nil, 3, "c", false)
	a.AddQualifier(c)
	// The resolve step should return unknown
	val, err = a.Resolve(partVars)
	if err != nil {
		t.Fatal(err)
	}
	unkAttr = types.NewAttributeTrail("a")
	types.QualifyAttribute[int64](unkAttr, 0)
	types.QualifyAttribute[string](unkAttr, "c")
	wantUnk = types.NewUnknown(3, unkAttr)
	if !wantUnk.Contains(val.(*types.Unknown)) {
		t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk)
	}
}

func genAttr(fac AttributeFactory, a attr) Attribute {
	id := int64(1)
	var attr Attribute
	if a.unchecked {
		attr = fac.MaybeAttribute(1, a.name)
	} else {
		attr = fac.AbsoluteAttribute(1, a.name)
	}
	for _, q := range a.quals {
		qual, _ := fac.NewQualifier(nil, id, q, false)
		attr.AddQualifier(qual)
		id++
	}
	return attr
}
