/*
 * Copyright 2018 Intel Corporation, Inc
 *
 * 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
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package db

import (
	"bytes"
	"context"
	"reflect"
	"strings"
	"testing"

	pkgerrors "github.com/pkg/errors"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

//Implements the functions used currently in mongo.go
type mockCollection struct {
	Err          error
	mCursor      *mongo.Cursor
	mCursorCount int
}

func (c *mockCollection) InsertOne(ctx context.Context, document interface{},
	opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) {

	if c.Err != nil {
		return nil, c.Err
	}

	return &mongo.InsertOneResult{InsertedID: "_id1234"}, nil
}

func (c *mockCollection) FindOne(ctx context.Context, filter interface{},
	opts ...*options.FindOneOptions) *mongo.SingleResult {

	return &mongo.SingleResult{}
}

func (c *mockCollection) FindOneAndUpdate(ctx context.Context, filter interface{},
	update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult {

	return &mongo.SingleResult{}
}

func (c *mockCollection) DeleteOne(ctx context.Context, filter interface{},
	opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) {

	return nil, c.Err
}

func (c *mockCollection) Find(ctx context.Context, filter interface{},
	opts ...*options.FindOptions) (*mongo.Cursor, error) {

	return c.mCursor, c.Err
}

func TestCreate(t *testing.T) {
	testCases := []struct {
		label         string
		input         map[string]interface{}
		mockColl      *mockCollection
		bson          bson.Raw
		expectedError string
	}{
		{
			label: "Successfull creation of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
				"data": "Data In String Format",
			},
			bson:     bson.Raw{'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00'},
			mockColl: &mockCollection{},
		},
		{
			label: "UnSuccessfull creation of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
				"data": "Data In String Format",
			},
			mockColl: &mockCollection{
				Err: pkgerrors.New("DB Error"),
			},
			expectedError: "DB Error",
		},
		{
			label: "Missing input fields",
			input: map[string]interface{}{
				"coll": "",
				"key":  MockKey{Key: ""},
				"tag":  "",
				"data": "",
			},
			expectedError: "No Data to store",
			mockColl:      &mockCollection{},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.label, func(t *testing.T) {
			m, _ := NewMongoStore("name", &mongo.Database{})
			// Override the getCollection function with our mocked version
			getCollection = func(coll string, m *MongoStore) MongoCollection {
				return testCase.mockColl
			}

			decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) {
				return testCase.bson, testCase.mockColl.Err
			}

			err := m.Create(testCase.input["coll"].(string), testCase.input["key"].(Key),
				testCase.input["tag"].(string), testCase.input["data"])
			if err != nil {
				if testCase.expectedError == "" {
					t.Fatalf("Create method returned an un-expected (%s)", err)
				}
				if !strings.Contains(string(err.Error()), testCase.expectedError) {
					t.Fatalf("Create method returned an error (%s)", err)
				}
			}
		})
	}
}

func TestUpdate(t *testing.T) {
	testCases := []struct {
		label         string
		input         map[string]interface{}
		mockColl      *mockCollection
		bson          bson.Raw
		expectedError string
	}{
		{
			label: "Successfull update of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "metadata",
				"data": "Data In String Format",
			},
			// Binary form of
			// {
			//	"_id" : ObjectId("5c115156777ff85654248ae1"),
			//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
			//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
			// }
			bson: bson.Raw{
				'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
				'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
				'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
				'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
				'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
				'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
				'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
				'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
				'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
				'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
				'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
			},
			mockColl: &mockCollection{},
		},
		{
			label: "Entry does not exist",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
				"data": "Data In String Format",
			},
			mockColl: &mockCollection{
				Err: pkgerrors.New("DB Error"),
			},
			expectedError: "DB Error",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.label, func(t *testing.T) {
			m, _ := NewMongoStore("name", &mongo.Database{})
			// Override the getCollection function with our mocked version
			getCollection = func(coll string, m *MongoStore) MongoCollection {
				return testCase.mockColl
			}

			decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) {
				return testCase.bson, testCase.mockColl.Err
			}

			err := m.Update(testCase.input["coll"].(string), testCase.input["key"].(Key),
				testCase.input["tag"].(string), testCase.input["data"])
			if err != nil {
				if testCase.expectedError == "" {
					t.Fatalf("Create method returned an un-expected (%s)", err)
				}
				if !strings.Contains(string(err.Error()), testCase.expectedError) {
					t.Fatalf("Create method returned an error (%s)", err)
				}
			}
		})
	}
}

func TestRead(t *testing.T) {
	testCases := []struct {
		label         string
		input         map[string]interface{}
		mockColl      *mockCollection
		bson          bson.Raw
		expectedError string
		expected      []byte
	}{
		{
			label: "Successfull Read of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "metadata",
			},
			// Binary form of
			// {
			//	"_id" : ObjectId("5c115156777ff85654248ae1"),
			//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
			//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
			// }
			bson: bson.Raw{
				'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
				'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
				'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
				'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
				'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
				'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
				'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
				'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
				'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
				'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
				'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
			},
			mockColl: &mockCollection{},
			// This is not the document because we are mocking decodeBytes
			expected: []byte{92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225},
		},
		{
			label: "UnSuccessfull Read of entry: object not found",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "badtag",
			},
			// Binary form of
			// {
			//	"_id" : ObjectId("5c115156777ff85654248ae1"),
			//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
			//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
			// }
			bson: bson.Raw{
				'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
				'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
				'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
				'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
				'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
				'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
				'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
				'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
				'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
				'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
				'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
			},
			mockColl:      &mockCollection{},
			expectedError: "Error finding objectID",
		},
		{
			label: "UnSuccessfull Read of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
			},
			mockColl: &mockCollection{
				Err: pkgerrors.New("DB Error"),
			},
			expectedError: "DB Error",
		},
		{
			label: "Missing input fields",
			input: map[string]interface{}{
				"coll": "",
				"key":  MockKey{Key: ""},
				"tag":  "",
			},
			expectedError: "Mandatory fields are missing",
			mockColl:      &mockCollection{},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.label, func(t *testing.T) {
			m, _ := NewMongoStore("name", &mongo.Database{})
			// Override the getCollection function with our mocked version
			getCollection = func(coll string, m *MongoStore) MongoCollection {
				return testCase.mockColl
			}

			decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) {
				return testCase.bson, testCase.mockColl.Err
			}
			got, err := m.Read(testCase.input["coll"].(string), testCase.input["key"].(Key),
				testCase.input["tag"].(string))
			if err != nil {
				if testCase.expectedError == "" {
					t.Fatalf("Read method returned an un-expected (%s)", err)
				}
				if !strings.Contains(string(err.Error()), testCase.expectedError) {
					t.Fatalf("Read method returned an error (%s)", err)
				}
			} else {
				if bytes.Compare(got, testCase.expected) != 0 {
					t.Fatalf("Read returned unexpected data: %v, expected: %v",
						string(got), testCase.expected)
				}
			}
		})
	}
}

func TestDelete(t *testing.T) {
	testCases := []struct {
		label         string
		input         map[string]interface{}
		mockColl      *mockCollection
		bson          bson.Raw
		expectedError string
	}{
		{
			label: "Successfull Delete of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "metadata",
			},
			// Binary form of
			// {
			//	"_id" : ObjectId("5c115156777ff85654248ae1"),
			//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
			//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
			// }
			bson: bson.Raw{
				'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
				'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
				'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
				'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
				'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
				'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
				'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
				'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
				'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
				'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
				'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
			},
			mockColl: &mockCollection{},
		},
		{
			label: "UnSuccessfull Delete of entry",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
			},
			mockColl: &mockCollection{
				Err: pkgerrors.New("DB Error"),
			},
			expectedError: "DB Error",
		},
		{
			label: "UnSuccessfull Delete, key not found",
			input: map[string]interface{}{
				"coll": "collname",
				"key":  MockKey{Key: "keyvalue"},
				"tag":  "tagName",
			},
			// Binary form of
			// {
			//	"_id" : ObjectId("5c115156777ff85654248ae1"),
			//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
			//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
			// }
			bson: bson.Raw{
				'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
				'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
				'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
				'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
				'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
				'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
				'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
				'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
				'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
				'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
				'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
			},
			mockColl:      &mockCollection{},
			expectedError: "Error finding objectID",
		},
		{
			label: "Missing input fields",
			input: map[string]interface{}{
				"coll": "",
				"key":  MockKey{Key: ""},
				"tag":  "",
			},
			expectedError: "Mandatory fields are missing",
			mockColl:      &mockCollection{},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.label, func(t *testing.T) {
			m, _ := NewMongoStore("name", &mongo.Database{})
			// Override the getCollection function with our mocked version
			getCollection = func(coll string, m *MongoStore) MongoCollection {
				return testCase.mockColl
			}

			decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) {
				return testCase.bson, testCase.mockColl.Err
			}
			err := m.Delete(testCase.input["coll"].(string), testCase.input["key"].(Key),
				testCase.input["tag"].(string))
			if err != nil {
				if testCase.expectedError == "" {
					t.Fatalf("Delete method returned an un-expected (%s)", err)
				}
				if !strings.Contains(string(err.Error()), testCase.expectedError) {
					t.Fatalf("Delete method returned an error (%s)", err)
				}
			}
		})
	}
}

func TestReadAll(t *testing.T) {
	testCases := []struct {
		label         string
		input         map[string]interface{}
		mockColl      *mockCollection
		bson          bson.Raw
		expectedError string
		expected      map[string][]byte
	}{
		{
			label: "Successfully Read all entries",
			input: map[string]interface{}{
				"coll": "collname",
				"tag":  "metadata",
			},
			mockColl: &mockCollection{
				mCursor: &mongo.Cursor{
					// Binary form of
					// {
					//	"_id" : ObjectId("5c115156777ff85654248ae1"),
					//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
					//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
					// }

					Current: bson.Raw{
						'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
						'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
						'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
						'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
						'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
						'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
						'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
						'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
						'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
						'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
						'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
					},
				},
				mCursorCount: 1,
			},
			expected: map[string][]byte{
				`{"name": "testdef","version": "v1"}`: []byte{
					92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225},
			},
		},
		{
			label: "UnSuccessfully Read of all entries",
			input: map[string]interface{}{
				"coll": "collname",
				"tag":  "tagName",
			},
			mockColl: &mockCollection{
				Err: pkgerrors.New("DB Error"),
			},
			expectedError: "DB Error",
		},
		{
			label: "UnSuccessfull Readall, tag not found",
			input: map[string]interface{}{
				"coll": "collname",
				"tag":  "tagName",
			},
			mockColl: &mockCollection{
				mCursor: &mongo.Cursor{
					// Binary form of
					// {
					//	"_id" : ObjectId("5c115156777ff85654248ae1"),
					//  "key" : bson.D{{"name","testdef"},{"version","v1"}},
					//  "metadata" : ObjectId("5c115156c9755047e318bbfd")
					// }
					Current: bson.Raw{
						'\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79',
						'\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61',
						'\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74',
						'\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02',
						'\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00',
						'\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00',
						'\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74',
						'\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f',
						'\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f',
						'\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77',
						'\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00',
					},
				},
				mCursorCount: 1,
			},
			expectedError: "Did not find any objects with tag",
		},
		{
			label: "Missing input fields",
			input: map[string]interface{}{
				"coll": "",
				"tag":  "",
			},
			expectedError: "Missing collection or tag name",
			mockColl:      &mockCollection{},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.label, func(t *testing.T) {
			m, _ := NewMongoStore("name", &mongo.Database{})
			// Override the getCollection function with our mocked version
			getCollection = func(coll string, m *MongoStore) MongoCollection {
				return testCase.mockColl
			}

			decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) {
				return testCase.mockColl.mCursor.Current, testCase.mockColl.Err
			}

			cursorNext = func(ctx context.Context, cursor *mongo.Cursor) bool {
				if testCase.mockColl.mCursorCount > 0 {
					testCase.mockColl.mCursorCount -= 1
					return true
				}
				return false
			}

			cursorClose = func(ctx context.Context, cursor *mongo.Cursor) error {
				return nil
			}

			got, err := m.ReadAll(testCase.input["coll"].(string), testCase.input["tag"].(string))
			if err != nil {
				if testCase.expectedError == "" {
					t.Fatalf("Readall method returned an un-expected (%s)", err)
				}
				if !strings.Contains(string(err.Error()), testCase.expectedError) {
					t.Fatalf("Readall method returned an error (%s)", err)
				}
			} else {
				if reflect.DeepEqual(got, testCase.expected) == false {
					t.Fatalf("Readall returned unexpected data: %v, expected: %v",
						got, testCase.expected)
				}
			}
		})
	}
}