/*
 * EliasDB
 *
 * Copyright 2016 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package util

import (
	"fmt"
	"strings"
	"testing"

	"devt.de/krotik/common/bitutil"
	"devt.de/krotik/eliasdb/hash"
	"devt.de/krotik/eliasdb/storage"
)

func TestIndexManager(t *testing.T) {
	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	obj1 := make(map[string]string)
	obj1["aaa"] = "DDD voldaaa ddd"
	obj1["bbb"] = "vbbb"

	im.Index("testkey", obj1)

	if res, _ := im.LookupWord("aaa", "ddd"); fmt.Sprint(res) != "map[testkey:[1 3]]" {
		t.Error("Unexpected lookup result:", res)
		return
	}

	CaseSensitiveWordIndex = true

	sm = storage.NewMemoryStorageManager("testsm")
	htree, _ = hash.NewHTree(sm)

	im = NewIndexManager(htree)

	im.Index("testkey", obj1)

	if res, _ := im.LookupWord("aaa", "ddd"); fmt.Sprint(res) != "map[testkey:[3]]" {
		t.Error("Unexpected lookup result:", res)
		return
	}

	if res, _ := im.Count("aaa", "ddd"); res != 1 {
		t.Error("Unexpected count result:", res)
		return
	}

	CaseSensitiveWordIndex = false

	sm = storage.NewMemoryStorageManager("testsm")
	htree, _ = hash.NewHTree(sm)

	im = NewIndexManager(htree)

	im.Index("testkey", obj1)

	if res, _ := im.LookupWord("aaa", "ddd"); fmt.Sprint(res) != "map[testkey:[1 3]]" {
		t.Error("Unexpected lookup result:", res)
		return
	}

	for i := 0; i < 6; i++ {
		sm.AccessMap[uint64(i)] = storage.AccessCacheAndFetchError
	}

	if _, err := im.LookupWord("aaa", "ddd"); !strings.Contains(err.Error(), "Slot not found") {
		t.Error("Unexpected lookup result:", err)
		return
	}

	if err := im.Index("testkey", obj1); !strings.Contains(err.Error(), "Slot not found") {
		t.Error("Unexpected index result:", err)
		return
	}

	if err := im.Deindex("testkey", obj1); !strings.Contains(err.Error(), "Slot not found") {
		t.Error("Unexpected index result:", err)
		return
	}

	for i := 0; i < 6; i++ {
		delete(sm.AccessMap, uint64(i))
	}

	obj2 := make(map[string]string)
	obj2["aaa"] = "ddd vnewaaa"
	obj2["ccc"] = "ccc"

	if err := im.Reindex("testkey", obj2, obj1); err != nil {
		t.Error(err)
		return
	}

	if res := countChildren(htree); res != 5 {
		t.Error("Unexpected number of children:", res)
		return
	}

	if res, err := im.LookupWord("aaa", "DdD"); fmt.Sprint(res) != "map[testkey:[1]]" {
		t.Error("Unexpected lookup result:", res, err)
		return
	}

	if res, _ := im.Count("aaa", "dDD"); res != 1 {
		t.Error("Unexpected count result:", res)
		return
	}

	if res, _ := im.Count("aab", "dDD"); res != 0 {
		t.Error("Unexpected count result:", res)
		return
	}

	if err := im.Deindex("testkey", obj2); err != nil {
		t.Error(err)
		return
	}

	if res := countChildren(htree); res != 0 {
		t.Error("Unexpected number of children:", res)
		return
	}
}

func TestPhraseSearch(t *testing.T) {

	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	obj1 := make(map[string]string)
	obj1["aaa"] = "the Is xxx The grass Is grEEn zzz"

	im.Index("testkey", obj1)

	obj2 := make(map[string]string)
	obj2["aaa"] = "the Is xxx The grass Is graY zzz"

	im.Index("testkey2", obj2)

	obj3 := make(map[string]string)
	obj3["aaa"] = "green grass is green zzz"

	im.Index("testkey3", obj3)

	obj4 := make(map[string]string)
	obj4["aaa"] = "green grass is so green zzz"
	obj4["bbb"] = "test"

	im.Index("testkey4", obj4)

	res, err := im.LookupPhrase("aaa", "grass is green")

	if fmt.Sprint(res) != "[testkey testkey3]" || err != nil {
		t.Error("Unexpected lookup result:", res, err)
	}

	res, err = im.LookupPhrase("aaa", "zzz")

	if fmt.Sprint(res) != "[testkey testkey2 testkey3 testkey4]" || err != nil {
		t.Error("Unexpected lookup result:", res, err)
	}

	res, err = im.LookupPhrase("bbb", "test")

	if fmt.Sprint(res) != "[testkey4]" || err != nil {
		t.Error("Unexpected lookup result:", res, err)
	}

	// Test empty return cases

	res, err = im.LookupPhrase("bbb", "")
	if res != nil || err != nil {
		t.Error("Unexpected result:", res, err)
	}

	res, err = im.LookupPhrase("bbb", "b")
	if res != nil || err != nil {
		t.Error("Unexpected result:", res, err)
	}

	for i := 0; i < 10; i++ {
		sm.AccessMap[uint64(i)] = storage.AccessCacheAndFetchError
	}

	res, err = im.LookupPhrase("aaa", "grass is green")
	if res != nil || !strings.Contains(err.Error(), "Slot not found") {
		t.Error("Unexpected result:", res, err)
	}

	if _, err := im.Count("aaa", "grass"); !strings.Contains(err.Error(), "Slot not found") {
		t.Error("Unexpected count result:", err)
		return
	}

	for i := 0; i < 10; i++ {
		delete(sm.AccessMap, uint64(i))
	}
}

func TestUpdateIndex(t *testing.T) {

	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	obj1 := make(map[string]string)
	obj1["aaa"] = "vnewaaa ddd"
	obj1["bbb"] = "vbbb"

	obj2 := make(map[string]string)
	obj2["aaa"] = "ddd voldaaa"
	obj2["ccc"] = "ccc"

	// Insert into the index

	im.updateIndex("123", obj2, nil)

	// Check that the entries exist

	entry, _ := htree.Get([]byte(PrefixAttrWord + "aaa" + "ddd"))
	pos := entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[1]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "aaa" + "voldaaa"))
	pos = entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[2]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "ccc" + "ccc"))
	pos = entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[1]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	if res := countChildren(htree); res != 5 {
		t.Error("Unexpected number of children:", res)
		return
	}

	// Update the index

	im.updateIndex("123", obj1, obj2)

	if res := countChildren(htree); res != 5 {
		t.Error("Unexpected number of children:", res)
		return
	}

	// Check that the entries exist

	entry, _ = htree.Get([]byte(PrefixAttrWord + "aaa" + "ddd"))
	pos = entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[2]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "aaa" + "voldaaa"))

	if entry != nil {
		t.Error("Unexpected result")
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "aaa" + "vnewaaa"))
	pos = entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[1]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "bbb" + "vbbb"))
	pos = entry.(*indexEntry).WordPos["123"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[1]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	// Delete from the index

	im.updateIndex("123", nil, obj1)

	if res := countChildren(htree); res != 0 {
		t.Error("Unexpected number of children:", res)
		return
	}
}

func countChildren(tree *hash.HTree) int {

	var count int

	it := hash.NewHTreeIterator(tree)

	for it.HasNext() {
		it.Next()
		count++
	}

	return count
}

func TestAddRemoveIndexHashEntry(t *testing.T) {
	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	oldsetting := CaseSensitiveWordIndex

	CaseSensitiveWordIndex = false

	im.addIndexHashEntry("mykey2", "myattr", "testvalue")
	im.addIndexHashEntry("mykey3", "myattr", "testvalue")

	sm.AccessMap[2] = storage.AccessCacheAndFetchError

	if err := im.addIndexHashEntry("mykey2", "myattr", "testvalue"); err.(*storage.StorageManagerError).Type != storage.ErrSlotNotFound {
		t.Error(err)
		return
	}
	if err := im.removeIndexHashEntry("mykey2", "myattr", "testvalue"); err.(*storage.StorageManagerError).Type != storage.ErrSlotNotFound {
		t.Error(err)
		return
	}

	if _, err := im.LookupValue("myattr", "testvalue"); !strings.Contains(err.Error(), "Slot not found") {
		t.Error(err)
		return
	}

	delete(sm.AccessMap, 2)

	// Test lookup

	res, _ := im.LookupValue("myattr", "testvalue")
	if fmt.Sprint(res) != "[mykey2 mykey3]" {
		t.Error("Unexpected lookup value result:", res)
		return
	}

	res, _ = im.LookupValue("myattr", "testvalue2")
	if fmt.Sprint(res) != "[]" {
		t.Error("Unexpected lookup value result:", res)
		return
	}

	im.removeIndexHashEntry("mykey2", "myattr", "testvalue")
	im.removeIndexHashEntry("mykey3", "myattr", "testvalue")

	if count := countChildren(htree); count != 0 {
		t.Error("Unexpected child count:", count)
		return
	}

	// Check handling of non-existent entries

	if res := im.removeIndexHashEntry("mykey4", "myattr", "testvalue"); res != nil {
		t.Error("Unexpected result:", res)
		return
	}

	// Test case sensitive case

	CaseSensitiveWordIndex = true

	im.addIndexHashEntry("mykey2", "myattr", "testValue")
	im.addIndexHashEntry("mykey3", "myattr", "testValue")

	if count := countChildren(htree); count != 1 {
		t.Error("Unexpected child count:", count)
		return
	}

	res, _ = im.LookupValue("myattr", "testvalue")
	if fmt.Sprint(res) != "[]" {
		t.Error("Unexpected lookup value result:", res)
		return
	}

	res, _ = im.LookupValue("myattr", "testValue")
	if fmt.Sprint(res) != "[mykey2 mykey3]" {
		t.Error("Unexpected lookup value result:", res)
		return
	}

	im.removeIndexHashEntry("mykey2", "myattr", "testvalue")
	im.removeIndexHashEntry("mykey3", "myattr", "testvalue")

	if count := countChildren(htree); count != 1 {
		t.Error("Unexpected child count:", count)
		return
	}

	im.removeIndexHashEntry("mykey2", "myattr", "testValue")
	im.removeIndexHashEntry("mykey3", "myattr", "testValue")

	if count := countChildren(htree); count != 0 {
		t.Error("Unexpected child count:", count)
		return
	}

	CaseSensitiveWordIndex = oldsetting
}

func TestAddRemoveIndexEntry(t *testing.T) {

	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	im.addIndexEntry("mykey", "myattr", "myword", []uint64{1, 5, 7})
	im.addIndexEntry("mykey2", "myattr", "myword", []uint64{10, 12, 80})

	entry, _ := htree.Get([]byte(PrefixAttrWord + "myattr" + "myword"))

	pos := entry.(*indexEntry).WordPos["mykey"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[1 5 7]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	pos = entry.(*indexEntry).WordPos["mykey2"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[10 12 80]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	testAddIndexPanic(t, im)

	sm.AccessMap[2] = storage.AccessCacheAndFetchError

	if res := im.addIndexEntry("mykey2", "myattr", "myword", []uint64{10, 12, 80}); res.(*storage.StorageManagerError).Type != storage.ErrSlotNotFound {
		t.Error("Unexpected result:", res)
		return
	}

	if res := im.removeIndexEntry("mykey2", "myattr", "myword", []uint64{10, 12, 80}); res.(*storage.StorageManagerError).Type != storage.ErrSlotNotFound {
		t.Error("Unexpected result:", res)
		return
	}

	delete(sm.AccessMap, 2)

	im.removeIndexEntry("mykey", "myattr", "myword", []uint64{1, 5, 7})

	entry, _ = htree.Get([]byte(PrefixAttrWord + "myattr" + "myword"))

	if res := len(entry.(*indexEntry).WordPos); res != 1 {
		t.Error("Unexpected length:", res)
		return
	}

	pos = entry.(*indexEntry).WordPos["mykey2"]

	if fmt.Sprint(bitutil.UnpackList(pos)) != "[10 12 80]" {
		t.Error("Unexpected result:", fmt.Sprint(bitutil.UnpackList(pos)))
		return
	}

	if im.removeIndexEntry("mykey3", "myattr", "myword", []uint64{10, 12, 80}) != nil {
		t.Error("Unexpected result")
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "myattr" + "myword"))

	if len(entry.(*indexEntry).WordPos) != 1 {
		t.Error("Unexpected length")
		return
	}

	if im.removeIndexEntry("mykey2", "myattr2", "myword", []uint64{10, 12, 80}) != nil {
		t.Error("Unexpected result")
		return
	}

	entry, _ = htree.Get([]byte(PrefixAttrWord + "myattr" + "myword"))

	if len(entry.(*indexEntry).WordPos) != 1 {
		t.Error("Unexpected length")
		return
	}

	if posres := fmt.Sprint(bitutil.UnpackList(entry.(*indexEntry).WordPos["mykey2"])); posres != "[10 12 80]" {
		t.Error("Unexpected pos list:", posres)
		return
	}

	// Test adding in non standard order

	if im.addIndexEntry("mykey2", "myattr", "myword", []uint64{45, 13}) != nil {
		t.Error("Unexpected result")
		return
	}

	if posres := fmt.Sprint(bitutil.UnpackList(entry.(*indexEntry).WordPos["mykey2"])); posres != "[10 12 13 45 80]" {
		t.Error("Unexpected pos list:", posres)
		return
	}

	// Test removal in non standard order

	if im.removeIndexEntry("mykey2", "myattr", "myword", []uint64{10, 80, 45, 13}) != nil {
		t.Error("Unexpected result")
		return
	}

	if posres := fmt.Sprint(bitutil.UnpackList(entry.(*indexEntry).WordPos["mykey2"])); posres != "[12]" {
		t.Error("Unexpected pos list:", posres)
		return
	}
}

func TestIndexManagerHashErrors(t *testing.T) {
	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	obj1 := make(map[string]string)
	obj1["aaa"] = "DDD voldaaa ddd"

	obj2 := make(map[string]string)
	obj2["aaa"] = "DDDe voldaaa ddd"

	im.Index("testkey", obj1)

	sm.AccessMap[4] = storage.AccessCacheAndFetchError
	if err := im.Index("testkey", obj1); err == nil {
		t.Error("Error expected")
		return
	}
	if err := im.Reindex("testkey", obj1, obj1); err == nil {
		t.Error("Error expected")
		return
	}
	if err := im.Deindex("testkey", obj1); err == nil {
		t.Error("Error expected")
		return
	}
	sm.AccessMap[5] = storage.AccessUpdateError
	if err := im.Reindex("testkey", obj1, obj2); err == nil {
		t.Error("Error expected")
		return
	}
	delete(sm.AccessMap, 4)
}

func testAddIndexPanic(t *testing.T, in *IndexManager) {
	defer func() {
		if r := recover(); r == nil {
			t.Error("Adding empty pos list did not cause a panic.")
		}
	}()

	in.addIndexEntry("", "", "", []uint64{})
}

func TestExtractWords(t *testing.T) {

	oldsetting := CaseSensitiveWordIndex

	CaseSensitiveWordIndex = false

	ws := extractWords("   aaa BBB  ;,   ccc...aaa ddd-bbb xxxx aaaa    test1\n" +
		"test2 xxxx bbb")

	if res := ws.String(); res != "WordSet:\n"+
		"    aaa [1 4]\n"+
		"    aaaa [8]\n"+
		"    bbb [2 6 12]\n"+
		"    ccc [3]\n"+
		"    ddd [5]\n"+
		"    test1 [9]\n"+
		"    test2 [10]\n"+
		"    xxxx [7 11]\n" {
		t.Error("Unexpected WordSet string result:", res)
	}

	CaseSensitiveWordIndex = true

	ws = extractWords("   aaa BBB     ccc aaa ddd bbb xxxx aaaa    test1\n" +
		"test2 xxxx bbb")

	if res := ws.String(); res != "WordSet:\n"+
		"    BBB [2]\n"+
		"    aaa [1 4]\n"+
		"    aaaa [8]\n"+
		"    bbb [6 12]\n"+
		"    ccc [3]\n"+
		"    ddd [5]\n"+
		"    test1 [9]\n"+
		"    test2 [10]\n"+
		"    xxxx [7 11]\n" {
		t.Error("Unexpected WordSet string result:", res)
	}

	CaseSensitiveWordIndex = oldsetting
}

func TestWordSet(t *testing.T) {
	ws := newWordSet(1)
	ws2 := newWordSet(1)
	ws3 := newWordSet(1)

	ws.Add("aaa", 1)
	ws.Add("bbb", 2)
	ws2.Add("bbb", 3)
	ws3.Add("ccc", 4)

	if !ws.Has("bbb") || ws.Has("ccc") {
		t.Error("Unexpected has result")
		return
	}

	if res := ws.String(); res != "WordSet:\n"+
		"    aaa [1]\n"+
		"    bbb [2]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	ws.AddAll(ws2)

	if res := ws2.String(); res != "WordSet:\n"+
		"    bbb [3]\n" {
		t.Error("Unexpected string result:", res)
		return
	}
	if res := ws.String(); res != "WordSet:\n"+
		"    aaa [1]\n"+
		"    bbb [2 3]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	ws.Add("bbb", 1)

	ws.AddAll(ws3)

	if res := ws.String(); res != "WordSet:\n"+
		"    aaa [1]\n"+
		"    bbb [1 2 3]\n"+
		"    ccc [4]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	ws.Remove("aaa", 1)
	ws.Remove("bbb", 2)
	ws.Remove("bbb", 1)

	if res := ws.String(); res != "WordSet:\n"+
		"    bbb [3]\n"+
		"    ccc [4]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	ws.Add("bbb", 4)
	ws.Add("bbb", 8)
	ws.Add("bbb", 10)

	ws4 := newWordSet(1)
	ws4.Add("bbb", 2)
	ws4.Add("bbb", 3)
	ws4.Add("bbb", 4)
	ws4.Add("bbb", 8)

	if res := ws.String(); res != "WordSet:\n"+
		"    bbb [3 4 8 10]\n"+
		"    ccc [4]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	ws.RemoveAll(ws4)

	if res := ws.String(); res != "WordSet:\n"+
		"    bbb [10]\n"+
		"    ccc [4]\n" {
		t.Error("Unexpected string result:", res)
		return
	}

	if res := fmt.Sprint(ws4.Pos("bbb")); res != "[2 3 4 8]" {
		t.Error("Unexpected pos result:", res)
		return
	}
	if res := ws4.Pos("abb"); res != nil {
		t.Error("Unexpected pos result:", res)
		return
	}

	// Test double entries

	ws.Add("ccc", 3)
	ws.Add("ccc", 5)
	ws.Add("ccc", 4)

	if res := ws.String(); res != "WordSet:\n"+
		"    bbb [10]\n"+
		"    ccc [3 4 5]\n" {
		t.Error("Unexpected string result:", res)
		return
	}
}

func TestRemoveDuplicates(t *testing.T) {

	if res := fmt.Sprint(removeDuplicates([]uint64{1, 2, 2, 3})); res != "[1 2 3]" {
		t.Error("Unexpected remove duplicates result:", res)
		return
	}

	if res := fmt.Sprint(removeDuplicates([]uint64{})); res != "[]" {
		t.Error("Unexpected remove duplicates result:", res)
		return
	}
}

func TestIndexManagerString(t *testing.T) {
	sm := storage.NewMemoryStorageManager("testsm")
	htree, _ := hash.NewHTree(sm)

	im := NewIndexManager(htree)

	obj1 := make(map[string]string)
	obj1["aaa"] = "bbb"

	im.Index("testkey", obj1)

	if res := im.String(); res != "IndexManager: 1\n"+
		"    1\"aaa\\b\\xf8\\xe0&\\fdA\\x85\\x10\\xce\\xfb+\\x06\\xee\\xe5\\xcd\" map[testkey:[]]\n"+
		"    1\"aaabbb\" map[testkey:[1]]\n" && res != "IndexManager: 1\n"+
		"    1\"aaabbb\" map[testkey:[1]]\n"+
		"    2\"aaa\\b\\xf8\\xe0&\\fdA\\x85\\x10\\xce\\xfb+\\x06\\xee\\xe5\\xcd\" map[testkey:[]]\n" {
		t.Error("Unexpected string output:", res)
		return
	}
}
