// Copyright 2016 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 storage

import (
	"errors"
	"math"
	"testing"

	"fmt"

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

func TestNewPathForString(t *testing.T) {

	tests := []struct {
		input  string
		result Path
		ok     bool
	}{
		{"", nil, false},
		{"foo", nil, false},
		{"/", RootPath, true},
		{"/", nil, true},
		{"/foo", Path{"foo"}, true},
		{"/foo/bar", Path{"foo", "bar"}, true},
	}

	for _, tc := range tests {
		result, ok := ParsePath(tc.input)
		if (tc.ok != ok) || !tc.result.Equal(result) {
			t.Errorf("For %v wanted (%v, %v) but got (%v, %v)", tc.input, tc.result, tc.ok, result, ok)
		}
	}
}

func TestNewPathForRef(t *testing.T) {
	tests := []struct {
		input  ast.Ref
		result Path
		err    error
	}{
		{ast.Ref{}, nil, errors.New("empty reference (indicates error in caller)")},
		{ast.MustParseRef("data.foo[x]"), nil, errors.New("unresolved reference (indicates error in caller): data.foo[x]")},
		{ast.MustParseRef("data.foo[true]"), nil, &Error{
			Code:    NotFoundErr,
			Message: fmt.Sprintf("%v: does not exist", ast.MustParseRef("data.foo[true]")),
		}},
		{ast.MustParseRef("data.foo[[1, 2]]"), nil, fmt.Errorf("composites cannot be base document keys: %v", ast.MustParseRef("data.foo[[1, 2]]"))},
		{ast.MustParseRef("data.foo[{1, 2}]"), nil, fmt.Errorf("composites cannot be base document keys: %v", ast.MustParseRef("data.foo[{1, 2}]"))},
		{ast.MustParseRef(`data.foo[{"foo": 2}]`), nil, fmt.Errorf("composites cannot be base document keys: %v", ast.MustParseRef(`data.foo[{"foo": 2}]`))},

		{ast.MustParseRef("data"), RootPath, nil},
		{ast.MustParseRef("data.foo"), Path{"foo"}, nil},
		{ast.MustParseRef("data.foo[1]"), Path{"foo", "1"}, nil},
		{ast.MustParseRef("data.foo.bar"), Path{"foo", "bar"}, nil},
	}

	for _, tc := range tests {
		result, err := NewPathForRef(tc.input)
		if tc.err != nil && tc.err.Error() != err.Error() {
			t.Errorf("For %v expected %v but got %v", tc.input, tc.err, err)
		} else if !result.Equal(tc.result) {
			t.Errorf("For %v expected %v but got %v", tc.input, tc.result, result)
		}
	}
}

func TestNewPathForStringEscaped(t *testing.T) {
	tests := []struct {
		input  string
		result Path
		ok     bool
	}{
		{
			input: "",
			ok:    false,
		},
		{
			input:  "/foo/bar", // no escaping
			result: Path{"foo", "bar"},
			ok:     true,
		},
		{
			input:  "/foo%2Fbar/baz", // single escape
			result: Path{"foo/bar", "baz"},
			ok:     true,
		},
		{
			input:  "/foo%2F%2Fbar/baz", // double escape
			result: Path{"foo//bar", "baz"},
			ok:     true,
		},
		{
			input:  "/foo%%%%bar",
			result: nil, // invalid escaping
			ok:     false,
		},
	}

	for _, tc := range tests {
		result, ok := ParsePathEscaped(tc.input)
		if (tc.ok != ok) || !tc.result.Equal(result) {
			t.Errorf("For %v wanted (%v, %v) but got (%v, %v)", tc.input, tc.result, tc.ok, result, ok)
		}
	}
}

func TestPathCompare(t *testing.T) {
	tests := []struct {
		a      Path
		b      Path
		result int
	}{
		{RootPath, RootPath, 0},
		{RootPath, Path{"x"}, -1},
		{Path{"x"}, RootPath, 1},
		{Path{"x"}, Path{"x"}, 0},
		{Path{"x"}, Path{"y"}, -1},
		{Path{"x"}, Path{"w"}, 1},
		{Path{"x"}, Path{"wz"}, 1},
		{Path{"x"}, Path{"xx"}, -1},
		{Path{"xx"}, Path{"x"}, 1},
		{Path{"xx"}, Path{"xx"}, 0},
		{Path{"xy"}, Path{"xx"}, 1},
	}
	for _, tc := range tests {
		result := tc.a.Compare(tc.b)
		if result != tc.result {
			t.Errorf("For %v.Compare(%v) expected %v but got %v", tc.a, tc.b, tc.result, result)
		}
	}
}

func TestPathEqual(t *testing.T) {
	tests := []struct {
		a      Path
		b      Path
		result bool
	}{
		{RootPath, RootPath, true},
		{RootPath, Path{"foo"}, false},
		{Path{"foo"}, RootPath, false},
		{Path{"foo", "bar"}, Path{"foo"}, false},
		{Path{"foo", "bar"}, Path{"foo", "bar"}, true},
	}
	for _, tc := range tests {
		result := tc.a.Equal(tc.b)
		if result != tc.result {
			t.Errorf("For %v.HasPrefix(%v) expected %v but got %v", tc.a, tc.b, tc.result, result)
		}
	}
}

func TestPathHasPrefix(t *testing.T) {
	tests := []struct {
		a      Path
		b      Path
		result bool
	}{
		{RootPath, RootPath, true},
		{RootPath, Path{"foo"}, false},
		{Path{"foo"}, RootPath, true},
		{Path{"foo"}, Path{"bar"}, false},
		{Path{"bar"}, Path{"foo"}, false},
		{Path{"foo", "bar"}, Path{"foo"}, true},
		{Path{"foo", "bar"}, Path{"foo", "bar"}, true},
		{Path{"foo", "bar"}, Path{"foo", "bar", "baz"}, false},
		{Path{"foo", "bar", "baz"}, RootPath, true},
	}
	for _, tc := range tests {
		result := tc.a.HasPrefix(tc.b)
		if result != tc.result {
			t.Errorf("For %v.HasPrefix(%v) expected %v but got %v", tc.a, tc.b, tc.result, result)
		}
	}
}

func TestPathRef(t *testing.T) {
	tests := []struct {
		path string
		head string
		ref  string
	}{
		{"/", "data", "data"},
		{"/foo/bar", "data", "data.foo.bar"},
		{"/foo/bar/3", "data", "data.foo.bar[3]"},
		{fmt.Sprintf("/foo/bar/%d", math.MaxInt64), "data", fmt.Sprintf("data.foo.bar[%d]", math.MaxInt64)},
	}
	for _, tc := range tests {
		path := MustParsePath(tc.path)
		head := ast.VarTerm(tc.head)
		ref := ast.MustParseRef(tc.ref)
		result := path.Ref(head)
		if !result.Equal(ref) {
			t.Errorf("Expected %v but got %v", ref, result)
		}
	}
}

func TestPathPolicyID(t *testing.T) {
	tests := []struct {
		path Path
		exp  string
	}{
		{RootPath, ""},
		{Path{"foo", "bar", "policy.rego"}, "foo/bar/policy.rego"},
		// Segments are joined verbatim, unlike String(), which escapes them.
		{Path{"foo", "foo bar", "policy.rego"}, "foo/foo bar/policy.rego"},
		{Path{"foo", "50%off", "policy.rego"}, "foo/50%off/policy.rego"},
	}
	for _, tc := range tests {
		if act := tc.path.PolicyID(); act != tc.exp {
			t.Errorf("For %v.PolicyID() expected %q but got %q", tc.path, tc.exp, act)
		}
	}
}

// 108.8 ns/op    80 B/op    3 allocs/op // original implementation concat + Join
// 68.60 ns/op    24 B/op    2 allocs/op // strings.Builder
// 50.28 ns/op    16 B/op    1 allocs/op // strings.Builder with pre-allocated buffer
func BenchmarkPathString(b *testing.B) {
	path := Path{"foo", "bar", "baz"}

	for b.Loop() {
		res := path.String()
		if res != "/foo/bar/baz" {
			b.Fatal("unexpected result:", res)
		}
	}
}
