package driver

import (
	"context"
	"fmt"
	"io/ioutil"
	"local/dndex/storage/entity"
	"local/dndex/storage/operator"
	"os"
	"testing"
	"time"

	"github.com/boltdb/bolt"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/bson"
)

const (
	testN  = 5
	testNS = "col"
)

func TestNewBoltDB(t *testing.T) {
	_, can := tempBoltDB(t)
	defer can()
}

func TestBoltDBCount(t *testing.T) {
	bdb, can := tempBoltDB(t)
	defer can()

	ch, err := bdb.Find(context.TODO(), testNS, map[string]string{})
	if err != nil {
		t.Fatal(err)
	}
	ones := make([]entity.One, testN)
	i := 0
	for j := range ch {
		var o entity.One
		if err := bson.Unmarshal(j, &o); err != nil {
			t.Fatal(err)
		}
		ones[i] = o
		i++
	}

	for name, filter := range map[string]struct {
		filter interface{}
		match  bool
	}{
		"one.Query": {
			filter: ones[0].Query(),
			match:  true,
		},
		"title:title": {
			filter: map[string]interface{}{entity.Title: ones[1].Title},
			match:  true,
		},
		"title:title, text:text": {
			filter: map[string]interface{}{entity.Title: ones[2].Title, entity.Text: ones[2].Text},
			match:  true,
		},
		"title:title, text:gibberish": {
			filter: map[string]interface{}{entity.Title: ones[3].Title, entity.Text: ones[2].Text},
			match:  false,
		},
		"name:$in[gibberish]": {
			filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name + ones[1].Name}),
			match:  false,
		},
		"name:$in[name]": {
			filter: operator.NewFilterIn(entity.Name, []string{ones[0].Name}),
			match:  true,
		},
		"name:$regex[gibberish]": {
			filter: operator.Regex{Key: entity.Name, Value: ones[3].Name + ones[4].Name},
			match:  false,
		},
		"name:$regex[name]": {
			filter: operator.Regex{Key: entity.Name, Value: ones[3].Name},
			match:  true,
		},
	} {
		f := filter
		t.Run(name, func(t *testing.T) {
			n, err := bdb.count(context.TODO(), testNS, f.filter)
			if err != nil {
				t.Fatal(err)
			}
			if f.match && n != 1 {
				t.Fatalf("%v results for %+v, want match=%v", n, f, f.match)
			} else if !f.match && n != 0 {
				t.Fatalf("%v results for %+v, want match=%v", n, f, f.match)
			}
		})
	}
}

func TestBoltDBFind(t *testing.T) {
	bdb, can := tempBoltDB(t)
	defer can()

	ch, err := bdb.Find(context.TODO(), testNS, map[string]string{})
	if err != nil {
		t.Fatal(err)
	}
	n := 0
	for b := range ch {
		n++
		o := entity.One{}
		if err := bson.Unmarshal(b, &o); err != nil {
			t.Fatal(err)
		}
		if o.Type == "" {
			t.Error(o.Type)
		}
		if o.Title == "" {
			t.Error(o.Title)
		}
		if o.Text == "" {
			t.Error(o.Text)
		}
		if o.Relationship != "" {
			t.Error(o.Relationship)
		}
		if o.Modified == 0 {
			t.Error(o.Modified)
		}
		if len(o.Attachments) == 0 {
			t.Error(o.Attachments)
		}
		if len(o.Connections) == 0 {
			t.Error(o.Connections)
		}
		for k := range o.Connections {
			if o.Connections[k].Name == "" {
				t.Error(o.Connections[k])
			}
			if o.Connections[k].Title == "" {
				t.Error(o.Connections[k])
			}
			if o.Connections[k].Relationship == "" {
				t.Error(o.Connections[k])
			}
			if o.Connections[k].Type == "" {
				t.Error(o.Connections[k])
			}
		}
	}
	if n != testN {
		t.Fatal(n)
	}
}

func TestBoltDBUpdate(t *testing.T) {
	bdb, can := tempBoltDB(t)
	defer can()

	ch, err := bdb.Find(context.TODO(), testNS, map[string]string{})
	if err != nil {
		t.Fatal(err)
	}
	ones := make([]entity.One, testN)
	i := 0
	for j := range ch {
		var o entity.One
		if err := bson.Unmarshal(j, &o); err != nil {
			t.Fatal(err)
		}
		ones[i] = o
		i++
	}

	if err := bdb.Update(context.TODO(), testNS, ones[0].Query(), operator.Set{Key: entity.Name, Value: "NEWNAME"}); err == nil {
		t.Fatal(err)
	}

	if err := bdb.Update(context.TODO(), testNS, ones[0].Query(), operator.Set{Key: entity.Title, Value: "NEWTITLE"}); err != nil {
		t.Fatal(err)
	}

	if err := bdb.Update(context.TODO(), testNS, ones[0].Query(), operator.Unset(entity.Type)); err != nil {
		t.Fatal(err)
	}

	if n, err := bdb.count(context.TODO(), testNS, map[string]string{}); err != nil {
		t.Fatal(err)
	} else if n != testN {
		t.Fatal(n)
	}

	if n, err := bdb.count(context.TODO(), testNS, map[string]string{entity.Title: "NEWTITLE"}); err != nil {
		t.Fatal(err)
	} else if n != 1 {
		t.Fatal(n)
	}

	ch, err = bdb.Find(context.TODO(), testNS, ones[0].Query())
	if err != nil {
		t.Fatal(err)
	}
	i = 0
	for j := range ch {
		i++
		o := entity.One{}
		if err := bson.Unmarshal(j, &o); err != nil {
			t.Fatal(err)
		}
		if o.Type != "" {
			t.Fatalf("doc still has Type even though $unset called: %+v", o)
		}
		if fmt.Sprint(ones[0]) == fmt.Sprint(o) {
			t.Fatal(ones[0], o)
		}
		ones[0].Title = ""
		o.Title = ""
		if fmt.Sprint(ones[0]) == fmt.Sprint(o) {
			t.Fatal(ones[0], o)
		}
		ones[0].Type = ""
		o.Type = ""
		if fmt.Sprint(ones[0]) == fmt.Sprint(o) {
			t.Fatal(ones[0], o)
		}
		ones[0].Modified = 0
		o.Modified = 0
		if fmt.Sprint(ones[0]) != fmt.Sprint(o) {
			t.Fatalf("after removing fields that should differ, still not the same:\n%+v\n%+v", ones[0], o)
		}
	}
	if i != 1 {
		t.Fatal(i)
	}
}

func TestBoltDBInsert(t *testing.T) {
	bdb, can := tempBoltDB(t)
	defer can()

	ch, err := bdb.Find(context.TODO(), testNS, map[string]string{})
	if err != nil {
		t.Fatal(err)
	}
	ones := make([]entity.One, testN)
	i := 0
	for j := range ch {
		var o entity.One
		if err := bson.Unmarshal(j, &o); err != nil {
			t.Fatal(err)
		}
		ones[i] = o
		i++
	}

	if err := bdb.Insert(context.TODO(), testNS, ones[0]); err == nil {
		t.Fatal("could insert colliding object:", err)
	}

	ones[0].Name = "NEWNAME"
	if err := bdb.Insert(context.TODO(), testNS, ones[0]); err != nil {
		t.Fatal("could not insert object with new Name:", err)
	}

	if n, err := bdb.count(context.TODO(), testNS, ones[0].Query()); err != nil {
		t.Fatal(err)
	} else if n != 1 {
		t.Fatal(err)
	}

	ch, err = bdb.Find(context.TODO(), testNS, ones[0].Query())
	if err != nil {
		t.Fatal(err)
	}
	for i := range ch {
		o := entity.One{}
		if err := bson.Unmarshal(i, &o); err != nil {
			t.Fatal(err)
		}
		if fmt.Sprint(o) == fmt.Sprint(ones[0]) {
			t.Fatal(o, ones[0])
		}
		o.Modified = 0
		for k := range ones[0].Connections {
			if _, ok := o.Connections[k]; !ok {
				t.Fatalf("db had fewer connections than real: %s", k)
			}
		}
		for k := range o.Connections {
			if _, ok := ones[0].Connections[k]; !ok {
				t.Fatalf("db had more connections than real: %s", k)
			}
			c := o.Connections[k]
			c.Modified = 0
			o.Connections[k] = c

			c = ones[0].Connections[k]
			c.Modified = 0
			ones[0].Connections[k] = c
		}
		o.Modified = 0
		ones[0].Modified = 0
		if fmt.Sprint(o) != fmt.Sprint(ones[0]) {
			t.Fatalf("objects should match after removing modify:\n%+v\n%+v", o, ones[0])
		}
	}
}

func TestBoltDBDelete(t *testing.T) {
	bdb, can := tempBoltDB(t)
	defer can()

	ch, err := bdb.Find(context.TODO(), testNS, map[string]string{})
	if err != nil {
		t.Fatal(err)
	}
	ones := make([]entity.One, testN)
	i := 0
	for j := range ch {
		var o entity.One
		if err := bson.Unmarshal(j, &o); err != nil {
			t.Fatal(err)
		}
		ones[i] = o
		i++
	}

	wantN := testN
	for _, filter := range []interface{}{
		ones[0].Query(),
		operator.NewFilterIn(entity.Title, []string{ones[1].Title}),
		operator.Regex{Key: entity.Text, Value: ones[2].Text},
	} {
		err = bdb.Delete(context.TODO(), testNS, filter)
		if err != nil {
			t.Fatal(err)
		}
		wantN--
		n, err := bdb.count(context.TODO(), testNS, map[string]string{})
		if err != nil {
			t.Fatal(err)
		}
		if n != wantN {
			t.Error(n, filter)
		}
	}
}

func tempBoltDB(t *testing.T) (*BoltDB, func()) {
	f, err := ioutil.TempFile(os.TempDir(), "*.bolt.db")
	if err != nil {
		t.Fatal(err)
	}
	f.Close()
	os.Args = []string{"a", "-dburi", f.Name()}
	bdb := NewBoltDB()
	fillBoltDB(t, bdb)
	return bdb, func() {
		bdb.db.Close()
		os.Remove(f.Name())
	}
}

func fillBoltDB(t *testing.T, bdb *BoltDB) {
	if err := bdb.db.Update(func(tx *bolt.Tx) error {
		bucket, err := tx.CreateBucketIfNotExists([]byte(testNS))
		if err != nil {
			return err
		}
		for i := 0; i < testN; i++ {
			p := entity.One{
				Name:         "name-" + uuid.New().String()[:5],
				Type:         "type-" + uuid.New().String()[:5],
				Relationship: "rshp-" + uuid.New().String()[:5],
				Title:        "titl-" + uuid.New().String()[:5],
			}
			o := entity.One{
				Name:        "name-" + uuid.New().String()[:5],
				Type:        "type-" + uuid.New().String()[:5],
				Title:       "titl-" + uuid.New().String()[:5],
				Text:        "text-" + uuid.New().String()[:5],
				Modified:    time.Now().UnixNano(),
				Connections: map[string]entity.One{p.Name: p},
				Attachments: map[string]string{"filename": "/path/to/file"},
			}
			b, err := bson.Marshal(o)
			if err != nil {
				return err
			}
			if err := bucket.Put([]byte(o.Name), b); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		t.Fatal(err)
	}
}

func TestApplySet(t *testing.T) {
	cases := map[string]struct {
		doc      bson.M
		operator bson.M
		want     bson.M
	}{
		"noop on empty": {},
		"noop on full": {
			doc:  bson.M{"hello": "world"},
			want: bson.M{"hello": "world"},
		},
		"add new field on full": {
			operator: bson.M{"hi": "mom"},
			doc:      bson.M{"hello": "world"},
			want:     bson.M{"hello": "world", "hi": "mom"},
		},
		"change only field on full": {
			operator: bson.M{"hello": "lol jk not world"},
			doc:      bson.M{"hello": "world"},
			want:     bson.M{"hello": "lol jk not world"},
		},
		"set existing, nested field": {
			operator: bson.M{"hello.world": "hi"},
			doc:      bson.M{"hello": bson.M{"world": "not hi"}},
			want:     bson.M{"hello": bson.M{"world": "hi"}},
		},
		"add to existing, nested field": {
			operator: bson.M{"hello.notworld": "hi"},
			doc:      bson.M{"hello": bson.M{"world": "not hi"}},
			want:     bson.M{"hello": bson.M{"world": "not hi", "notworld": "hi"}},
		},
	}

	for name, d := range cases {
		c := d
		t.Run(name, func(t *testing.T) {
			out, err := applySet(c.doc, c.operator)
			if err != nil {
				t.Fatal(err)
			}
			if fmt.Sprint(out) != fmt.Sprint(c.want) {
				t.Fatalf("(%+v, %+v) => want \n%+v\n, got \n%+v", c.doc, c.operator, c.want, out)
			}
		})
	}
}
