package hashmap

import (
	"math/rand"
	"strconv"
	"testing"
	"time"

	"github.com/xiaq/persistent/hash"
)

const (
	NSequential = 0x1000
	NCollision  = 0x100
	NRandom     = 0x4000
	NReplace    = 0x200

	SmallRandomPass      = 0x100
	NSmallRandom         = 0x400
	SmallRandomHighBound = 0x50
	SmallRandomLowBound  = 0x200

	NArrayNode = 0x100

	NIneffectiveDissoc = 0x200

	N1 = nodeCap + 1
	N2 = nodeCap*nodeCap + 1
	N3 = nodeCap*nodeCap*nodeCap + 1
)

type testKey uint64
type anotherTestKey uint32

func equalFunc(k1, k2 interface{}) bool {
	switch k1 := k1.(type) {
	case testKey:
		t2, ok := k2.(testKey)
		return ok && k1 == t2
	case anotherTestKey:
		return false
	default:
		return k1 == k2
	}
}

func hashFunc(k interface{}) uint32 {
	switch k := k.(type) {
	case uint32:
		return k
	case string:
		return hash.String(k)
	case testKey:
		// Return the lower 32 bits for testKey. This is intended so that hash
		// collisions can be easily constructed.
		return uint32(k & 0xffffffff)
	case anotherTestKey:
		return uint32(k)
	default:
		return 0
	}
}

var empty = New(equalFunc, hashFunc)

type refEntry struct {
	k testKey
	v string
}

func hex(i uint64) string {
	return "0x" + strconv.FormatUint(i, 16)
}

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

var randomStrings []string

// getRandomStrings returns a slice of N3 random strings. It builds the slice
// once and caches it. If the slice is built for the first time, it stops the
// timer of the benchmark.
func getRandomStrings(b *testing.B) []string {
	if randomStrings == nil {
		b.StopTimer()
		defer b.StartTimer()
		randomStrings = make([]string, N3)
		for i := 0; i < N3; i++ {
			randomStrings[i] = makeRandomString()
		}
	}
	return randomStrings
}

// makeRandomString builds a random string consisting of n bytes (randomized
// between 0 and 99) and each byte is randomized between 0 and 255. The string
// need not be valid UTF-8.
func makeRandomString() string {
	bytes := make([]byte, rand.Intn(100))
	for i := range bytes {
		bytes[i] = byte(rand.Intn(256))
	}
	return string(bytes)
}

func TestHashMap(t *testing.T) {
	var refEntries []refEntry
	add := func(k testKey, v string) {
		refEntries = append(refEntries, refEntry{k, v})
	}

	for i := 0; i < NSequential; i++ {
		add(testKey(i), hex(uint64(i)))
	}
	for i := 0; i < NCollision; i++ {
		add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i)))
	}
	for i := 0; i < NRandom; i++ {
		// Avoid rand.Uint64 for compatibility with pre 1.8 Go
		k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32
		add(testKey(k), "random "+hex(k))
	}
	for i := 0; i < NReplace; i++ {
		k := uint64(rand.Int31n(NSequential))
		add(testKey(k), "replace "+hex(k))
	}

	testHashMapWithRefEntries(t, refEntries)
}

func TestHashMapSmallRandom(t *testing.T) {
	for p := 0; p < SmallRandomPass; p++ {
		var refEntries []refEntry
		add := func(k testKey, v string) {
			refEntries = append(refEntries, refEntry{k, v})
		}

		for i := 0; i < NSmallRandom; i++ {
			k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 |
				uint64(rand.Int31n(SmallRandomLowBound)))
			add(testKey(k), "random "+hex(k))
		}

		testHashMapWithRefEntries(t, refEntries)
	}
}

var marshalJSONTests = []struct {
	in      Map
	wantOut string
	wantErr bool
}{
	{makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false},
	// Invalid key type
	{makeHashMap([]interface{}{}, "x"), "", true},
}

func TestMarshalJSON(t *testing.T) {
	for i, test := range marshalJSONTests {
		out, err := test.in.MarshalJSON()
		if string(out) != test.wantOut {
			t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut)
		}
		if (err != nil) != test.wantErr {
			var wantErr string
			if test.wantErr {
				wantErr = "non-nil"
			} else {
				wantErr = "nil"
			}
			t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr)
		}
	}
}

func makeHashMap(data ...interface{}) Map {
	m := empty
	for i := 0; i+1 < len(data); i += 2 {
		k, v := data[i], data[i+1]
		m = m.Assoc(k, v)
	}
	return m
}

// testHashMapWithRefEntries tests the operations of a Map. It uses the supplied
// list of entries to build the map, and then test all its operations.
func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) {
	m := empty
	// Len of Empty should be 0.
	if m.Len() != 0 {
		t.Errorf("m.Len = %d, want %d", m.Len(), 0)
	}

	// Assoc and Len, test by building a map simutaneously.
	ref := make(map[testKey]string, len(refEntries))
	for _, e := range refEntries {
		ref[e.k] = e.v
		m = m.Assoc(e.k, e.v)
		if m.Len() != len(ref) {
			t.Errorf("m.Len = %d, want %d", m.Len(), len(ref))
		}
	}

	// Index.
	testMapContent(t, m, ref)
	got, in := m.Index(anotherTestKey(0))
	if in {
		t.Errorf("m.Index <bad key> returns entry %v", got)
	}
	// Iterator.
	testIterator(t, m, ref)

	// Dissoc.
	// Ineffective ones.
	for i := 0; i < NIneffectiveDissoc; i++ {
		k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16)
		m = m.Dissoc(k)
		if m.Len() != len(ref) {
			t.Errorf("m.Dissoc removes item when it shouldn't")
		}
	}

	// Effective ones.
	for i := len(refEntries) - 1; i >= 0; i-- {
		k := refEntries[i].k
		delete(ref, k)
		m = m.Dissoc(k)
		if m.Len() != len(ref) {
			t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref))
		}
		_, in := m.Index(k)
		if in {
			t.Errorf("m.Index(%v) still returns item after removal", k)
		}
		// Checking all elements is expensive. Only do this 1% of the time.
		if rand.Float64() < 0.01 {
			testMapContent(t, m, ref)
		}
	}
}

func testMapContent(t *testing.T, m Map, ref map[testKey]string) {
	for k, v := range ref {
		got, in := m.Index(k)
		if !in {
			t.Errorf("m.Index 0x%x returns no entry", k)
		}
		if got != v {
			t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v)
		}
	}
}

func testIterator(t *testing.T, m Map, ref map[testKey]string) {
	ref2 := map[interface{}]interface{}{}
	for k, v := range ref {
		ref2[k] = v
	}
	for it := m.Iterator(); it.HasElem(); it.Next() {
		k, v := it.Elem()
		if ref2[k] != v {
			t.Errorf("iterator yields unexpected pair %v, %v", k, v)
		}
		delete(ref2, k)
	}
	if len(ref2) != 0 {
		t.Errorf("iterating was not exhaustive")
	}
}

func BenchmarkSequentialConsNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) }
func BenchmarkSequentialConsNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) }
func BenchmarkSequentialConsNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) }

// nativeSequntialAdd starts with an empty native map and adds elements 0...n-1
// to the map, using the same value as the key, repeating for N times.
func nativeSequentialAdd(N int, n uint32) {
	for r := 0; r < N; r++ {
		m := make(map[uint32]uint32)
		for i := uint32(0); i < n; i++ {
			m[i] = i
		}
	}
}

func BenchmarkSequentialConsPersistent1(b *testing.B) { sequentialCons(b.N, N1) }
func BenchmarkSequentialConsPersistent2(b *testing.B) { sequentialCons(b.N, N2) }
func BenchmarkSequentialConsPersistent3(b *testing.B) { sequentialCons(b.N, N3) }

// sequentialCons starts with an empty hash map and adds elements 0...n-1 to the
// map, using the same value as the key, repeating for N times.
func sequentialCons(N int, n uint32) {
	for r := 0; r < N; r++ {
		m := empty
		for i := uint32(0); i < n; i++ {
			m = m.Assoc(i, i)
		}
	}
}

func BenchmarkRandomStringsConsNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) }
func BenchmarkRandomStringsConsNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) }
func BenchmarkRandomStringsConsNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) }

// nativeSequntialAdd starts with an empty native map and adds n random strings
// to the map, using the same value as the key, repeating for b.N times.
func nativeRandomStringsAdd(b *testing.B, n int) {
	ss := getRandomStrings(b)
	for r := 0; r < b.N; r++ {
		m := make(map[string]string)
		for i := 0; i < n; i++ {
			s := ss[i]
			m[s] = s
		}
	}
}

func BenchmarkRandomStringsConsPersistent1(b *testing.B) { randomStringsCons(b, N1) }
func BenchmarkRandomStringsConsPersistent2(b *testing.B) { randomStringsCons(b, N2) }
func BenchmarkRandomStringsConsPersistent3(b *testing.B) { randomStringsCons(b, N3) }

func randomStringsCons(b *testing.B, n int) {
	ss := getRandomStrings(b)
	for r := 0; r < b.N; r++ {
		m := empty
		for i := 0; i < n; i++ {
			s := ss[i]
			m = m.Assoc(s, s)
		}
	}
}
