package explain

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/Masterminds/semver"
	"github.com/kr/pretty"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	tu "github.com/percona/percona-toolkit/src/go/internal/testutils"
	"github.com/percona/percona-toolkit/src/go/lib/tutil"
	"github.com/percona/percona-toolkit/src/go/mongolib/proto"
)

const (
	samples = "/src/go/tests/"
)

type testVars struct {
	RootPath string
}

var vars testVars

func TestMain(m *testing.M) {
	var err error
	if vars.RootPath, err = tutil.RootPath(); err != nil {
		log.Printf("cannot get root path: %s", err.Error())
		os.Exit(1)
	}
	os.Exit(m.Run())
}

func TestExplain(t *testing.T) {
	t.Parallel()

	uri := fmt.Sprintf("mongodb://%s:%s@%s:%s", tu.MongoDBUser, tu.MongoDBPassword, tu.MongoDBHost, tu.MongoDBMongosPort)
	client, err := mongo.NewClient(options.Client().ApplyURI(uri))
	if err != nil {
		t.Fatalf("cannot get a new MongoDB client: %s", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	err = client.Connect(ctx)
	if err != nil {
		t.Fatalf("Cannot connect to MongoDB: %s", err)
	}

	dir := vars.RootPath + samples + "/doc/out/"
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		t.Fatalf("cannot list samples: %s", err)
	}

	res := client.Database("admin").RunCommand(ctx, primitive.M{"buildInfo": 1})
	if res.Err() != nil {
		t.Fatalf("Cannot get buildInfo: %s", err)
	}
	bi := proto.BuildInfo{}
	if err := res.Decode(&bi); err != nil {
		t.Fatalf("Cannot decode buildInfo response: %s", err)
	}

	versions := []string{
		"2.6.12",
		"3.0.15",
		"3.2.19",
		"3.4.12",
		"3.6.2",
	}

	samples := map[string]bool{
		"aggregate":        false,
		"count":            false,
		"count_with_query": false,
		"delete":           false,
		"delete_all":       false,
		"distinct":         false,
		"find_empty":       true,
		"find":             false,
		"find_with_sort":   false,
		"find_andrii":      false,
		"findandmodify":    false,
		"geonear":          true,
		"getmore":          false,
		"group":            false,
		"insert":           true,
		"mapreduce":        true,
		"update":           false,
		"explain":          true,
		"eval":             true,
	}

	expectError := map[string]bool{}

	// For versions < 3.0 explain is not supported
	if ok, _ := Constraint("< 3.0", bi.Version); ok {
		for _, v := range versions {
			for sample := range samples {
				expectError[sample+"_"+v] = true
			}
		}
	} else {
		for _, v := range versions {
			for sample, msg := range samples {
				expectError[sample+"_"+v] = msg
			}
		}

		for _, v := range versions {
			// For versions < 3.4 parsing "getmore" is not supported and returns error
			if ok, _ := Constraint("< 3.4", v); ok {
				expectError["getmore_"+v] = true
			}
		}

		for _, v := range versions {
			// For versions < 3.4 parsing "getmore" is not supported and returns error
			if ok, _ := Constraint(">= 2.4, <= 2.6", v); ok {
				expectError["find_empty_"+v] = false
			}
			if ok, _ := Constraint(">= 3.2", v); ok {
				expectError["find_empty_"+v] = false
			}
		}

		// For versions >= 3.0, < 3.4 trying to explain "insert" returns different error
		if ok, _ := Constraint(">= 3.0, < 3.4", bi.Version); ok {
			for _, v := range versions {
				expectError["insert_"+v] = true
			}
		}

		// Explaining `distinct` and `findAndModify` was introduced in MongoDB 3.2
		if ok, _ := Constraint(">= 3.0, < 3.2", bi.Version); ok {
			for _, v := range versions {
				expectError["distinct_"+v] = true
				expectError["findandmodify_"+v] = true
			}
		}
	}

	ex := New(ctx, client)
	for _, file := range files {
		t.Run(file.Name(), func(t *testing.T) {
			eq := proto.ExampleQuery{}
			err := tutil.LoadBson(dir+file.Name(), &eq)
			if err != nil {
				t.Fatalf("cannot load sample %s: %s", dir+file.Name(), err)
			}
			pretty.Println(eq)

			query, err := bson.MarshalExtJSON(eq, true, true)
			if err != nil {
				t.Fatalf("cannot marshal json %s: %s", dir+file.Name(), err)
			}
			got, err := ex.Run("", query)
			expectErrMsg := expectError[file.Name()]
			if (err != nil) != expectErrMsg {
				t.Fatalf("explain error for %q \n %s\nshould be '%v' but was '%v'", string(query), file.Name(), expectErrMsg, err)
			}

			if err == nil {
				result := proto.BsonD{}
				err = bson.UnmarshalExtJSON(got, true, &result)
				if err != nil {
					t.Fatalf("cannot unmarshal json explain result: %s", err)
				}
			}
		})
	}
}

func Constraint(constraint, version string) (bool, error) {
	// Drop everything after first dash.
	// Version with dash is considered a pre-release
	// but some MongoDB builds add additional information after dash
	// even though it's not considered a pre-release but a release.
	s := strings.SplitN(version, "-", 2)
	version = s[0]

	// Create new version
	v, err := semver.NewVersion(version)
	if err != nil {
		return false, err
	}

	// Check if version matches constraint
	constraints, err := semver.NewConstraint(constraint)
	if err != nil {
		return false, err
	}
	return constraints.Check(v), nil
}
