package gval

/*
	Tests to make sure evaluation fails in the expected ways.
*/
import (
	"errors"
	"fmt"
	"testing"
)

func TestModifierTyping(test *testing.T) {
	var (
		invalidOperator      = "invalid operation"
		unknownParameter     = "unknown parameter"
		invalidRegex         = "error parsing regex"
		tooFewArguments      = "reflect: Call with too few input arguments"
		tooManyArguments     = "reflect: Call with too many input arguments"
		mismatchedParameters = "reflect: Call using"
		custom               = "test error"
	)
	evaluationTests := []evaluationTest{
		//ModifierTyping
		{
			name:       "PLUS literal number to literal bool",
			expression: "1 + true",
			want:       "1true", // + on string is defined
		},
		{
			name:       "PLUS number to bool",
			expression: "number + bool",
			want:       "1true", // + on string is defined
		},
		{
			name:       "MINUS number to bool",
			expression: "number - bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "MINUS number to bool",
			expression: "number - bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "MULTIPLY number to bool",
			expression: "number * bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "DIVIDE number to bool",
			expression: "number / bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "EXPONENT number to bool",
			expression: "number ** bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "MODULUS number to bool",
			expression: "number % bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "XOR number to bool",
			expression: "number % bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "BITWISE_OR number to bool",
			expression: "number | bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "BITWISE_AND number to bool",
			expression: "number & bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "BITWISE_XOR number to bool",
			expression: "number ^ bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "BITWISE_LSHIFT number to bool",
			expression: "number << bool",
			wantErr:    invalidOperator,
		},
		{
			name:       "BITWISE_RSHIFT number to bool",
			expression: "number >> bool",
			wantErr:    invalidOperator,
		},
		//LogicalOperatorTyping
		{
			name:       "AND number to number",
			expression: "number && number",
			want:       true, // number != 0 is true
		},
		{

			name:       "OR number to number",
			expression: "number || number",
			want:       true, // number != 0 is true
		},
		{
			name:       "AND string to string",
			expression: "string && string",
			wantErr:    invalidOperator,
		},
		{
			name:       "OR string to string",
			expression: "string || string",
			wantErr:    invalidOperator,
		},
		{
			name:       "AND number to string",
			expression: "number && string",
			wantErr:    invalidOperator,
		},
		{
			name:       "OR number to string",
			expression: "number || string",
			wantErr:    invalidOperator,
		},
		{
			name:       "AND bool to string",
			expression: "bool && string",
			wantErr:    invalidOperator,
		},
		{
			name:       "OR string to bool",
			expression: "string || bool",
			wantErr:    invalidOperator,
		},
		//ComparatorTyping
		{
			name:       "GT literal bool to literal bool",
			expression: "true > true",
			want:       false, //lexical order on "true"
		},
		{
			name:       "GT bool to bool",
			expression: "bool > bool",
			want:       false, //lexical order on "true"
		},
		{
			name:       "GTE bool to bool",
			expression: "bool >= bool",
			want:       true, //lexical order on "true"
		},
		{
			name:       "LT bool to bool",
			expression: "bool < bool",
			want:       false, //lexical order on "true"
		},
		{
			name:       "LTE bool to bool",
			expression: "bool <= bool",
			want:       true, //lexical order on "true"
		},
		{
			name:       "GT number to string",
			expression: "number > string",
			want:       false, //lexical order "1" < "foo"
		},
		{

			name:       "GTE number to string",
			expression: "number >= string",
			want:       false, //lexical order "1" < "foo"
		},
		{
			name:       "LT number to string",
			expression: "number < string",
			want:       true, //lexical order "1" < "foo"
		},
		{
			name:       "REQ number to string",
			expression: "number =~ string",
			want:       false,
		},
		{
			name:       "REQ number to bool",
			expression: "number =~ bool",
			want:       false,
		},
		{
			name:       "REQ bool to number",
			expression: "bool =~ number",
			want:       false,
		},
		{
			name:       "REQ bool to string",
			expression: "bool =~ string",
			want:       false,
		},
		{
			name:       "NREQ number to string",
			expression: "number !~ string",
			want:       true,
		},
		{
			name:       "NREQ number to bool",
			expression: "number !~ bool",
			want:       true,
		},
		{
			name:       "NREQ bool to number",
			expression: "bool !~ number",
			want:       true,
		},
		{

			name:       "NREQ bool to string",
			expression: "bool !~ string",
			want:       true,
		},
		{
			name:       "IN non-array numeric",
			expression: "1 in 2",
			wantErr:    "expected type []interface{} for in operator but got float64",
		},
		{
			name:       "IN non-array string",
			expression: `1 in "foo"`,
			wantErr:    "expected type []interface{} for in operator but got string",
		},
		{

			name:       "IN non-array boolean",
			expression: "1 in true",
			wantErr:    "expected type []interface{} for in operator but got bool",
		},
		//TernaryTyping
		{
			name:       "Ternary with number",
			expression: "10 ? true",
			want:       true, // 10 != nil && 10 != false
		},
		{
			name:       "Ternary with string",
			expression: `"foo" ? true`,
			want:       true, // "foo" != nil && "foo" != false
		},
		//RegexParameterCompilation
		{
			name:       "Regex equality runtime parsing",
			expression: `"foo" =~ foo`,
			parameter: map[string]interface{}{
				"foo": "[foo",
			},
			wantErr: invalidRegex,
		},
		{
			name:       "Regex inequality runtime parsing",
			expression: `"foo" !~ foo`,
			parameter: map[string]interface{}{
				"foo": "[foo",
			},
			wantErr: invalidRegex,
		},
		{
			name:       "Regex equality runtime right side evaluation",
			expression: `"foo" =~ error()`,
			wantErr:    custom,
		},
		{
			name:       "Regex inequality runtime right side evaluation",
			expression: `"foo" !~ error()`,
			wantErr:    custom,
		},
		{
			name:       "Regex equality runtime left side evaluation",
			expression: `error() =~ "."`,
			wantErr:    custom,
		},
		{
			name:       "Regex inequality runtime left side evaluation",
			expression: `error() !~ "."`,
			wantErr:    custom,
		},
		//FuncExecution
		{
			name:       "Func error bubbling",
			expression: "error()",
			extension: Function("error", func(arguments ...interface{}) (interface{}, error) {
				return nil, errors.New("Huge problems")
			}),
			wantErr: "Huge problems",
		},
		//InvalidParameterCalls
		{
			name:       "Missing parameter field reference",
			expression: "foo.NotExists",
			parameter:  fooFailureParameters,
			wantErr:    unknownParameter,
		},
		{
			name:       "Parameter method call on missing function",
			expression: "foo.NotExist()",
			parameter:  fooFailureParameters,
			wantErr:    unknownParameter,
		},
		{
			name:       "Nested missing parameter field reference",
			expression: "foo.Nested.NotExists",
			parameter:  fooFailureParameters,
			wantErr:    unknownParameter,
		},
		{
			name:       "Parameter method call returns error",
			expression: "foo.AlwaysFail()",
			parameter:  fooFailureParameters,
			wantErr:    "function should always fail",
		},
		{
			name:       "Too few arguments to parameter call",
			expression: "foo.FuncArgStr()",
			parameter:  fooFailureParameters,
			wantErr:    tooFewArguments,
		},
		{
			name:       "Too many arguments to parameter call",
			expression: `foo.FuncArgStr("foo", "bar", 15)`,
			parameter:  fooFailureParameters,
			wantErr:    tooManyArguments,
		},
		{
			name:       "Mismatched parameters",
			expression: "foo.FuncArgStr(5)",
			parameter:  fooFailureParameters,
			wantErr:    mismatchedParameters,
		},
		{
			name:       "Negative Array Index",
			expression: "foo[-1]",
			parameter: map[string]interface{}{
				"foo": []int{1, 2, 3},
			},
			wantErr: unknownParameter,
		},
		{
			name:       "Nested slice call index out of bound",
			expression: `foo.Nested.Slice[10]`,
			parameter:  map[string]interface{}{"foo": foo},
			wantErr:    unknownParameter,
		},
		{
			name:       "Nested map call missing key",
			expression: `foo.Nested.Map["d"]`,
			parameter:  map[string]interface{}{"foo": foo},
			wantErr:    unknownParameter,
		},
		{
			name:       "invalid selector",
			expression: "hello[world()]",
			extension: NewLanguage(Base(), Function("world", func() (int, error) {
				return 0, fmt.Errorf("test error")
			})),
			wantErr: "test error",
		},
		{
			name:       "eval `nil > 1` returns true #23",
			expression: `nil > 1`,
			wantErr:    "invalid operation (<nil>) > (float64)",
		},
		{
			name:       "map with unknown func",
			expression: `foo.MapWithFunc.NotExist()`,
			parameter:  map[string]interface{}{"foo": foo},
			wantErr:    unknownParameter,
		},
		{
			name:       "map with unknown func",
			expression: `foo.SliceWithFunc.NotExist()`,
			parameter:  map[string]interface{}{"foo": foo},
			wantErr:    unknownParameter,
		},
	}

	for i := range evaluationTests {
		if evaluationTests[i].parameter == nil {
			evaluationTests[i].parameter = map[string]interface{}{
				"number": 1,
				"string": "foo",
				"bool":   true,
				"error": func() (int, error) {
					return 0, fmt.Errorf("test error")
				},
			}
		}
	}

	testEvaluate(evaluationTests, test)
}
