package index

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

	"github.com/restic/restic/internal/restic"
	rtest "github.com/restic/restic/internal/test"
)

func TestIndexMapBasic(t *testing.T) {
	t.Parallel()

	var (
		id restic.ID
		m  indexMap
		r  = rand.New(rand.NewSource(98765))
	)

	for i := 1; i <= 400; i++ {
		r.Read(id[:])
		rtest.Assert(t, m.get(id) == nil, "%v retrieved but not added", id)

		m.add(id, 0, 0, 0, 0)
		rtest.Assert(t, m.get(id) != nil, "%v added but not retrieved", id)
		rtest.Equals(t, uint(i), m.len())
	}
}

func TestIndexMapForeach(t *testing.T) {
	t.Parallel()

	const N = 10

	var m indexMap

	// Don't crash on empty map.
	//nolint:revive // ignore empty iteration
	for range m.values() {
		// empty iteration
	}

	for i := 0; i < N; i++ {
		var id restic.ID
		id[0] = byte(i)
		m.add(id, i, uint32(i), uint32(i), uint32(i/2))
	}

	seen := make(map[int]struct{})
	for e := range m.values() {
		i := int(e.id[0])
		rtest.Assert(t, i < N, "unknown id %v in indexMap", e.id)
		rtest.Equals(t, i, e.packIndex)
		rtest.Equals(t, i, int(e.length))
		rtest.Equals(t, i, int(e.offset))
		rtest.Equals(t, i/2, int(e.uncompressedLength))

		seen[i] = struct{}{}
	}

	rtest.Equals(t, N, len(seen))

	ncalls := 0
	for range m.values() {
		ncalls++
		break
	}
	rtest.Equals(t, 1, ncalls)
}

func TestIndexMapForeachWithID(t *testing.T) {
	t.Parallel()

	const ndups = 3

	var (
		id restic.ID
		m  indexMap
		r  = rand.New(rand.NewSource(1234321))
	)
	r.Read(id[:])

	// No result (and no crash) for empty map.
	n := 0
	for range m.valuesWithID(id) {
		n++
	}
	rtest.Equals(t, 0, n)

	// Test insertion and retrieval of duplicates.
	for i := 0; i < ndups; i++ {
		m.add(id, i, 0, 0, 0)
	}

	for i := 0; i < 100; i++ {
		var otherid restic.ID
		r.Read(otherid[:])
		m.add(otherid, -1, 0, 0, 0)
	}

	n = 0
	var packs [ndups]bool
	for e := range m.valuesWithID(id) {
		packs[e.packIndex] = true
		n++
	}
	rtest.Equals(t, ndups, n)

	for i := range packs {
		rtest.Assert(t, packs[i], "duplicate from pack %d not retrieved", i)
	}
}

func TestHashedArrayTree(t *testing.T) {
	hat := newHAT()
	const testSize = 1024
	for i := uint(0); i < testSize; i++ {
		rtest.Assert(t, hat.Size() == i, "expected hat size %v got %v", i, hat.Size())
		e, idx := hat.Alloc()
		rtest.Assert(t, idx == i, "expected entry at idx %v got %v", i, idx)
		e.length = uint32(i)
	}
	for i := uint(0); i < testSize; i++ {
		e := hat.Ref(i)
		rtest.Assert(t, e.length == uint32(i), "expected entry to contain %v got %v", uint32(i), e.length)
	}
}

func BenchmarkIndexMapHash(b *testing.B) {
	var m indexMap
	m.add(restic.ID{}, 0, 0, 0, 0) // Trigger lazy initialization.

	ids := make([]restic.ID, 128) // 4 KiB.
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := range ids {
		r.Read(ids[i][:])
	}

	b.ReportAllocs()
	b.SetBytes(int64(len(restic.ID{}) * len(ids)))
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		for _, id := range ids {
			m.hash(id)
		}
	}
}

func TestIndexMapFirstIndex(t *testing.T) {
	t.Parallel()

	var (
		id restic.ID
		m  indexMap
		r  = rand.New(rand.NewSource(98765))
		fi = make(map[restic.ID]int)
	)

	for i := 1; i <= 400; i++ {
		r.Read(id[:])
		rtest.Equals(t, -1, m.firstIndex(id), "wrong firstIndex for nonexistent id")

		m.add(id, 0, 0, 0, 0)
		idx := m.firstIndex(id)
		rtest.Equals(t, i, idx, "unexpected index for id")
		fi[id] = idx
	}
	// iterate over blobs, as this is a hashmap the order is effectively random
	for id, idx := range fi {
		rtest.Equals(t, idx, m.firstIndex(id), "wrong index returned")
	}
}

func TestIndexMapFirstIndexDuplicates(t *testing.T) {
	t.Parallel()

	var (
		id restic.ID
		m  indexMap
		r  = rand.New(rand.NewSource(98765))
	)

	r.Read(id[:])
	for i := 1; i <= 10; i++ {
		m.add(id, 0, 0, 0, 0)
	}
	idx := m.firstIndex(id)
	rtest.Equals(t, 1, idx, "unexpected index for id")
}
