// Copyright 2020 CUE Authors
//
// 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 cue_test

import (
	"fmt"
	"reflect"
	"testing"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/internal/cuetdtest"
)

func TestPaths(t *testing.T) {
	type testCase struct {
		in   string
		path cue.Path
		out  string
		str  string
		err  bool
	}
	testCases := []testCase{{
		in:   `list: [...int]`,
		path: cue.MakePath(cue.Str("list"), cue.AnyIndex),
		out:  "int",
		str:  "list.[_]",
	}, {
		in:   `#Foo: a: b: 1`,
		path: cue.MakePath(cue.Def("#Foo"), cue.Str("a"), cue.Str("b")),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		in:   `#Foo: a: b: 1`,
		path: cue.ParsePath(`#Foo.a.b`),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		in:   `"#Foo": c: d: 2`,
		path: cue.ParsePath(`"#Foo".c.d`),
		out:  "2",
		str:  `"#Foo".c.d`,
	}, {
		// fallback Def(Foo) -> Def(#Foo)
		in:   `#Foo: a: b: 1`,
		path: cue.MakePath(cue.Def("Foo"), cue.Str("a"), cue.Str("b")),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		in:   `b: [4, 5, 6]`,
		path: cue.MakePath(cue.Str("b"), cue.Index(2)),
		out:  "6",
		str:  "b[2]", // #Foo.b.2
	}, {
		in:   `c: "#Foo": 7`,
		path: cue.MakePath(cue.Str("c"), cue.Str("#Foo")),
		out:  "7",
		str:  `c."#Foo"`,
	}, {
		in:   `_foo: b: 5`,
		path: cue.MakePath(cue.Hid("_foo", "_"), cue.Str("b")),
		out:  "5",
		str:  `_foo.b`,
	}, {
		in:   `#Foo: a: b: 1`,
		path: cue.ParsePath("#Foo.a.b"),
		str:  "#Foo.a.b",
		out:  "1",
	}, {
		in:   `#Foo: a: b: 1`,
		path: cue.ParsePath("#Foo.a.c"),
		str:  "#Foo.a.c",
		out:  `_|_ // field not found: c`,
	}, {
		in:   `b: [4, 5, 6]`,
		path: cue.ParsePath(`b[2]`),
		str:  `b[2]`,
		out:  "6",
	}, {
		// Issue 3467: ParsePath must accept an index as the first element
		// of a path, so that the output of Path.String round-trips through
		// ParsePath.
		in:   `[4, 5, 6]`,
		path: cue.ParsePath(`[2]`),
		str:  `[2]`,
		out:  "6",
	}, {
		// Issue 3467: a leading index followed by a field selector.
		in:   `[{foo: 7}]`,
		path: cue.ParsePath(`[0].foo`),
		str:  `[0].foo`,
		out:  "7",
	}, {
		// Issue 3467: consecutive leading indices.
		in:   `[["a"], [10, 11]]`,
		path: cue.ParsePath(`[1][0]`),
		str:  `[1][0]`,
		out:  "10",
	}, {
		in:   `c: "#Foo": 7`,
		path: cue.ParsePath(`c."#Foo"`),
		str:  `c."#Foo"`,
		out:  "7",
	}, {
		path: cue.ParsePath("foo._foo"),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid path: hidden label _foo not allowed; use Hid to construct a selector for hidden fields`,
	}, {
		path: cue.ParsePath(`c."#Foo`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // string literal not terminated`,
	}, {
		path: cue.ParsePath(`b[a]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // non-constant expression a`,
	}, {
		path: cue.ParsePath(`b['1']`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid string index '1'`,
	}, {
		path: cue.ParsePath(`b[3T]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // int label out of range (3000000000000 not >=0 and <= 268435454)`,
	}, {
		path: cue.ParsePath(`b[3.3]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid literal 3.3`,
	}, {
		in:   `map: [string]: int`,
		path: cue.MakePath(cue.Str("map"), cue.AnyString),
		out:  "int",
		str:  "map.[_]",
	}, {
		in:   `list: [...int]`,
		path: cue.MakePath(cue.Str("list"), cue.AnyIndex),
		out:  "int",
		str:  "list.[_]",
	}, {
		// Issue 2060
		in: `
			let X = {a: b: 0}
			x: y: X.a
		`,
		path: cue.ParsePath("x.y"),
		out:  "{\n\tb: 0\n}",
		str:  "x.y",
	}, {
		// Issue 2060
		in: `
			let X = {a: b: 0}
			x: y: X.a
		`,
		path: cue.ParsePath("x.y.b"),
		out:  "0",
		str:  "x.y.b",
	}, {
		// Issue 3577
		in: `
			pkg: z
			z: y: "hello"
		`,
		path: cue.ParsePath("pkg.y"),
		out:  `"hello"`,
		str:  "pkg.y", // show original path, not structure shared path.
	}, {
		// Issue 3922
		in: `
			out: #Output
			#Output: name: _data.name
			_data: name: "one"
		`,
		path: cue.ParsePath("out.name"),
		out:  `"one"`,
		str:  "out.name",
	}}

	cuetdtest.Run(t, testCases, func(t *cuetdtest.T, tc *testCase) {
		ctx := t.M.CueContext()
		val := mustCompile(t, ctx, tc.in)

		t.Equal(tc.path.Err() != nil, tc.err)

		w := val.LookupPath(tc.path)

		t.Equal(fmt.Sprint(w), tc.out)

		if w.Err() != nil {
			return
		}

		t.Equal(w.Path().String(), tc.str)
	})
}

// TestWalkPath is a more comprehensive table-driven test.
func TestWalkPath(t *testing.T) {
	ctx := cuecontext.New()

	testCases := []struct {
		name      string
		cueInput  string
		wantPaths []string
	}{
		{
			name: "issue 3922",
			cueInput: `
				out: #Output
				#Output: name: _data.name
				_data: name: "one"
			`,
			wantPaths: []string{
				"", // root
				"out",
				"out.name",
			},
		},
		{
			name: "simple struct",
			cueInput: `
				b: {
					d: 3
					c: 2
				}
				a: 1
			`,
			wantPaths: []string{
				"", // root
				"b",
				"b.d",
				"b.c",
				"a",
			},
		},
		{
			name: "struct with list",
			cueInput: `
				l: [10, {y: 30, x: 20}]
			`,
			wantPaths: []string{
				"", // root
				"l",
				"l[0]",
				"l[1]",
				"l[1].y",
				"l[1].x",
			},
		},
		{
			name:     "root list",
			cueInput: `[10, {x: 20}]`,
			wantPaths: []string{
				"", // root
				"[0]",
				"[1]",
				"[1].x",
			},
		},
		{
			name:     "root literal string",
			cueInput: `"hello"`,
			wantPaths: []string{
				"", // root
			},
		},
		{
			name:     "root literal int",
			cueInput: `123`,
			wantPaths: []string{
				"", // root
			},
		},
		{
			name:     "empty struct",
			cueInput: `{}`,
			wantPaths: []string{
				"", // root
			},
		},
		{
			name: "struct with various field types",
			cueInput: `
				c: _h
				_h: #D
				#D: b: c: 3
				a: _g
				_g: #B
				#B: x: "definition B"
			`,
			// Order: regular (a, c), definitions (#B, #D), hidden (_g, _h)
			wantPaths: []string{
				"", // root
				"c",
				"c.b",
				"c.b.c",
				"a",
				"a.x",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			v := ctx.CompileString(tc.cueInput)
			if err := v.Err(); err != nil {
				t.Fatalf("CompileString failed for input\n%s\nError: %v", tc.cueInput, err)
			}

			var gotPaths []string
			v.Walk(func(val cue.Value) bool {
				gotPaths = append(gotPaths, val.Path().String())
				return true
			}, nil)

			if !reflect.DeepEqual(gotPaths, tc.wantPaths) {
				t.Errorf("Walk() paths mismatch for input\n%s\ngot:  %#v\nwant: %#v", tc.cueInput, gotPaths, tc.wantPaths)
			}
		})
	}
}

var selectorTests = []struct {
	sel          cue.Selector
	stype        cue.SelectorType
	string       string
	unquoted     string
	index        int
	isHidden     bool
	isConstraint bool
	isDefinition bool
	isString     bool
	pkgPath      string
}{{
	sel:      cue.Str("foo"),
	stype:    cue.StringLabel,
	string:   "foo",
	unquoted: "foo",
	isString: true,
}, {
	sel:      cue.Str("_foo"),
	stype:    cue.StringLabel,
	string:   `"_foo"`,
	unquoted: "_foo",
	isString: true,
}, {
	sel:      cue.Str(`a "b`),
	stype:    cue.StringLabel,
	string:   `"a \"b"`,
	unquoted: `a "b`,
	isString: true,
}, {
	sel:    cue.Index(5),
	stype:  cue.IndexLabel,
	string: "5",
	index:  5,
}, {
	sel:          cue.Def("foo"),
	stype:        cue.DefinitionLabel,
	string:       "#foo",
	isDefinition: true,
}, {
	sel:          cue.Str("foo").Optional(),
	stype:        cue.StringLabel | cue.OptionalConstraint,
	string:       "foo?",
	unquoted:     "foo",
	isString:     true,
	isConstraint: true,
}, {
	sel:          cue.Str("foo").Required(),
	stype:        cue.StringLabel | cue.RequiredConstraint,
	string:       "foo!",
	unquoted:     "foo",
	isString:     true,
	isConstraint: true,
}, {
	sel:          cue.Def("foo").Required().Optional(),
	stype:        cue.DefinitionLabel | cue.OptionalConstraint,
	string:       "#foo?",
	isDefinition: true,
	isConstraint: true,
}, {
	sel:          cue.Def("foo").Optional().Required(),
	stype:        cue.DefinitionLabel | cue.RequiredConstraint,
	string:       "#foo!",
	isDefinition: true,
	isConstraint: true,
}, {
	sel:          cue.AnyString,
	stype:        cue.StringLabel | cue.PatternConstraint,
	string:       "[_]",
	isConstraint: true,
}, {
	sel:          cue.AnyIndex,
	stype:        cue.IndexLabel | cue.PatternConstraint,
	string:       "[_]",
	isConstraint: true,
}, {
	sel:      cue.Hid("_foo", "example.com"),
	stype:    cue.HiddenLabel,
	string:   "_foo",
	isHidden: true,
	pkgPath:  "example.com",
}, {
	sel:          cue.Hid("_#foo", "example.com"),
	stype:        cue.HiddenDefinitionLabel,
	string:       "_#foo",
	isHidden:     true,
	isDefinition: true,
	pkgPath:      "example.com",
}}

func TestSelector(t *testing.T) {
	for _, tc := range selectorTests {
		t.Run(tc.sel.String(), func(t *testing.T) {
			sel := tc.sel
			if got, want := sel.Type(), tc.stype; got != want {
				t.Errorf("unexpected type; got %v want %v", got, want)
			}
			if got, want := sel.String(), tc.string; got != want {
				t.Errorf("unexpected sel.String result; got %q want %q", got, want)
			}
			if tc.unquoted == "" {
				checkPanic(t, "Selector.Unquoted invoked on non-string label", func() {
					sel.Unquoted()
				})
			} else {
				if got, want := sel.Unquoted(), tc.unquoted; got != want {
					t.Errorf("unexpected sel.Unquoted result; got %q want %q", got, want)
				}
			}
			if sel.Type() != cue.IndexLabel {
				checkPanic(t, "Index called on non-index selector", func() {
					sel.Index()
				})
			} else {
				if got, want := sel.Index(), tc.index; got != want {
					t.Errorf("unexpected sel.Index result; got %v want %v", got, want)
				}
			}
			if got, want := sel.Type().IsHidden(), tc.isHidden; got != want {
				t.Errorf("unexpected sel.IsHidden result; got %v want %v", got, want)
			}
			if got, want := sel.IsConstraint(), tc.isConstraint; got != want {
				t.Errorf("unexpected sel.IsOptional result; got %v want %v", got, want)
			}
			if got, want := sel.IsString(), tc.isString; got != want {
				t.Errorf("unexpected sel.IsString result; got %v want %v", got, want)
			}
			if got, want := sel.IsDefinition(), tc.isDefinition; got != want {
				t.Errorf("unexpected sel.IsDefinition result; got %v want %v", got, want)
			}
			if got, want := sel.PkgPath(), tc.pkgPath; got != want {
				t.Errorf("unexpected sel.PkgPath result; got %v want %v", got, want)
			}
		})
	}
}

func TestSelectorTypeString(t *testing.T) {
	if got, want := cue.InvalidSelectorType.String(), "NoLabels"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := cue.PatternConstraint.String(), "PatternConstraint"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := (cue.StringLabel | cue.OptionalConstraint).String(), "StringLabel|OptionalConstraint"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := cue.SelectorType(255).String(), "StringLabel|IndexLabel|DefinitionLabel|HiddenLabel|HiddenDefinitionLabel|OptionalConstraint|RequiredConstraint|PatternConstraint"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
}

func TestPathAppend(t *testing.T) {
	testCases := []struct {
		name     string
		path     cue.Path
		selector cue.Selector
		want     string
	}{{
		name:     "append string to empty path",
		path:     cue.MakePath(),
		selector: cue.Str("foo"),
		want:     "foo",
	}, {
		name:     "append string to existing path",
		path:     cue.MakePath(cue.Str("a")),
		selector: cue.Str("b"),
		want:     "a.b",
	}, {
		name:     "append index to path",
		path:     cue.MakePath(cue.Str("list")),
		selector: cue.Index(0),
		want:     "list[0]",
	}, {
		name:     "append definition to path",
		path:     cue.MakePath(cue.Str("root")),
		selector: cue.Def("Foo"),
		want:     "root.#Foo",
	}, {
		name:     "append optional selector",
		path:     cue.MakePath(cue.Str("a")),
		selector: cue.Str("b").Optional(),
		want:     "a.b?",
	}, {
		name:     "append required selector",
		path:     cue.MakePath(cue.Str("a")),
		selector: cue.Str("b").Required(),
		want:     "a.b!",
	}, {
		name:     "append AnyString",
		path:     cue.MakePath(cue.Str("map")),
		selector: cue.AnyString,
		want:     "map.[_]",
	}, {
		name:     "append AnyIndex",
		path:     cue.MakePath(cue.Str("list")),
		selector: cue.AnyIndex,
		want:     "list.[_]",
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result := tc.path.Append(tc.selector)
			if got := result.String(); got != tc.want {
				t.Errorf("Path.Append().String() = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestSelectorCompare(t *testing.T) {
	// ordered is a list of selectors in expected sorted order.
	// Selectors on the same line (separated by the test logic below)
	// are expected to compare as equal.
	ordered := []cue.Selector{
		// String selectors, alphabetical; then optional, then required.
		cue.Str("a"),
		cue.Str("b"),
		cue.Str("z"),
		cue.Str("a").Optional(),
		cue.Str("a").Required(),
		// Definition selectors, alphabetical; then optional.
		cue.Def("A"),
		cue.Def("B"),
		cue.Def("B").Optional(),
		// Hidden selectors, by name then package.
		cue.Hid("_a", "p"),
		cue.Hid("_a", "q"),
		cue.Hid("_b", "p"),
		// Index selectors, numerical.
		cue.Index(0),
		cue.Index(1),
		cue.Index(100),
		// Any selectors.
		cue.AnyString,
		cue.AnyIndex,
	}

	for i, a := range ordered {
		if got := a.Compare(a); got != 0 {
			t.Errorf("Compare(%v, %v) = %d, want 0", a, a, got)
		}
		for _, b := range ordered[i+1:] {
			if got := a.Compare(b); got != -1 {
				t.Errorf("Compare(%v, %v) = %d, want -1", a, b, got)
			}
			if got := b.Compare(a); got != 1 {
				t.Errorf("Compare(%v, %v) = %d, want 1", b, a, got)
			}
		}
	}
}

func TestSelectorCompareNoAllocs(t *testing.T) {
	pairs := [][2]cue.Selector{
		{cue.Str("a"), cue.Str("b")},
		{cue.Def("X"), cue.Def("Y")},
		{cue.Index(0), cue.Index(1)},
		{cue.Hid("_a", "p"), cue.Hid("_b", "q")},
		{cue.Str("a"), cue.Def("X")},
		{cue.Str("a").Optional(), cue.Str("a").Required()},
		{cue.AnyString, cue.AnyIndex},
	}
	allocs := testing.AllocsPerRun(100, func() {
		for _, p := range pairs {
			p[0].Compare(p[1])
		}
	})
	if allocs != 0 {
		t.Errorf("Selector.Compare allocated %v times, want 0", allocs)
	}
}

func TestPathCompareNoAllocs(t *testing.T) {
	pairs := [][2]cue.Path{
		{cue.MakePath(cue.Str("a"), cue.Str("b")), cue.MakePath(cue.Str("a"), cue.Str("c"))},
		{cue.MakePath(cue.Str("a")), cue.MakePath(cue.Str("a"), cue.Str("b"))},
		{cue.MakePath(cue.Index(0)), cue.MakePath(cue.Def("X"))},
	}
	allocs := testing.AllocsPerRun(100, func() {
		for _, p := range pairs {
			p[0].Compare(p[1])
		}
	})
	if allocs != 0 {
		t.Errorf("Path.Compare allocated %v times, want 0", allocs)
	}
}

func TestPathCompare(t *testing.T) {
	ordered := []cue.Path{
		cue.MakePath(),
		cue.MakePath(cue.Str("a")),
		cue.MakePath(cue.Str("a"), cue.Str("a")),
		cue.MakePath(cue.Str("a"), cue.Str("b")),
		cue.MakePath(cue.Str("a"), cue.Def("X")),
		cue.MakePath(cue.Str("a"), cue.Index(0)),
		cue.MakePath(cue.Str("b")),
		cue.MakePath(cue.Def("X")),
		cue.MakePath(cue.Def("X"), cue.Str("a")),
		cue.MakePath(cue.Index(0)),
		cue.MakePath(cue.Index(0), cue.Str("a")),
		cue.MakePath(cue.Index(1)),
	}

	for i, a := range ordered {
		if got := a.Compare(a); got != 0 {
			t.Errorf("Compare(%v, %v) = %d, want 0", a, a, got)
		}
		for _, b := range ordered[i+1:] {
			if got := a.Compare(b); got != -1 {
				t.Errorf("Compare(%v, %v) = %d, want -1", a, b, got)
			}
			if got := b.Compare(a); got != 1 {
				t.Errorf("Compare(%v, %v) = %d, want 1", b, a, got)
			}
		}
	}
}

func checkPanic(t *testing.T, wantPanicStr string, f func()) {
	gotPanicStr := ""
	func() {
		defer func() {
			e := recover()
			if e == nil {
				t.Errorf("function did not panic")
				return
			}
			gotPanicStr = fmt.Sprint(e)
		}()
		f()
	}()
	if got, want := gotPanicStr, wantPanicStr; got != want {
		t.Errorf("unexpected panic message; got %q want %q", got, want)
	}
}
