// 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

import (
	"fmt"
	"testing"
)

func TestPaths(t *testing.T) {
	var r Runtime
	inst, _ := r.Compile("", `
		#Foo:   a: b: 1
		"#Foo": c: d: 2
		_foo: b: 5
		a: 3
		b: [4, 5, 6]
		c: "#Foo": 7
		map: [string]: int
		list: [...int]

		// Issue 2060
		let X = {a: b: 0}
		x: y: X.a
	`)
	testCases := []struct {
		path Path
		out  string
		str  string
		err  bool
	}{{
		path: MakePath(Str("list"), AnyIndex),
		out:  "int",
		str:  "list.[_]",
	}, {

		path: MakePath(Def("#Foo"), Str("a"), Str("b")),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		path: ParsePath(`#Foo.a.b`),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		path: ParsePath(`"#Foo".c.d`),
		out:  "2",
		str:  `"#Foo".c.d`,
	}, {
		// fallback Def(Foo) -> Def(#Foo)
		path: MakePath(Def("Foo"), Str("a"), Str("b")),
		out:  "1",
		str:  "#Foo.a.b",
	}, {
		path: MakePath(Str("b"), Index(2)),
		out:  "6",
		str:  "b[2]", // #Foo.b.2
	}, {
		path: MakePath(Str("c"), Str("#Foo")),
		out:  "7",
		str:  `c."#Foo"`,
	}, {
		path: MakePath(Hid("_foo", "_"), Str("b")),
		out:  "5",
		str:  `_foo.b`,
	}, {
		path: ParsePath("#Foo.a.b"),
		str:  "#Foo.a.b",
		out:  "1",
	}, {
		path: ParsePath("#Foo.a.c"),
		str:  "#Foo.a.c",
		out:  `_|_ // field not found: c`,
	}, {
		path: ParsePath(`b[2]`),
		str:  `b[2]`,
		out:  "6",
	}, {
		path: ParsePath(`c."#Foo"`),
		str:  `c."#Foo"`,
		out:  "7",
	}, {
		path: ParsePath("foo._foo"),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid path: hidden label _foo not allowed`,
	}, {
		path: ParsePath(`c."#Foo`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // string literal not terminated`,
	}, {
		path: ParsePath(`b[a]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // non-constant expression a`,
	}, {
		path: ParsePath(`b['1']`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid string index '1'`,
	}, {
		path: ParsePath(`b[3T]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // int label out of range (3000000000000 not >=0 and <= 268435454)`,
	}, {
		path: ParsePath(`b[3.3]`),
		str:  "_|_",
		err:  true,
		out:  `_|_ // invalid literal 3.3`,
	}, {
		path: MakePath(Str("map"), AnyString),
		out:  "int",
		str:  "map.[_]",
	}, {
		path: MakePath(Str("list"), AnyIndex),
		out:  "int",
		str:  "list.[_]",
	}, {
		path: ParsePath("x.y"),
		out:  "{\n\tb: 0\n}",
		str:  "x.y",
	}, {
		path: ParsePath("x.y.b"),
		out:  "0",
		str:  "x.y.b",
	}}

	v := inst.Value()
	for _, tc := range testCases {
		t.Run(tc.str, func(t *testing.T) {
			if gotErr := tc.path.Err() != nil; gotErr != tc.err {
				t.Errorf("error: got %v; want %v", gotErr, tc.err)
			}

			w := v.LookupPath(tc.path)

			if got := fmt.Sprint(w); got != tc.out {
				t.Errorf("Value: got %v; want %v", got, tc.out)
			}

			if got := tc.path.String(); got != tc.str {
				t.Errorf("String: got %v; want %v", got, tc.str)
			}

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

			if got := w.Path().String(); got != tc.str {
				t.Errorf("Path: got %v; want %v", got, tc.str)
			}
		})
	}
}

var selectorTests = []struct {
	sel          Selector
	stype        SelectorType
	string       string
	unquoted     string
	index        int
	isHidden     bool
	isConstraint bool
	isDefinition bool
	isString     bool
	pkgPath      string
}{{
	sel:      Str("foo"),
	stype:    StringLabel,
	string:   "foo",
	unquoted: "foo",
	isString: true,
}, {
	sel:      Str("_foo"),
	stype:    StringLabel,
	string:   `"_foo"`,
	unquoted: "_foo",
	isString: true,
}, {
	sel:      Str(`a "b`),
	stype:    StringLabel,
	string:   `"a \"b"`,
	unquoted: `a "b`,
	isString: true,
}, {
	sel:    Index(5),
	stype:  IndexLabel,
	string: "5",
	index:  5,
}, {
	sel:          Def("foo"),
	stype:        DefinitionLabel,
	string:       "#foo",
	isDefinition: true,
}, {
	sel:          Str("foo").Optional(),
	stype:        StringLabel | OptionalConstraint,
	string:       "foo?",
	unquoted:     "foo",
	isString:     true,
	isConstraint: true,
}, {
	sel:          Str("foo").Required(),
	stype:        StringLabel | RequiredConstraint,
	string:       "foo!",
	unquoted:     "foo",
	isString:     true,
	isConstraint: true,
}, {
	sel:          Def("foo").Required().Optional(),
	stype:        DefinitionLabel | OptionalConstraint,
	string:       "#foo?",
	isDefinition: true,
	isConstraint: true,
}, {
	sel:          Def("foo").Optional().Required(),
	stype:        DefinitionLabel | RequiredConstraint,
	string:       "#foo!",
	isDefinition: true,
	isConstraint: true,
}, {
	sel:          AnyString,
	stype:        StringLabel | PatternConstraint,
	string:       "[_]",
	isConstraint: true,
}, {
	sel:          AnyIndex,
	stype:        IndexLabel | PatternConstraint,
	string:       "[_]",
	isConstraint: true,
}, {
	sel:      Hid("_foo", "example.com"),
	stype:    HiddenLabel,
	string:   "_foo",
	isHidden: true,
	pkgPath:  "example.com",
}, {
	sel:          Hid("_#foo", "example.com"),
	stype:        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() != 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 := InvalidSelectorType.String(), "NoLabels"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := PatternConstraint.String(), "PatternConstraint"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := (StringLabel | OptionalConstraint).String(), "StringLabel|OptionalConstraint"; got != want {
		t.Errorf("unexpected SelectorType.String result; got %q want %q", got, want)
	}
	if got, want := 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 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)
	}
}
