package filtering

import (
	"testing"
	"time"

	expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
	"google.golang.org/protobuf/testing/protocmp"
	"gotest.tools/v3/assert"
)

func TestParser(t *testing.T) {
	t.Parallel()
	for _, tt := range []struct {
		filter        string
		expected      *expr.Expr
		errorContains string
	}{
		{
			filter:   "New York Giants",
			expected: Sequence(Text("New"), Text("York"), Text("Giants")),
		},

		{
			filter:   "New York Giants OR Yankees",
			expected: Sequence(Text("New"), Text("York"), Or(Text("Giants"), Text("Yankees"))),
		},

		{
			filter:   "New York (Giants OR Yankees)",
			expected: Sequence(Text("New"), Text("York"), Or(Text("Giants"), Text("Yankees"))),
		},

		{
			filter: "a b AND c AND d",
			expected: And(
				Sequence(Text("a"), Text("b")),
				Text("c"),
				Text("d"),
			),
		},

		{
			filter: "(a b) AND c AND d",
			expected: And(
				Sequence(Text("a"), Text("b")),
				Text("c"),
				Text("d"),
			),
		},

		{
			filter: "a < 10 OR a >= 100",
			expected: Or(
				LessThan(Text("a"), Int(10)),
				GreaterEquals(Text("a"), Int(100)),
			),
		},
		{
			filter: "a OR b OR c",
			expected: Or(
				Text("a"),
				Text("b"),
				Text("c"),
			),
		},

		{
			filter:   "NOT (a OR b)",
			expected: Not(Or(Text("a"), Text("b"))),
		},

		{
			filter:   `-file:".java"`,
			expected: Not(Has(Text("file"), String(".java"))),
		},

		{
			filter:   "-30",
			expected: Int(-30),
		},

		{
			filter:   "package=com.google",
			expected: Equals(Text("package"), Member(Text("com"), "google")),
		},

		// Simple string unescaping tests.
		{
			filter:   `msg != 'hello'`,
			expected: NotEquals(Text("msg"), String(`hello`)),
		},
		{
			filter:   `msg != "hello"`,
			expected: NotEquals(Text("msg"), String(`hello`)),
		},
		{
			filter:   `msg != ""`,
			expected: NotEquals(Text("msg"), String(``)),
		},
		{
			filter:   `msg != "\\\""`,
			expected: NotEquals(Text("msg"), String(`\"`)),
		},
		{
			filter:   `msg != "\\"`,
			expected: NotEquals(Text("msg"), String(`\`)),
		},
		{
			filter:   `msg != "\303\277"`,
			expected: NotEquals(Text("msg"), String(`Ã¿`)),
		},
		{
			filter:   `msg != "\377"`,
			expected: NotEquals(Text("msg"), String(`ÿ`)),
		},
		{
			filter:   `msg != "\u263A\u263A"`,
			expected: NotEquals(Text("msg"), String(`☺☺`)),
		},
		{
			filter:   `msg != "\a\b\f\n\r\t\v\'\"\\\? Legal escapes"`,
			expected: NotEquals(Text("msg"), String("\a\b\f\n\r\t\v'\"\\? Legal escapes")),
		},
		{
			filter:   `msg != "[ 'hello' ]"`,
			expected: NotEquals(Text("msg"), String(`[ 'hello' ]`)),
		},
		{
			filter:   `msg != "[ \'hello\' ]"`,
			expected: NotEquals(Text("msg"), String(`[ 'hello' ]`)),
		},
		{
			filter:   `msg != "[ \"hello\" ]"`,
			expected: NotEquals(Text("msg"), String(`[ "hello" ]`)),
		},

		{
			filter:   `1 > 0`,
			expected: GreaterThan(Int(1), Int(0)),
		},

		{
			filter:   `2.5 >= 2.4`,
			expected: GreaterEquals(Float(2.5), Float(2.4)),
		},

		{
			filter:   `foo >= -2.4`,
			expected: GreaterEquals(Text("foo"), Float(-2.4)),
		},

		{
			filter:   `foo >= (-2.4)`,
			expected: GreaterEquals(Text("foo"), Float(-2.4)),
		},

		{
			filter:   `-2.5 >= -2.4`,
			expected: Not(GreaterEquals(Float(2.5), Float(-2.4))),
		},

		{
			filter:   `yesterday < request.time`,
			expected: LessThan(Text("yesterday"), Member(Text("request"), "time")),
		},

		{
			filter: `experiment.rollout <= cohort(request.user)`,
			expected: LessEquals(
				Member(Text("experiment"), "rollout"),
				Function("cohort", Member(Text("request"), "user")),
			),
		},

		{
			filter:   `prod`,
			expected: Text("prod"),
		},

		{
			filter:   `expr.type_map.1.type`,
			expected: Member(Member(Member(Text("expr"), "type_map"), "1"), "type"),
		},

		{
			filter:   `regex(m.key, '^.*prod.*$')`,
			expected: Function("regex", Member(Text("m"), "key"), String("^.*prod.*$")),
		},

		{
			filter:   `math.mem('30mb')`,
			expected: Function("math.mem", String("30mb")),
		},

		{
			filter: `(msg.endsWith('world') AND retries < 10)`,
			expected: And(
				Function("msg.endsWith", String("world")),
				LessThan(Text("retries"), Int(10)),
			),
		},

		{
			filter: `(endsWith(msg, 'world') AND retries < 10)`,
			expected: And(
				Function("endsWith", Text("msg"), String("world")),
				LessThan(Text("retries"), Int(10)),
			),
		},

		{
			filter:   "time.now()",
			expected: Function("time.now"),
		},

		{
			filter: `timestamp("2012-04-21T11:30:00-04:00")`,
			expected: Timestamp(
				time.Date(2012, time.April, 21, 11, 30, 0, 0, time.FixedZone("EST", -4*int(time.Hour.Seconds()))),
			),
		},

		{
			filter:   `duration("32s")`,
			expected: Duration(32 * time.Second),
		},

		{
			filter:   `duration("4h0m0s")`,
			expected: Duration(4 * time.Hour),
		},

		{
			filter: `
				start_time > timestamp("2006-01-02T15:04:05+07:00") AND
				(driver = "driver1" OR start_driver = "driver1" OR end_driver = "driver1")
			`,
			expected: And(
				GreaterThan(
					Text("start_time"),
					Timestamp(
						time.Date(
							2006, time.January, 2, 15, 4, 5, 0,
							time.FixedZone("EST", 7*int(time.Hour.Seconds())),
						),
					),
				),
				Or(
					Equals(Text("driver"), String("driver1")),
					Equals(Text("start_driver"), String("driver1")),
					Equals(Text("end_driver"), String("driver1")),
				),
			),
		},

		{
			filter:   `annotations:schedule`,
			expected: Has(Text("annotations"), String("schedule")),
		},

		{
			filter:   `annotations.schedule = "test"`,
			expected: Equals(Member(Text("annotations"), "schedule"), String("test")),
		},

		{
			filter:        "<",
			errorContains: "unexpected token <",
		},

		{
			filter:        `(-2.5) >= -2.4`,
			errorContains: "unexpected token >=",
		},

		{
			filter:        `a = "foo`,
			errorContains: "unterminated string",
		},

		{
			filter:        "invalid = foo\xa0\x01bar",
			errorContains: "invalid UTF-8",
		},
	} {
		t.Run(tt.filter, func(t *testing.T) {
			t.Parallel()
			var parser Parser
			parser.Init(tt.filter)
			actual, err := parser.Parse()
			if tt.errorContains != "" {
				if actual != nil {
					t.Log(actual.GetExpr().String())
				}
				assert.ErrorContains(t, err, tt.errorContains)
			} else {
				assert.NilError(t, err)
				assert.DeepEqual(
					t,
					tt.expected,
					actual.GetExpr(),
					protocmp.Transform(),
					protocmp.IgnoreFields(&expr.Expr{}, "id"),
				)
				assertUniqueExprIDs(t, actual.GetExpr())
			}
		})
	}
}

func assertUniqueExprIDs(t *testing.T, exp *expr.Expr) {
	t.Helper()
	seenIDs := make(map[int64]struct{})
	Walk(func(currExpr, _ *expr.Expr) bool {
		if _, ok := seenIDs[currExpr.GetId()]; ok {
			t.Fatalf("duplicate expression ID '%d' for expr %v", currExpr.GetId(), currExpr)
		}
		seenIDs[currExpr.GetId()] = struct{}{}
		return true
	}, exp)
}
