// Copyright 2018 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 test

import (
	"github.com/google/cel-go/common/operators"

	"google.golang.org/protobuf/proto"

	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
	structpb "google.golang.org/protobuf/types/known/structpb"
)

// TestExpr packages an Expr with SourceInfo, for testing.
type TestExpr struct {
	Expr       *exprpb.Expr
	SourceInfo *exprpb.SourceInfo
}

// Info returns a copy of the SourceInfo with the given location.
func (t *TestExpr) Info(location string) *exprpb.SourceInfo {
	info := proto.Clone(t.SourceInfo).(*exprpb.SourceInfo)
	info.Location = location
	return info
}

var (
	// Empty generates a program with no instructions.
	Empty = &TestExpr{
		Expr: &exprpb.Expr{},

		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// Exists generates "[1, 1u, 1.0].exists(x, type(x) == uint)".
	Exists = &TestExpr{
		Expr: ExprComprehension(1,
			"x",
			ExprList(8,
				ExprLiteral(2, int64(0)),
				ExprLiteral(3, int64(1)),
				ExprLiteral(4, int64(2)),
				ExprLiteral(5, int64(3)),
				ExprLiteral(6, int64(4)),
				ExprLiteral(7, uint64(5))),
			"__result__",
			ExprLiteral(9, false),
			ExprCall(12,
				operators.NotStrictlyFalse,
				ExprCall(10,
					operators.LogicalNot,
					ExprIdent(11, "__result__"))),
			ExprCall(13,
				operators.LogicalOr,
				ExprIdent(14, "__result__"),
				ExprCall(15,
					operators.Equals,
					ExprCall(16,
						"type",
						ExprIdent(17, "x")),
					ExprIdent(18, "uint"))),
			ExprIdent(19, "__result__")),

		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{0},
			Positions: map[int64]int32{
				0:  12,
				1:  0,
				2:  1,
				3:  4,
				4:  8,
				5:  0,
				6:  18,
				7:  18,
				8:  18,
				9:  18,
				10: 18,
				11: 20,
				12: 20,
				13: 28,
				14: 28,
				15: 28,
				16: 28,
				17: 28,
				18: 28,
				19: 28}}}

	// ExistsWithInput generates "elems.exists(x, type(x) == uint)".
	ExistsWithInput = &TestExpr{
		Expr: ExprComprehension(1,
			"x",
			ExprIdent(2, "elems"),
			"__result__",
			ExprLiteral(3, false),
			ExprCall(4,
				operators.LogicalNot,
				ExprIdent(5, "__result__")),
			ExprCall(6,
				operators.Equals,
				ExprCall(7,
					"type",
					ExprIdent(8, "x")),
				ExprIdent(9, "uint")),
			ExprIdent(10, "__result__")),

		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{0},
			Positions: map[int64]int32{
				0:  12,
				1:  0,
				2:  1,
				3:  4,
				4:  8,
				5:  0,
				6:  18,
				7:  18,
				8:  18,
				9:  18,
				10: 18}}}

	// DynMap generates a map literal:
	// {"hello": "world".size(),
	//  "dur": duration.Duration{10},
	//  "ts": timestamp.Timestamp{1000},
	//  "null": null,
	//  "bytes": b"bytes-string"}
	DynMap = &TestExpr{
		Expr: ExprMap(17,
			ExprEntry(2,
				ExprLiteral(1, "hello"),
				ExprMemberCall(3,
					"size",
					ExprLiteral(4, "world"))),
			ExprEntry(12,
				ExprLiteral(11, "null"),
				ExprLiteral(13, structpb.NullValue_NULL_VALUE)),
			ExprEntry(15,
				ExprLiteral(14, "bytes"),
				ExprLiteral(16, []byte("bytes-string")))),

		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// LogicalAnd generates "a && {c: true}.c".
	LogicalAnd = &TestExpr{
		ExprCall(2, operators.LogicalAnd,
			ExprIdent(1, "a"),
			ExprSelect(8,
				ExprMap(5,
					ExprEntry(4, ExprLiteral(6, "c"), ExprLiteral(7, true))),
				"c")),
		&exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// LogicalOr generates "{c: false}.c || a".
	LogicalOr = &TestExpr{
		ExprCall(2, operators.LogicalOr,
			ExprSelect(8,
				ExprMap(5,
					ExprEntry(4, ExprLiteral(6, "c"), ExprLiteral(7, false))),
				"c"),
			ExprIdent(1, "a")),
		&exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// LogicalOrEquals generates "a || b == 'b'".
	LogicalOrEquals = &TestExpr{
		ExprCall(5, operators.LogicalOr,
			ExprIdent(1, "a"),
			ExprCall(4, operators.Equals,
				ExprIdent(2, "b"),
				ExprLiteral(3, "b"))),
		&exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// LogicalAndMissingType generates "a && TestProto{c: true}.c" where the
	// type 'TestProto' is undefined.
	LogicalAndMissingType = &TestExpr{
		ExprCall(2, operators.LogicalAnd,
			ExprIdent(1, "a"),
			ExprSelect(7,
				ExprType(5, "TestProto",
					ExprField(4, "c", ExprLiteral(6, true))),
				"c")),
		&exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// Conditional generates "a ? b < 1.0 : c == ["hello"]".
	Conditional = &TestExpr{
		Expr: ExprCall(9, operators.Conditional,
			ExprIdent(1, "a"),
			ExprCall(3,
				operators.Less,
				ExprIdent(2, "b"),
				ExprLiteral(4, 1.0)),
			ExprCall(6,
				operators.Equals,
				ExprIdent(5, "c"),
				ExprList(8, ExprLiteral(7, "hello")))),
		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// Select generates "a.b.c".
	Select = &TestExpr{
		Expr: ExprSelect(3,
			ExprSelect(2,
				ExprIdent(1, "a"),
				"b"),
			"c"),
		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// Equality generates "a == 42".
	Equality = &TestExpr{
		Expr: ExprCall(2,
			operators.Equals,
			ExprIdent(1, "a"),
			ExprLiteral(3, int64(42))),
		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}

	// TypeEquality generates "type(a) == uint".
	TypeEquality = &TestExpr{
		Expr: ExprCall(4,
			operators.Equals,
			ExprCall(1, "type",
				ExprIdent(2, "a")),
			ExprIdent(3, "uint")),
		SourceInfo: &exprpb.SourceInfo{
			LineOffsets: []int32{},
			Positions:   map[int64]int32{}}}
)

// ExprIdent creates an ident (variable) Expr.
func ExprIdent(id int64, name string) *exprpb.Expr {
	return &exprpb.Expr{Id: id, ExprKind: &exprpb.Expr_IdentExpr{
		IdentExpr: &exprpb.Expr_Ident{Name: name}}}
}

// ExprSelect creates a select Expr.
func ExprSelect(id int64, operand *exprpb.Expr, field string) *exprpb.Expr {
	return &exprpb.Expr{Id: id,
		ExprKind: &exprpb.Expr_SelectExpr{
			SelectExpr: &exprpb.Expr_Select{
				Operand:  operand,
				Field:    field,
				TestOnly: false}}}
}

// ExprLiteral creates a literal (constant) Expr.
func ExprLiteral(id int64, value interface{}) *exprpb.Expr {
	var literal *exprpb.Constant
	switch value.(type) {
	case bool:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_BoolValue{
			BoolValue: value.(bool)}}
	case int64:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_Int64Value{
			Int64Value: value.(int64)}}
	case uint64:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_Uint64Value{
			Uint64Value: value.(uint64)}}
	case float64:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_DoubleValue{
			DoubleValue: value.(float64)}}
	case string:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_StringValue{
			StringValue: value.(string)}}
	case structpb.NullValue:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_NullValue{
			NullValue: value.(structpb.NullValue)}}
	case []byte:
		literal = &exprpb.Constant{ConstantKind: &exprpb.Constant_BytesValue{
			BytesValue: value.([]byte)}}
	default:
		panic("literal type not implemented")
	}
	return &exprpb.Expr{Id: id, ExprKind: &exprpb.Expr_ConstExpr{ConstExpr: literal}}
}

// ExprCall creates a call Expr.
func ExprCall(id int64, function string, args ...*exprpb.Expr) *exprpb.Expr {
	return &exprpb.Expr{Id: id,
		ExprKind: &exprpb.Expr_CallExpr{
			CallExpr: &exprpb.Expr_Call{Target: nil, Function: function, Args: args}}}
}

// ExprMemberCall creates a receiver-style call Expr.
func ExprMemberCall(id int64, function string, target *exprpb.Expr, args ...*exprpb.Expr) *exprpb.Expr {
	return &exprpb.Expr{Id: id,
		ExprKind: &exprpb.Expr_CallExpr{
			CallExpr: &exprpb.Expr_Call{Target: target, Function: function, Args: args}}}
}

// ExprList creates a create list Expr.
func ExprList(id int64, elements ...*exprpb.Expr) *exprpb.Expr {
	return &exprpb.Expr{Id: id,
		ExprKind: &exprpb.Expr_ListExpr{
			ListExpr: &exprpb.Expr_CreateList{Elements: elements}}}
}

// ExprMap creates a create struct Expr for a map.
func ExprMap(id int64, entries ...*exprpb.Expr_CreateStruct_Entry) *exprpb.Expr {
	return &exprpb.Expr{Id: id, ExprKind: &exprpb.Expr_StructExpr{
		StructExpr: &exprpb.Expr_CreateStruct{Entries: entries}}}
}

// ExprType creates creates a create struct Expr for a message.
func ExprType(id int64, messageName string,
	entries ...*exprpb.Expr_CreateStruct_Entry) *exprpb.Expr {
	return &exprpb.Expr{Id: id, ExprKind: &exprpb.Expr_StructExpr{
		StructExpr: &exprpb.Expr_CreateStruct{
			MessageName: messageName, Entries: entries}}}
}

// ExprEntry creates a map entry for a create struct Expr.
func ExprEntry(id int64, key *exprpb.Expr,
	value *exprpb.Expr) *exprpb.Expr_CreateStruct_Entry {
	return &exprpb.Expr_CreateStruct_Entry{Id: id,
		KeyKind: &exprpb.Expr_CreateStruct_Entry_MapKey{MapKey: key},
		Value:   value}
}

// ExprField creates a field entry for a create struct Expr.
func ExprField(id int64, field string,
	value *exprpb.Expr) *exprpb.Expr_CreateStruct_Entry {
	return &exprpb.Expr_CreateStruct_Entry{Id: id,
		KeyKind: &exprpb.Expr_CreateStruct_Entry_FieldKey{FieldKey: field},
		Value:   value}
}

// ExprComprehension returns a comprehension Expr.
func ExprComprehension(id int64,
	iterVar string, iterRange *exprpb.Expr,
	accuVar string, accuInit *exprpb.Expr,
	loopCondition *exprpb.Expr, loopStep *exprpb.Expr,
	resultExpr *exprpb.Expr) *exprpb.Expr {
	return &exprpb.Expr{Id: id,
		ExprKind: &exprpb.Expr_ComprehensionExpr{
			ComprehensionExpr: &exprpb.Expr_Comprehension{
				IterVar:       iterVar,
				IterRange:     iterRange,
				AccuVar:       accuVar,
				AccuInit:      accuInit,
				LoopCondition: loopCondition,
				LoopStep:      loopStep,
				Result:        resultExpr}}}
}
