package tsdb

import (
	"testing"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/influxdata/influxdb/v2/models"
	"github.com/influxdata/influxql"
)

func TestGuard(t *testing.T) {
	tests := []struct {
		min, max int64
		names    []string
		expr     string
		point    string
		matches  bool
	}{
		{ // in time matching
			min: 0, max: 1000,
			point:   "cpu value=1 100",
			matches: true,
		},
		{ // out of time range doesn't match
			min: 0, max: 10,
			names:   []string{"cpu"},
			point:   "cpu value=1 100",
			matches: false,
		},
		{ // measurement name matches
			min: 0, max: 1000,
			names:   []string{"cpu"},
			point:   "cpu value=1 100",
			matches: true,
		},
		{ // measurement doesn't match
			min: 0, max: 1000,
			names:   []string{"mem"},
			point:   "cpu value=1 100",
			matches: false,
		},
		{ // basic expression matching
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server1'",
			matches: true,
		},
		{ // basic expression matching
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host != 'server2'",
			matches: true,
		},
		{ // basic expression mismatch
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server2'",
			matches: false,
		},
		{ // basic expression mismatch
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host != 'server1'",
			matches: false,
		},
		{ // parenthesis unwrap
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "(host = 'server1')",
			matches: true,
		},
		{ // compound expression matching
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server2' or host = 'server1'",
			matches: true,
		},
		{ // compound expression mismatch
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server1' and host = 'server2'",
			matches: false,
		},
		{ // regex expression matching
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host =~ /server1/",
			matches: true,
		},
		{ // regex expression mismatch
			min: 0, max: 1000,
			point:   "cpu,foo=server1 value=1 100",
			expr:    "host =~ /server1/",
			matches: false,
		},
		{ // regex over-approximation
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host =~ /server2/",
			matches: true,
		},
		{ // regex over-approximation
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host !~ /server1/",
			matches: true,
		},
		{ // key doesn't have to come first
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "'server1' = host",
			matches: true,
		},
		{ // key doesn't have to come first
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "'server2' = host",
			matches: false,
		},
		{ // conservative on no var refs
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "1 = 2",
			matches: true,
		},
		{ // expr matches measurement
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "_name = 'cpu'",
			matches: true,
		},
		{ // expr mismatches measurement
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "_name = 'mem'",
			matches: false,
		},
		{ // expr conservative on dual var ref
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = test",
			matches: true,
		},
		{ // expr conservative on dual var ref mismatches
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "foo = bar",
			matches: false,
		},
		{ // expr conservative on dual var ref involving measurement
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "_name = host",
			matches: true,
		},
		{ // expr conservative on dual var ref involving measurement
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = _name",
			matches: true,
		},
		{ // boolean literal matches
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "true",
			matches: true,
		},
		{ // boolean literal mismatches
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "false",
			matches: false,
		},
		{ // reduce and
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "true and host = 'server1'",
			matches: true,
		},
		{ // reduce and
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server1' and true",
			matches: true,
		},
		{ // reduce or
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "false or host = 'server1'",
			matches: true,
		},
		{ // reduce or
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server1' or false",
			matches: true,
		},
		{ // short circuit and
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "false and host = 'server1'",
			matches: false,
		},
		{ // short circuit and
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server1' and false",
			matches: false,
		},
		{ // short circuit or
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "true or host = 'server2'",
			matches: true,
		},
		{ // short circuit or
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = 'server2' or true",
			matches: true,
		},
		{ // conservative match weird exprs
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "'wierd'",
			matches: true,
		},
		{ // conservative match weird exprs
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "value::field = '1'",
			matches: true,
		},
		{ // conservative match weird exprs
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host <= 'aaa'",
			matches: true,
		},
		{ // conservative match weird exprs
			min: 0, max: 1000,
			point:   "cpu,host=server1 value=1 100",
			expr:    "host = ('server2')",
			matches: true,
		},
	}

	for i, test := range tests {
		var expr influxql.Expr
		if test.expr != "" {
			var err error
			expr, err = influxql.ParseExpr(test.expr)
			if err != nil {
				t.Fatal(err)
			}
		}
		points, err := models.ParsePointsString(test.point)
		if err != nil {
			t.Fatal(err)
		}
		guard := newGuard(test.min, test.max, test.names, expr)

		if guard.Matches(points) != test.matches {
			t.Errorf("%d: expected matching %q with time:[%d, %d] measurements:%v expr:%q to be %t",
				i, test.point, test.min, test.max, test.names, test.expr, test.matches)
			cs := &spew.ConfigState{DisableMethods: true, SpewKeys: true, Indent: "  "}
			t.Errorf("%d: expr: %s", i, cs.Sdump(expr))
			t.Errorf("%d: guard: %s", i, cs.Sdump(guard.expr))
		}
	}
}

func BenchmarkGuard(b *testing.B) {
	tag := func(key, value string) models.Tag {
		return models.Tag{Key: []byte(key), Value: []byte(value)}
	}

	run := func(b *testing.B, g *guard) {
		run := func(b *testing.B, batch int) {
			points := make([]models.Point, batch)
			for i := range points {
				points[i] = models.MustNewPoint("cpu", models.Tags{
					tag("t0", "v0"), tag("t1", "v1"), tag("t2", "v2"),
					tag("t3", "v3"), tag("t4", "v4"), tag("t5", "v5"),
					tag("t6", "v6"), tag("t7", "v7"), tag("t8", "v8"),
				}, models.Fields{"value": 100}, time.Unix(0, 50))
			}

			for i := 0; i < b.N; i++ {
				if g.Matches(points) {
					b.Fatal("matched")
				}
			}
		}

		b.Run("1", func(b *testing.B) { run(b, 1) })
		b.Run("100", func(b *testing.B) { run(b, 100) })
		b.Run("10000", func(b *testing.B) { run(b, 10000) })
	}

	b.Run("Time Filtered", func(b *testing.B) {
		run(b, newGuard(0, 10, nil, nil))
	})

	b.Run("Measurement Filtered", func(b *testing.B) {
		run(b, newGuard(0, 100, []string{"mem"}, nil))
	})

	b.Run("Tag Filtered", func(b *testing.B) {
		expr, _ := influxql.ParseExpr("t4 = 'v5'")
		run(b, newGuard(0, 100, []string{"cpu"}, expr))
	})
}
