// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package integration

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"reflect"
	"strings"
	"testing"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/internal/testutil/assert"
	"go.mongodb.org/mongo-driver/internal/testutil/helpers"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)

const (
	crudTestsDir = "../../testdata/crud"
	crudReadDir  = "v1/read"
	crudWriteDir = "v1/write"
)

type crudTestFile struct {
	Data             []bson.Raw `bson:"data"`
	MinServerVersion string     `bson:"minServerVersion"`
	MaxServerVersion string     `bson:"maxServerVersion"`
	Serverless       string     `bson:"serverless"`
	Tests            []crudTest `bson:"tests"`
}

type crudTest struct {
	Description string        `bson:"description"`
	SkipReason  string        `bson:"skipReason"`
	Operation   crudOperation `bson:"operation"`
	Outcome     crudOutcome   `bson:"outcome"`
}

type crudOperation struct {
	Name      string   `bson:"name"`
	Arguments bson.Raw `bson:"arguments"`
}

type crudOutcome struct {
	Error      bool               `bson:"error"` // only used by retryable writes tests
	Result     interface{}        `bson:"result"`
	Collection *outcomeCollection `bson:"collection"`
}

var crudRegistry = bson.NewRegistryBuilder().
	RegisterTypeMapEntry(bson.TypeEmbeddedDocument, reflect.TypeOf(bson.Raw{})).Build()

func TestCrudSpec(t *testing.T) {
	for _, dir := range []string{crudReadDir, crudWriteDir} {
		for _, file := range jsonFilesInDir(t, path.Join(crudTestsDir, dir)) {
			t.Run(file, func(t *testing.T) {
				runCrudFile(t, path.Join(crudTestsDir, dir, file))
			})
		}
	}
}

func verifyServerlessConstraint(mt *mtest.T, expected string) error {
	serverless := os.Getenv("SERVERLESS") == "serverless"

	switch expected {
	case "require":
		if !serverless {
			return fmt.Errorf("test requires serverless")
		}
	case "forbid":
		if serverless {
			return fmt.Errorf("test forbids serverless")
		}
	case "allow", "":
	default:
		mt.Fatalf("invalid value for serverless: %s", expected)
	}
	return nil
}

func runCrudFile(t *testing.T, file string) {
	content, err := ioutil.ReadFile(file)
	assert.Nil(t, err, "ReadFile error for %v: %v", file, err)

	var testFile crudTestFile
	err = bson.UnmarshalExtJSONWithRegistry(crudRegistry, content, false, &testFile)
	assert.Nil(t, err, "UnmarshalExtJSONWithRegistry error: %v", err)

	mt := mtest.New(t, mtest.NewOptions().MinServerVersion(testFile.MinServerVersion).MaxServerVersion(testFile.MaxServerVersion))
	defer mt.Close()

	// Skip test if serverless requirements are not met.
	if err = verifyServerlessConstraint(mt, testFile.Serverless); err != nil {
		mt.Skipf("%v", err)
	}

	for _, test := range testFile.Tests {
		mt.Run(test.Description, func(mt *mtest.T) {
			runCrudTest(mt, test, testFile)
		})
	}
}

func runCrudTest(mt *mtest.T, test crudTest, testFile crudTestFile) {
	if len(testFile.Data) > 0 {
		docs := helpers.RawToInterfaces(testFile.Data...)
		_, err := mt.Coll.InsertMany(context.Background(), docs)
		assert.Nil(mt, err, "InsertMany error: %v", err)
	}

	runCrudOperation(mt, test.Description, test.Operation, test.Outcome)
}

func verifyCrudError(mt *mtest.T, outcome crudOutcome, err error) {
	opError := errorFromResult(mt, outcome.Result)
	if opError == nil {
		return
	}
	verificationErr := verifyError(opError, err)
	assert.Nil(mt, verificationErr, "error mismatch: %v", verificationErr)
}

// run a CRUD operation and verify errors and outcomes.
// the test description is needed to see determine if the test is an aggregate with $out
func runCrudOperation(mt *mtest.T, testDescription string, operation crudOperation, outcome crudOutcome) {
	switch operation.Name {
	case "aggregate":
		cursor, err := executeAggregate(mt, mt.Coll, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected Aggregate error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "Aggregate error: %v", err)
		// only verify cursor contents for pipelines without $out
		if !strings.Contains(testDescription, "$out") {
			verifyCursorResult(mt, cursor, outcome.Result)
		}
	case "bulkWrite":
		res, err := executeBulkWrite(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected BulkWrite error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "BulkWrite error: %v", err)
		verifyBulkWriteResult(mt, res, outcome.Result)
	case "count":
		res, err := executeCountDocuments(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected CountDocuments error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "CountDocuments error: %v", err)
		verifyCountResult(mt, res, outcome.Result)
	case "distinct":
		res, err := executeDistinct(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected Distinct error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "Distinct error: %v", err)
		verifyDistinctResult(mt, res, outcome.Result)
	case "find":
		cursor, err := executeFind(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected Find error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "Find error: %v", err)
		verifyCursorResult(mt, cursor, outcome.Result)
	case "deleteOne":
		res, err := executeDeleteOne(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected DeleteOne error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "DeleteOne error: %v", err)
		verifyDeleteResult(mt, res, outcome.Result)
	case "deleteMany":
		res, err := executeDeleteMany(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected DeleteMany error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "DeleteMany error: %v", err)
		verifyDeleteResult(mt, res, outcome.Result)
	case "findOneAndDelete":
		res := executeFindOneAndDelete(mt, nil, operation.Arguments)
		err := res.Err()
		if outcome.Error {
			assert.NotNil(mt, err, "expected FindOneAndDelete error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		if outcome.Result == nil {
			assert.Equal(mt, mongo.ErrNoDocuments, err, "expected error %v, got %v", mongo.ErrNoDocuments, err)
			break
		}
		assert.Nil(mt, err, "FindOneAndDelete error: %v", err)
		verifySingleResult(mt, res, outcome.Result)
	case "findOneAndReplace":
		res := executeFindOneAndReplace(mt, nil, operation.Arguments)
		err := res.Err()
		if outcome.Error {
			assert.NotNil(mt, err, "expected FindOneAndReplace error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		if outcome.Result == nil {
			assert.Equal(mt, mongo.ErrNoDocuments, err, "expected error %v, got %v", mongo.ErrNoDocuments, err)
			break
		}
		assert.Nil(mt, err, "FindOneAndReplace error: %v", err)
		verifySingleResult(mt, res, outcome.Result)
	case "findOneAndUpdate":
		res := executeFindOneAndUpdate(mt, nil, operation.Arguments)
		err := res.Err()
		if outcome.Error {
			assert.NotNil(mt, err, "expected FindOneAndUpdate error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		if outcome.Result == nil {
			assert.Equal(mt, mongo.ErrNoDocuments, err, "expected error %v, got %v", mongo.ErrNoDocuments, err)
			break
		}
		assert.Nil(mt, err, "FindOneAndUpdate error: %v", err)
		verifySingleResult(mt, res, outcome.Result)
	case "insertOne":
		res, err := executeInsertOne(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected InsertOne error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "InsertOne error: %v", err)
		verifyInsertOneResult(mt, res, outcome.Result)
	case "insertMany":
		res, err := executeInsertMany(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected InsertMany error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "InsertMany error: %v", err)
		verifyInsertManyResult(mt, res, outcome.Result)
	case "replaceOne":
		res, err := executeReplaceOne(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected ReplaceOne error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "ReplaceOne error: %v", err)
		verifyUpdateResult(mt, res, outcome.Result)
	case "updateOne":
		res, err := executeUpdateOne(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected UpdateOne error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "UpdateOne error: %v", err)
		verifyUpdateResult(mt, res, outcome.Result)
	case "updateMany":
		res, err := executeUpdateMany(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected UpdateMany error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "UpdateMany error: %v", err)
		verifyUpdateResult(mt, res, outcome.Result)
	case "estimatedDocumentCount":
		res, err := executeEstimatedDocumentCount(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected EstimatedDocumentCount error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "EstimatedDocumentCount error: %v", err)
		verifyCountResult(mt, res, outcome.Result)
	case "countDocuments":
		res, err := executeCountDocuments(mt, nil, operation.Arguments)
		if outcome.Error {
			assert.NotNil(mt, err, "expected CountDocuments error, got nil")
			verifyCrudError(mt, outcome, err)
			break
		}
		assert.Nil(mt, err, "CountDocuments error: %v", err)
		verifyCountResult(mt, res, outcome.Result)
	default:
		mt.Fatalf("unrecognized operation: %v", operation.Name)
	}

	if outcome.Collection != nil {
		verifyTestOutcome(mt, outcome.Collection)
	}
}
