//go:build expr_debug

package exprhelpers

import (
	"reflect"
	"strings"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/expr-lang/expr"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/types"
)

type ExprDbgTest struct {
	Name                  string
	Expr                  string
	ExpectedOutputs       []OpOutput
	ExpectedFailedCompile bool
	ExpectedFailRuntime   bool
	Env                   map[string]interface{}
	LogLevel              log.Level
}

// For the sake of testing functions with 2, 3 and N args
func UpperTwo(params ...any) (any, error) {
	s := params[0].(string)
	v := params[1].(string)

	return strings.ToUpper(s) + strings.ToUpper(v), nil
}

func UpperThree(params ...any) (any, error) {
	s := params[0].(string)
	v := params[1].(string)
	x := params[2].(string)

	return strings.ToUpper(s) + strings.ToUpper(v) + strings.ToUpper(x), nil
}

func UpperN(params ...any) (any, error) {
	s := params[0].(string)
	v := params[1].(string)
	x := params[2].(string)
	y := params[3].(string)

	return strings.ToUpper(s) + strings.ToUpper(v) + strings.ToUpper(x) + strings.ToUpper(y), nil
}

func boolPtr(b bool) *bool {
	return &b
}

type teststruct struct {
	Foo string
}

// You need to add the tag expr_debug when running the tests
func TestBaseDbg(t *testing.T) {
	defaultEnv := map[string]interface{}{
		"queue":        &types.Queue{},
		"evt":          &types.Event{},
		"sample_array": []string{"a", "b", "c", "ZZ"},
		"base_string":  "hello world",
		"base_int":     42,
		"base_float":   42.42,
		"nilvar":       &teststruct{},
		"base_struct": struct {
			Foo   string
			Bar   int
			Myarr []string
		}{
			Foo:   "bar",
			Bar:   42,
			Myarr: []string{"a", "b", "c"},
		},
	}
	// tips for the tests:
	// use '%#v' to dump in golang syntax
	// use regexp to clear empty/default fields:
	// [a-z]+: (false|\[\]string\(nil\)|""),
	// ConditionResult:(*bool)

	// Missing multi parametes function
	tests := []ExprDbgTest{
		{
			Name:                "nil deref",
			Expr:                "Upper('1') == '1' && nilvar.Foo == '42'",
			Env:                 defaultEnv,
			ExpectedFailRuntime: true,
			ExpectedOutputs: []OpOutput{
				{Code: "Upper('1')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"1\""}, FuncResults: []string{"\"1\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== '1'", CodeDepth: 0, Comparison: true, Left: "\"1\"", Right: "\"1\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "<nil>", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "OpCall2",
			Expr: "UpperTwo('hello', 'world') == 'HELLOWORLD'",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "UpperTwo('hello', 'world')", CodeDepth: 0, Func: true, FuncName: "UpperTwo", Args: []string{"\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLD\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'HELLOWORLD'", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLD\"", Right: "\"HELLOWORLD\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "OpCall3",
			Expr: "UpperThree('hello', 'world', 'foo') == 'HELLOWORLDFOO'",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "UpperThree('hello', 'world', 'foo')", CodeDepth: 0, Func: true, FuncName: "UpperThree", Args: []string{"\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOO\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'HELLOWORLDFOO'", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLDFOO\"", Right: "\"HELLOWORLDFOO\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "OpCallN",
			Expr: "UpperN('hello', 'world', 'foo', 'lol') == UpperN('hello', 'world', 'foo', 'lol')",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Func: true, FuncName: "OpCallN", Args: []string{"\"lol\"", "\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOOLOL\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Func: true, FuncName: "OpCallN", Args: []string{"\"lol\"", "\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOOLOL\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLDFOOLOL\"", Right: "\"HELLOWORLDFOOLOL\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "base string cmp",
			Expr: "base_string == 'hello world'",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "loop with func call",
			Expr: "count(base_struct.Myarr, {Upper(#) == 'C'}) == 1",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "count(base_struct.Myarr, {", CodeDepth: 4, BlockStart: true, ConditionResult: (*bool)(nil), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"a\""}, FuncResults: []string{"\"A\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"A\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"b\""}, FuncResults: []string{"\"B\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"B\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"c\""}, FuncResults: []string{"\"C\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"C\"", Right: "\"C\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 0, BlockEnd: true, StrConditionResult: "[1]", ConditionResult: (*bool)(nil), Finalized: false},
				{Code: "== 1", CodeDepth: 0, Comparison: true, Left: "1", Right: "1", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "loop with func call and extra check",
			Expr: "count(base_struct.Myarr, {Upper(#) == 'C'}) == 1 && Upper(base_struct.Foo) == 'BAR'",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "count(base_struct.Myarr, {", CodeDepth: 4, BlockStart: true, ConditionResult: (*bool)(nil), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"a\""}, FuncResults: []string{"\"A\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"A\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"b\""}, FuncResults: []string{"\"B\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"B\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"c\""}, FuncResults: []string{"\"C\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"C\"", Right: "\"C\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false},
				{Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 0, BlockEnd: true, StrConditionResult: "[1]", ConditionResult: (*bool)(nil), Finalized: false},
				{Code: "== 1", CodeDepth: 0, Comparison: true, Left: "1", Right: "1", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false},
				{Code: "Upper(base_struct.Foo)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"bar\""}, FuncResults: []string{"\"BAR\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "== 'BAR'", CodeDepth: 0, Comparison: true, Left: "\"BAR\"", Right: "\"BAR\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "base 'in' test",
			Expr: "base_int in [1,2,3,4,42]",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "in [1,2,3,4,42]", CodeDepth: 0, Args: []string{"42", "map[1:{} 2:{} 3:{} 4:{} 42:{}]"}, Condition: true, ConditionIn: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "base string cmp",
			Expr: "base_string == 'hello world'",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "base int cmp",
			Expr: "base_int == 42",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "negative check",
			Expr: "base_int != 43",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "!= 43", CodeDepth: 0, Negated: true, Comparison: true, Left: "42", Right: "43", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "testing ORs",
			Expr: "base_int == 43 || base_int == 42",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "== 43", CodeDepth: 0, Comparison: true, Left: "42", Right: "43", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "testing basic true",
			Expr: "true",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "true", CodeDepth: 0, Condition: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "testing basic false",
			Expr: "false",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "false", CodeDepth: 0, Condition: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: true},
			},
		},
		{
			Name: "testing multi lines",
			Expr: `base_int == 42 &&
					base_string == 'hello world' &&
					(base_struct.Bar == 41 || base_struct.Bar == 42)`,
			Env: defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false},
				{Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "&& (", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false},
				{Code: "== 41", CodeDepth: 0, Comparison: true, Left: "42", Right: "41", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "== 42)", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "upper + in",
			Expr: "Upper(base_string) contains Upper('wOrlD')",
			Env:  defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "Upper('wOrlD')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"wOrlD\""}, FuncResults: []string{"\"WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "contains Upper('wOrlD')", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"WORLD\""}, Condition: true, ConditionContains: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true},
			},
		},
		{
			Name: "upper + complex",
			Expr: `( Upper(base_string) contains Upper('/someurl?x=1') || 
								Upper(base_string) contains Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') ) 
								and base_string startsWith ('40') and Upper(base_string) == 'POST'`,
			Env: defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "Upper('/someurl?x=1')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"/someurl?x=1\""}, FuncResults: []string{"\"/SOMEURL?X=1\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "contains Upper('/someurl?x=1')", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"/SOMEURL?X=1\""}, Condition: true, ConditionContains: true, StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false},
				{Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') )", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"/someotherurl?account-name=admin&account...\""}, FuncResults: []string{"\"/SOMEOTHERURL?ACCOUNT-NAME=ADMIN&ACCOUNT...\""}, ConditionResult: (*bool)(nil), Finalized: true},
				{Code: "contains Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') )", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"/SOMEOTHERURL?ACCOUNT-NAME=ADMIN&ACCOUNT...\""}, Condition: true, ConditionContains: true, StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true},
				{Code: "and", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: true},
			},
		},
		{
			Name: "if",
			Expr: `if base_int != 42 { 
    true
} else {
    false
}`,
			Env: defaultEnv,
			ExpectedOutputs: []OpOutput{
				{Code: "!= 42 {", CodeDepth: 0, Negated: true, Comparison: true, Left: "42", Right: "42", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true},
				{Code: "if base_int != 42 {", CodeDepth: 0, ConditionResult: boolPtr(false), Finalized: false, IfFalse: true, JumpIf: true, StrConditionResult: "false"},
				{Code: "false }", CodeDepth: 0, StrConditionResult: "false", Condition: true, ConditionResult: boolPtr(false), Finalized: true},
			},
		},
	}

	logger := log.WithField("test", "exprhelpers")

	for _, test := range tests {
		if test.LogLevel != 0 {
			log.SetLevel(test.LogLevel)
		} else {
			log.SetLevel(log.DebugLevel)
		}

		extraFuncs := []expr.Option{}
		extraFuncs = append(extraFuncs,
			expr.Function("UpperTwo",
				UpperTwo,
				[]interface{}{new(func(string, string) string)}...,
			))
		extraFuncs = append(extraFuncs,
			expr.Function("UpperThree",
				UpperThree,
				[]interface{}{new(func(string, string, string) string)}...,
			))
		extraFuncs = append(extraFuncs,
			expr.Function("UpperN",
				UpperN,
				[]interface{}{new(func(string, string, string, string) string)}...,
			))
		supaEnv := GetExprOptions(test.Env)
		supaEnv = append(supaEnv, extraFuncs...)

		prog, err := expr.Compile(test.Expr, supaEnv...)
		if test.ExpectedFailedCompile {
			if err == nil {
				t.Fatalf("test %s : expected compile error", test.Name)
			}
		} else {
			if err != nil {
				t.Fatalf("test %s : unexpected compile error : %s", test.Name, err)
			}
		}

		if test.Name == "nil deref" {
			test.Env["nilvar"] = nil
		}

		outdbg, ret, err := RunWithDebug(prog, test.Env, logger)

		if test.ExpectedFailRuntime {
			if err == nil {
				t.Fatalf("test %s : expected runtime error", test.Name)
			}
		} else {
			if err != nil {
				t.Fatalf("test %s : unexpected runtime error : %s", test.Name, err)
			}
		}

		log.SetLevel(log.DebugLevel)
		DisplayExprDebug(prog, outdbg, logger, ret)

		if len(outdbg) != len(test.ExpectedOutputs) {
			t.Errorf("failed test %s", test.Name)
			t.Errorf("%#v", outdbg)
			// out, _ := yaml.Marshal(outdbg)
			// fmt.Printf("%s", string(out))
			t.Fatalf("test %s : expected %d outputs, got %d", test.Name, len(test.ExpectedOutputs), len(outdbg))
		}

		for i, out := range outdbg {
			if reflect.DeepEqual(out, test.ExpectedOutputs[i]) {
				// DisplayExprDebug(prog, outdbg, logger, ret)
				continue
			}

			spew.Config.DisableMethods = true

			t.Errorf("failed test %s", test.Name)
			t.Errorf("expected : %#v", test.ExpectedOutputs[i])
			t.Errorf("got      : %#v", out)
			t.Fatalf("%d/%d    : mismatch", i, len(outdbg))
		}
	}
}
