// Copyright (c) 2012-present The upper.io/db authors. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Tests for the mongodb adapter.
package mongo

import (
	"fmt"
	"log"
	"math/rand"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/suite"
	"gopkg.in/mgo.v2/bson"
	db "upper.io/db.v3"
	"upper.io/db.v3/internal/testsuite"
)

type artistType struct {
	ID   bson.ObjectId `bson:"_id,omitempty"`
	Name string        `bson:"name"`
}

// Structure for testing conversions and datatypes.
type testValuesStruct struct {
	Uint   uint   `bson:"_uint"`
	Uint8  uint8  `bson:"_uint8"`
	Uint16 uint16 `bson:"_uint16"`
	Uint32 uint32 `bson:"_uint32"`
	Uint64 uint64 `bson:"_uint64"`

	Int   int   `bson:"_int"`
	Int8  int8  `bson:"_int8"`
	Int16 int16 `bson:"_int16"`
	Int32 int32 `bson:"_int32"`
	Int64 int64 `bson:"_int64"`

	Float32 float32 `bson:"_float32"`
	Float64 float64 `bson:"_float64"`

	Bool   bool   `bson:"_bool"`
	String string `bson:"_string"`

	Date  time.Time     `bson:"_date"`
	DateN *time.Time    `bson:"_nildate"`
	DateP *time.Time    `bson:"_ptrdate"`
	Time  time.Duration `bson:"_time"`
}

var testValues testValuesStruct

func init() {
	t := time.Date(2012, 7, 28, 1, 2, 3, 0, time.Local)

	testValues = testValuesStruct{
		1, 1, 1, 1, 1,
		-1, -1, -1, -1, -1,
		1.337, 1.337,
		true,
		"Hello world!",
		t,
		nil,
		&t,
		time.Second * time.Duration(7331),
	}
}

type AdapterTests struct {
	testsuite.Suite
}

func (s *AdapterTests) SetupSuite() {
	s.Helper = &Helper{}
}

func (s *AdapterTests) TestOpenWithWrongData() {
	var err error
	var rightSettings, wrongSettings ConnectionURL

	// Attempt to open with safe settings.
	rightSettings = ConnectionURL{
		Database: settings.Database,
		Host:     settings.Host,
		User:     settings.User,
		Password: settings.Password,
	}

	// Attempt to open an empty database.
	_, err = Open(rightSettings)
	s.NoError(err)

	// Attempt to open with wrong password.
	wrongSettings = ConnectionURL{
		Database: settings.Database,
		Host:     settings.Host,
		User:     settings.User,
		Password: "fail",
	}

	_, err = Open(wrongSettings)
	s.Error(err)

	// Attempt to open with wrong database.
	wrongSettings = ConnectionURL{
		Database: "fail",
		Host:     settings.Host,
		User:     settings.User,
		Password: settings.Password,
	}

	_, err = Open(wrongSettings)
	s.Error(err)

	// Attempt to open with wrong username.
	wrongSettings = ConnectionURL{
		Database: settings.Database,
		Host:     settings.Host,
		User:     "fail",
		Password: settings.Password,
	}

	_, err = Open(wrongSettings)
	s.Error(err)
}

func (s *AdapterTests) TestTruncate() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a list of all collections in this database.
	collections, err := sess.Collections()
	s.NoError(err)

	for _, name := range collections {

		// Pointing the collection.
		col := sess.Collection(name)

		// The collection may ot may not exists.
		exists := col.Exists()

		if exists == true {
			// Truncating the structure, if exists.
			err = col.Truncate()
			s.NoError(err)
		}
	}
}

func (s *AdapterTests) TestInsert() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "artist" collection.
	artist := sess.Collection("artist")
	_ = artist.Truncate()

	// Inserting a map.
	id, err := artist.Insert(map[string]string{
		"name": "Ozzie",
	})
	s.NoError(err)
	s.NotZero(id)

	_, ok := id.(bson.ObjectId)
	s.True(ok)

	s.True(id.(bson.ObjectId).Valid())

	// Inserting a struct.
	id, err = artist.Insert(struct {
		Name string
	}{
		"Flea",
	})
	s.NoError(err)
	s.NotZero(id)

	_, ok = id.(bson.ObjectId)
	s.True(ok)
	s.True(id.(bson.ObjectId).Valid())

	// Inserting a struct (using tags to specify the field name).
	id, err = artist.Insert(struct {
		ArtistName string `bson:"name"`
	}{
		"Slash",
	})
	s.NoError(err)
	s.NotNil(id)

	_, ok = id.(bson.ObjectId)
	s.True(ok)
	s.True(id.(bson.ObjectId).Valid())

	// Inserting a pointer to a struct
	id, err = artist.Insert(&struct {
		ArtistName string `bson:"name"`
	}{
		"Metallica",
	})
	s.NoError(err)
	s.NotZero(id)

	_, ok = id.(bson.ObjectId)
	s.True(ok)
	s.True(id.(bson.ObjectId).Valid())

	// Inserting a pointer to a map
	id, err = artist.Insert(&map[string]string{
		"name": "Freddie",
	})
	s.NoError(err)
	s.NotZero(id)

	_, ok = id.(bson.ObjectId)
	s.True(ok)
	s.True(id.(bson.ObjectId).Valid())

	// Counting elements, must be exactly 6 elements.
	total, err := artist.Find().Count()
	s.NoError(err)
	s.Equal(uint64(5), total)
}

func (s *AdapterTests) TestGetNonExistentRow_Issue426() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	defer sess.Close()

	artist := sess.Collection("artist")

	var one artistType
	err = artist.Find(db.Cond{"name": "nothing"}).One(&one)

	s.NotZero(err)
	s.Equal(db.ErrNoMoreRows, err)

	var all []artistType
	err = artist.Find(db.Cond{"name": "nothing"}).All(&all)

	s.Zero(err, "All should not return mgo.ErrNotFound")
	s.Equal(0, len(all))
}

func (s *AdapterTests) TestResultCount() {
	var err error
	var res db.Result

	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	defer sess.Close()

	// We should close the database when it's no longer in use.
	artist := sess.Collection("artist")

	res = artist.Find()

	// Counting all the matching rows.
	total, err := res.Count()
	s.NoError(err)
	s.NotZero(total)
}

func (s *AdapterTests) TestGroup() {
	var stats db.Collection

	sess, err := Open(settings)
	s.NoError(err)

	type statsT struct {
		Numeric int `db:"numeric" bson:"numeric"`
		Value   int `db:"value" bson:"value"`
	}

	defer sess.Close()

	stats = sess.Collection("statsTest")

	// Truncating table.
	_ = stats.Truncate()

	// Adding row append.
	for i := 0; i < 1000; i++ {
		numeric, value := rand.Intn(10), rand.Intn(100)
		_, err = stats.Insert(statsT{numeric, value})
		s.NoError(err)
	}

	// db.statsTest.group({key: {numeric: true}, initial: {sum: 0}, reduce: function(doc, prev) { prev.sum += 1}});

	// Testing GROUP BY
	res := stats.Find().Group(bson.M{
		"key":     bson.M{"numeric": true},
		"initial": bson.M{"sum": 0},
		"reduce":  `function(doc, prev) { prev.sum += 1}`,
	})

	var results []map[string]interface{}

	err = res.All(&results)
	s.Equal(db.ErrUnsupported, err)
}

func (s *AdapterTests) TestResultNonExistentCount() {
	sess, err := Open(settings)
	s.NoError(err)

	defer sess.Close()

	total, err := sess.Collection("notartist").Find().Count()
	s.NoError(err)
	s.Zero(total)
}

func (s *AdapterTests) TestResultFetch() {

	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	artist := sess.Collection("artist")

	// Testing map
	res := artist.Find()

	rowM := map[string]interface{}{}

	for res.Next(&rowM) {
		s.NotZero(rowM["_id"])

		_, ok := rowM["_id"].(bson.ObjectId)
		s.True(ok)

		s.True(rowM["_id"].(bson.ObjectId).Valid())

		name, ok := rowM["name"].(string)
		s.True(ok)
		s.NotZero(name)
	}

	err = res.Close()
	s.NoError(err)

	// Testing struct
	rowS := struct {
		ID   bson.ObjectId `bson:"_id"`
		Name string        `bson:"name"`
	}{}

	res = artist.Find()

	for res.Next(&rowS) {
		s.True(rowS.ID.Valid())
		s.NotZero(rowS.Name)
	}

	err = res.Close()
	s.NoError(err)

	// Testing tagged struct
	rowT := struct {
		Value1 bson.ObjectId `bson:"_id"`
		Value2 string        `bson:"name"`
	}{}

	res = artist.Find()

	for res.Next(&rowT) {
		s.True(rowT.Value1.Valid())
		s.NotZero(rowT.Value2)
	}

	err = res.Close()
	s.NoError(err)

	// Testing Result.All() with a slice of maps.
	res = artist.Find()

	allRowsM := []map[string]interface{}{}
	err = res.All(&allRowsM)
	s.NoError(err)

	for _, singleRowM := range allRowsM {
		s.NotZero(singleRowM["_id"])
	}

	// Testing Result.All() with a slice of structs.
	res = artist.Find()

	allRowsS := []struct {
		ID   bson.ObjectId `bson:"_id"`
		Name string
	}{}
	err = res.All(&allRowsS)
	s.NoError(err)

	for _, singleRowS := range allRowsS {
		s.True(singleRowS.ID.Valid())
	}

	// Testing Result.All() with a slice of tagged structs.
	res = artist.Find()

	allRowsT := []struct {
		Value1 bson.ObjectId `bson:"_id"`
		Value2 string        `bson:"name"`
	}{}
	err = res.All(&allRowsT)
	s.NoError(err)

	for _, singleRowT := range allRowsT {
		s.True(singleRowT.Value1.Valid())
	}
}

func (s *AdapterTests) TestUpdate() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "artist" collection.
	artist := sess.Collection("artist")

	// Value
	value := struct {
		ID   bson.ObjectId `bson:"_id"`
		Name string
	}{}

	// Getting the first artist.
	res := artist.Find(db.Cond{"_id": db.NotEq(nil)}).Limit(1)

	err = res.One(&value)
	s.NoError(err)

	// Updating with a map
	rowM := map[string]interface{}{
		"name": strings.ToUpper(value.Name),
	}

	err = res.Update(rowM)
	s.NoError(err)

	err = res.One(&value)
	s.NoError(err)

	s.Equal(value.Name, rowM["name"])

	// Updating with a struct
	rowS := struct {
		Name string
	}{strings.ToLower(value.Name)}

	err = res.Update(rowS)
	s.NoError(err)

	err = res.One(&value)
	s.NoError(err)

	s.Equal(value.Name, rowS.Name)

	// Updating with a tagged struct
	rowT := struct {
		Value1 string `bson:"name"`
	}{strings.Replace(value.Name, "z", "Z", -1)}

	err = res.Update(rowT)
	s.NoError(err)

	err = res.One(&value)
	s.NoError(err)

	s.Equal(value.Name, rowT.Value1)
}

func (s *AdapterTests) TestOperators() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "artist" collection.
	artist := sess.Collection("artist")

	rowS := struct {
		ID   uint64
		Name string
	}{}

	res := artist.Find(db.Cond{"_id": db.NotIn([]int{0, -1})})

	err = res.One(&rowS)
	s.NoError(err)

	err = res.Close()
	s.NoError(err)
}

func (s *AdapterTests) TestDelete() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "artist" collection.
	artist := sess.Collection("artist")

	// Getting the first artist.
	res := artist.Find(db.Cond{"_id": db.NotEq(nil)}).Limit(1)

	var first struct {
		ID bson.ObjectId `bson:"_id"`
	}

	err = res.One(&first)
	s.NoError(err)

	res = artist.Find(db.Cond{"_id": db.Eq(first.ID)})

	// Trying to remove the row.
	err = res.Delete()
	s.NoError(err)
}

func (s *AdapterTests) TestDataTypes() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "data_types" collection.
	dataTypes := sess.Collection("data_types")

	// Inserting our test subject.
	id, err := dataTypes.Insert(testValues)
	s.NoError(err)
	s.NotZero(id)

	// Trying to get the same subject we added.
	res := dataTypes.Find(db.Cond{"_id": db.Eq(id)})

	exists, err := res.Count()
	s.NoError(err)
	s.NotZero(exists)

	// Trying to dump the subject into an empty structure of the same type.
	var item testValuesStruct
	err = res.One(&item)
	s.NoError(err)

	// The original value and the test subject must match.
	s.Equal(testValues, item)
}

func (s *AdapterTests) TestPaginator() {
	// Opening database.
	sess, err := Open(settings)
	s.NoError(err)

	// We should close the database when it's no longer in use.
	defer sess.Close()

	// Getting a pointer to the "artist" collection.
	artist := sess.Collection("artist")

	err = artist.Truncate()
	s.NoError(err)

	for i := 0; i < 999; i++ {
		_, err = artist.Insert(artistType{
			Name: fmt.Sprintf("artist-%d", i),
		})
		s.NoError(err)
	}

	q := sess.Collection("artist").Find().Paginate(15)
	paginator := q.Paginate(13)

	var zerothPage []artistType
	err = paginator.Page(0).All(&zerothPage)
	s.NoError(err)
	s.Equal(13, len(zerothPage))

	var secondPage []artistType
	err = paginator.Page(2).All(&secondPage)
	s.NoError(err)
	s.Equal(13, len(secondPage))

	tp, err := paginator.TotalPages()
	s.NoError(err)
	s.NotZero(tp)
	s.Equal(uint(77), tp)

	ti, err := paginator.TotalEntries()
	s.NoError(err)
	s.NotZero(ti)
	s.Equal(uint64(999), ti)

	var seventySixthPage []artistType
	err = paginator.Page(76).All(&seventySixthPage)
	s.NoError(err)
	s.Equal(11, len(seventySixthPage))

	var seventySeventhPage []artistType
	err = paginator.Page(77).All(&seventySeventhPage)
	s.NoError(err)
	s.Equal(0, len(seventySeventhPage))

	var hundredthPage []artistType
	err = paginator.Page(100).All(&hundredthPage)
	s.NoError(err)
	s.Equal(0, len(hundredthPage))

	for i := uint(0); i < tp; i++ {
		current := paginator.Page(i)

		var items []artistType
		err := current.All(&items)
		s.NoError(err)
		if len(items) < 1 {
			break
		}
		for j := 0; j < len(items); j++ {
			s.Equal(fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name)
		}
	}

	paginator = paginator.Cursor("_id")
	{
		current := paginator.Page(0)
		for i := 0; ; i++ {
			var items []artistType
			err := current.All(&items)
			s.NoError(err)

			if len(items) < 1 {
				break
			}

			for j := 0; j < len(items); j++ {
				s.Equal(fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name)
			}
			current = current.NextPage(items[len(items)-1].ID)
		}
	}

	{
		log.Printf("Page 76")
		current := paginator.Page(76)
		for i := 76; ; i-- {
			var items []artistType

			err := current.All(&items)
			s.NoError(err)

			if len(items) < 1 {
				s.Equal(0, len(items))
				break
			}
			for j := 0; j < len(items); j++ {
				s.Equal(fmt.Sprintf("artist-%d", 13*int(i)+j), items[j].Name)
			}

			current = current.PrevPage(items[0].ID)
		}
	}

	{
		resultPaginator := sess.Collection("artist").Find().Paginate(15)

		count, err := resultPaginator.TotalPages()
		s.Equal(uint(67), count)
		s.NoError(err)

		var items []artistType
		err = resultPaginator.Page(5).All(&items)
		s.NoError(err)

		for j := 0; j < len(items); j++ {
			s.Equal(fmt.Sprintf("artist-%d", 15*5+j), items[j].Name)
		}

		resultPaginator = resultPaginator.Cursor("_id").Page(0)
		for i := 0; ; i++ {
			var items []artistType

			err = resultPaginator.All(&items)
			s.NoError(err)

			if len(items) < 1 {
				break
			}

			for j := 0; j < len(items); j++ {
				s.Equal(fmt.Sprintf("artist-%d", 15*i+j), items[j].Name)
			}
			resultPaginator = resultPaginator.NextPage(items[len(items)-1].ID)
		}

		resultPaginator = resultPaginator.Cursor("_id").Page(66)
		for i := 66; ; i-- {
			var items []artistType

			err = resultPaginator.All(&items)
			s.NoError(err)

			if len(items) < 1 {
				break
			}

			for j := 0; j < len(items); j++ {
				s.Equal(fmt.Sprintf("artist-%d", 15*i+j), items[j].Name)
			}
			resultPaginator = resultPaginator.PrevPage(items[0].ID)
		}
	}
}

func TestAdapter(t *testing.T) {
	suite.Run(t, &AdapterTests{})
}