// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package runtime_test

import (
	"fmt"
	"math/rand"
	. "runtime"
	"testing"
)

// Ensures that got and want are the same, and if not, reports
// detailed diff information.
func checkPallocBits(t *testing.T, got, want *PallocBits) bool {
	d := DiffPallocBits(got, want)
	if len(d) != 0 {
		t.Errorf("%d range(s) different", len(d))
		for _, bits := range d {
			t.Logf("\t@ bit index %d", bits.I)
			t.Logf("\t|  got: %s", StringifyPallocBits(got, bits))
			t.Logf("\t| want: %s", StringifyPallocBits(want, bits))
		}
		return false
	}
	return true
}

// makePallocBits produces an initialized PallocBits by setting
// the ranges in s to 1 and the rest to zero.
func makePallocBits(s []BitRange) *PallocBits {
	b := new(PallocBits)
	for _, v := range s {
		b.AllocRange(v.I, v.N)
	}
	return b
}

// Ensures that PallocBits.AllocRange works, which is a fundamental
// method used for testing and initialization since it's used by
// makePallocBits.
func TestPallocBitsAllocRange(t *testing.T) {
	test := func(t *testing.T, i, n uint, want *PallocBits) {
		checkPallocBits(t, makePallocBits([]BitRange{{i, n}}), want)
	}
	t.Run("OneLow", func(t *testing.T) {
		want := new(PallocBits)
		want[0] = 0x1
		test(t, 0, 1, want)
	})
	t.Run("OneHigh", func(t *testing.T) {
		want := new(PallocBits)
		want[PallocChunkPages/64-1] = 1 << 63
		test(t, PallocChunkPages-1, 1, want)
	})
	t.Run("Inner", func(t *testing.T) {
		want := new(PallocBits)
		want[2] = 0x3e
		test(t, 129, 5, want)
	})
	t.Run("Aligned", func(t *testing.T) {
		want := new(PallocBits)
		want[2] = ^uint64(0)
		want[3] = ^uint64(0)
		test(t, 128, 128, want)
	})
	t.Run("Begin", func(t *testing.T) {
		want := new(PallocBits)
		want[0] = ^uint64(0)
		want[1] = ^uint64(0)
		want[2] = ^uint64(0)
		want[3] = ^uint64(0)
		want[4] = ^uint64(0)
		want[5] = 0x1
		test(t, 0, 321, want)
	})
	t.Run("End", func(t *testing.T) {
		want := new(PallocBits)
		want[PallocChunkPages/64-1] = ^uint64(0)
		want[PallocChunkPages/64-2] = ^uint64(0)
		want[PallocChunkPages/64-3] = ^uint64(0)
		want[PallocChunkPages/64-4] = 1 << 63
		test(t, PallocChunkPages-(64*3+1), 64*3+1, want)
	})
	t.Run("All", func(t *testing.T) {
		want := new(PallocBits)
		for i := range want {
			want[i] = ^uint64(0)
		}
		test(t, 0, PallocChunkPages, want)
	})
}

// Inverts every bit in the PallocBits.
func invertPallocBits(b *PallocBits) {
	for i := range b {
		b[i] = ^b[i]
	}
}

// Ensures two packed summaries are identical, and reports a detailed description
// of the difference if they're not.
func checkPallocSum(t testing.TB, got, want PallocSum) {
	if got.Start() != want.Start() {
		t.Errorf("inconsistent start: got %d, want %d", got.Start(), want.Start())
	}
	if got.Max() != want.Max() {
		t.Errorf("inconsistent max: got %d, want %d", got.Max(), want.Max())
	}
	if got.End() != want.End() {
		t.Errorf("inconsistent end: got %d, want %d", got.End(), want.End())
	}
}

func TestMallocBitsPopcntRange(t *testing.T) {
	type test struct {
		i, n uint // bit range to popcnt over.
		want uint // expected popcnt result on that range.
	}
	tests := map[string]struct {
		init  []BitRange // bit ranges to set to 1 in the bitmap.
		tests []test     // a set of popcnt tests to run over the bitmap.
	}{
		"None": {
			tests: []test{
				{0, 1, 0},
				{5, 3, 0},
				{2, 11, 0},
				{PallocChunkPages/4 + 1, PallocChunkPages / 2, 0},
				{0, PallocChunkPages, 0},
			},
		},
		"All": {
			init: []BitRange{{0, PallocChunkPages}},
			tests: []test{
				{0, 1, 1},
				{5, 3, 3},
				{2, 11, 11},
				{PallocChunkPages/4 + 1, PallocChunkPages / 2, PallocChunkPages / 2},
				{0, PallocChunkPages, PallocChunkPages},
			},
		},
		"Half": {
			init: []BitRange{{PallocChunkPages / 2, PallocChunkPages / 2}},
			tests: []test{
				{0, 1, 0},
				{5, 3, 0},
				{2, 11, 0},
				{PallocChunkPages/2 - 1, 1, 0},
				{PallocChunkPages / 2, 1, 1},
				{PallocChunkPages/2 + 10, 1, 1},
				{PallocChunkPages/2 - 1, 2, 1},
				{PallocChunkPages / 4, PallocChunkPages / 4, 0},
				{PallocChunkPages / 4, PallocChunkPages/4 + 1, 1},
				{PallocChunkPages/4 + 1, PallocChunkPages / 2, PallocChunkPages/4 + 1},
				{0, PallocChunkPages, PallocChunkPages / 2},
			},
		},
		"OddBound": {
			init: []BitRange{{0, 111}},
			tests: []test{
				{0, 1, 1},
				{5, 3, 3},
				{2, 11, 11},
				{110, 2, 1},
				{99, 50, 12},
				{110, 1, 1},
				{111, 1, 0},
				{99, 1, 1},
				{120, 1, 0},
				{PallocChunkPages / 2, PallocChunkPages / 2, 0},
				{0, PallocChunkPages, 111},
			},
		},
		"Scattered": {
			init: []BitRange{
				{1, 3}, {5, 1}, {7, 1}, {10, 2}, {13, 1}, {15, 4},
				{21, 1}, {23, 1}, {26, 2}, {30, 5}, {36, 2}, {40, 3},
				{44, 6}, {51, 1}, {53, 2}, {58, 3}, {63, 1}, {67, 2},
				{71, 10}, {84, 1}, {89, 7}, {99, 2}, {103, 1}, {107, 2},
				{111, 1}, {113, 1}, {115, 1}, {118, 1}, {120, 2}, {125, 5},
			},
			tests: []test{
				{0, 11, 6},
				{0, 64, 39},
				{13, 64, 40},
				{64, 64, 34},
				{0, 128, 73},
				{1, 128, 74},
				{0, PallocChunkPages, 75},
			},
		},
	}
	for name, v := range tests {
		v := v
		t.Run(name, func(t *testing.T) {
			b := makePallocBits(v.init)
			for _, h := range v.tests {
				if got := b.PopcntRange(h.i, h.n); got != h.want {
					t.Errorf("bad popcnt (i=%d, n=%d): got %d, want %d", h.i, h.n, got, h.want)
				}
			}
		})
	}
}

// Ensures computing bit summaries works as expected by generating random
// bitmaps and checking against a reference implementation.
func TestPallocBitsSummarizeRandom(t *testing.T) {
	b := new(PallocBits)
	for i := 0; i < 1000; i++ {
		// Randomize bitmap.
		for i := range b {
			b[i] = rand.Uint64()
		}
		// Check summary against reference implementation.
		checkPallocSum(t, b.Summarize(), SummarizeSlow(b))
	}
}

// Ensures computing bit summaries works as expected.
func TestPallocBitsSummarize(t *testing.T) {
	var emptySum = PackPallocSum(PallocChunkPages, PallocChunkPages, PallocChunkPages)
	type test struct {
		free []BitRange // Ranges of free (zero) bits.
		hits []PallocSum
	}
	tests := make(map[string]test)
	tests["NoneFree"] = test{
		free: []BitRange{},
		hits: []PallocSum{
			PackPallocSum(0, 0, 0),
		},
	}
	tests["OnlyStart"] = test{
		free: []BitRange{{0, 10}},
		hits: []PallocSum{
			PackPallocSum(10, 10, 0),
		},
	}
	tests["OnlyEnd"] = test{
		free: []BitRange{{PallocChunkPages - 40, 40}},
		hits: []PallocSum{
			PackPallocSum(0, 40, 40),
		},
	}
	tests["StartAndEnd"] = test{
		free: []BitRange{{0, 11}, {PallocChunkPages - 23, 23}},
		hits: []PallocSum{
			PackPallocSum(11, 23, 23),
		},
	}
	tests["StartMaxEnd"] = test{
		free: []BitRange{{0, 4}, {50, 100}, {PallocChunkPages - 4, 4}},
		hits: []PallocSum{
			PackPallocSum(4, 100, 4),
		},
	}
	tests["OnlyMax"] = test{
		free: []BitRange{{1, 20}, {35, 241}, {PallocChunkPages - 50, 30}},
		hits: []PallocSum{
			PackPallocSum(0, 241, 0),
		},
	}
	tests["MultiMax"] = test{
		free: []BitRange{{35, 2}, {40, 5}, {100, 5}},
		hits: []PallocSum{
			PackPallocSum(0, 5, 0),
		},
	}
	tests["One"] = test{
		free: []BitRange{{2, 1}},
		hits: []PallocSum{
			PackPallocSum(0, 1, 0),
		},
	}
	tests["AllFree"] = test{
		free: []BitRange{{0, PallocChunkPages}},
		hits: []PallocSum{
			emptySum,
		},
	}
	for name, v := range tests {
		v := v
		t.Run(name, func(t *testing.T) {
			b := makePallocBits(v.free)
			// In the PallocBits we create 1's represent free spots, but in our actual
			// PallocBits 1 means not free, so invert.
			invertPallocBits(b)
			for _, h := range v.hits {
				checkPallocSum(t, b.Summarize(), h)
			}
		})
	}
}

// Benchmarks how quickly we can summarize a PallocBits.
func BenchmarkPallocBitsSummarize(b *testing.B) {
	patterns := []uint64{
		0,
		^uint64(0),
		0xaa,
		0xaaaaaaaaaaaaaaaa,
		0x80000000aaaaaaaa,
		0xaaaaaaaa00000001,
		0xbbbbbbbbbbbbbbbb,
		0x80000000bbbbbbbb,
		0xbbbbbbbb00000001,
		0xcccccccccccccccc,
		0x4444444444444444,
		0x4040404040404040,
		0x4000400040004000,
		0x1000404044ccaaff,
	}
	for _, p := range patterns {
		buf := new(PallocBits)
		for i := 0; i < len(buf); i++ {
			buf[i] = p
		}
		b.Run(fmt.Sprintf("Unpacked%02X", p), func(b *testing.B) {
			checkPallocSum(b, buf.Summarize(), SummarizeSlow(buf))
			for i := 0; i < b.N; i++ {
				buf.Summarize()
			}
		})
	}
}

// Ensures page allocation works.
func TestPallocBitsAlloc(t *testing.T) {
	tests := map[string]struct {
		before []BitRange
		after  []BitRange
		npages uintptr
		hits   []uint
	}{
		"AllFree1": {
			npages: 1,
			hits:   []uint{0, 1, 2, 3, 4, 5},
			after:  []BitRange{{0, 6}},
		},
		"AllFree2": {
			npages: 2,
			hits:   []uint{0, 2, 4, 6, 8, 10},
			after:  []BitRange{{0, 12}},
		},
		"AllFree5": {
			npages: 5,
			hits:   []uint{0, 5, 10, 15, 20},
			after:  []BitRange{{0, 25}},
		},
		"AllFree64": {
			npages: 64,
			hits:   []uint{0, 64, 128},
			after:  []BitRange{{0, 192}},
		},
		"AllFree65": {
			npages: 65,
			hits:   []uint{0, 65, 130},
			after:  []BitRange{{0, 195}},
		},
		"SomeFree64": {
			before: []BitRange{{0, 32}, {64, 32}, {100, PallocChunkPages - 100}},
			npages: 64,
			hits:   []uint{^uint(0)},
			after:  []BitRange{{0, 32}, {64, 32}, {100, PallocChunkPages - 100}},
		},
		"NoneFree1": {
			before: []BitRange{{0, PallocChunkPages}},
			npages: 1,
			hits:   []uint{^uint(0), ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"NoneFree2": {
			before: []BitRange{{0, PallocChunkPages}},
			npages: 2,
			hits:   []uint{^uint(0), ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"NoneFree5": {
			before: []BitRange{{0, PallocChunkPages}},
			npages: 5,
			hits:   []uint{^uint(0), ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"NoneFree65": {
			before: []BitRange{{0, PallocChunkPages}},
			npages: 65,
			hits:   []uint{^uint(0), ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"ExactFit1": {
			before: []BitRange{{0, PallocChunkPages/2 - 3}, {PallocChunkPages/2 - 2, PallocChunkPages/2 + 2}},
			npages: 1,
			hits:   []uint{PallocChunkPages/2 - 3, ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"ExactFit2": {
			before: []BitRange{{0, PallocChunkPages/2 - 3}, {PallocChunkPages/2 - 1, PallocChunkPages/2 + 1}},
			npages: 2,
			hits:   []uint{PallocChunkPages/2 - 3, ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"ExactFit5": {
			before: []BitRange{{0, PallocChunkPages/2 - 3}, {PallocChunkPages/2 + 2, PallocChunkPages/2 - 2}},
			npages: 5,
			hits:   []uint{PallocChunkPages/2 - 3, ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"ExactFit65": {
			before: []BitRange{{0, PallocChunkPages/2 - 31}, {PallocChunkPages/2 + 34, PallocChunkPages/2 - 34}},
			npages: 65,
			hits:   []uint{PallocChunkPages/2 - 31, ^uint(0)},
			after:  []BitRange{{0, PallocChunkPages}},
		},
		"SomeFree161": {
			before: []BitRange{{0, 185}, {331, 1}},
			npages: 161,
			hits:   []uint{332},
			after:  []BitRange{{0, 185}, {331, 162}},
		},
	}
	for name, v := range tests {
		v := v
		t.Run(name, func(t *testing.T) {
			b := makePallocBits(v.before)
			for iter, i := range v.hits {
				a, _ := b.Find(v.npages, 0)
				if i != a {
					t.Fatalf("find #%d picked wrong index: want %d, got %d", iter+1, i, a)
				}
				if i != ^uint(0) {
					b.AllocRange(a, uint(v.npages))
				}
			}
			want := makePallocBits(v.after)
			checkPallocBits(t, b, want)
		})
	}
}

// Ensures page freeing works.
func TestPallocBitsFree(t *testing.T) {
	tests := map[string]struct {
		beforeInv []BitRange
		afterInv  []BitRange
		frees     []uint
		npages    uintptr
	}{
		"SomeFree": {
			npages:    1,
			beforeInv: []BitRange{{0, 32}, {64, 32}, {100, 1}},
			frees:     []uint{32},
			afterInv:  []BitRange{{0, 33}, {64, 32}, {100, 1}},
		},
		"NoneFree1": {
			npages:   1,
			frees:    []uint{0, 1, 2, 3, 4, 5},
			afterInv: []BitRange{{0, 6}},
		},
		"NoneFree2": {
			npages:   2,
			frees:    []uint{0, 2, 4, 6, 8, 10},
			afterInv: []BitRange{{0, 12}},
		},
		"NoneFree5": {
			npages:   5,
			frees:    []uint{0, 5, 10, 15, 20},
			afterInv: []BitRange{{0, 25}},
		},
		"NoneFree64": {
			npages:   64,
			frees:    []uint{0, 64, 128},
			afterInv: []BitRange{{0, 192}},
		},
		"NoneFree65": {
			npages:   65,
			frees:    []uint{0, 65, 130},
			afterInv: []BitRange{{0, 195}},
		},
	}
	for name, v := range tests {
		v := v
		t.Run(name, func(t *testing.T) {
			b := makePallocBits(v.beforeInv)
			invertPallocBits(b)
			for _, i := range v.frees {
				b.Free(i, uint(v.npages))
			}
			want := makePallocBits(v.afterInv)
			invertPallocBits(want)
			checkPallocBits(t, b, want)
		})
	}
}

func TestFindBitRange64(t *testing.T) {
	check := func(x uint64, n uint, result uint) {
		i := FindBitRange64(x, n)
		if result == ^uint(0) && i < 64 {
			t.Errorf("case (%016x, %d): got %d, want failure", x, n, i)
		} else if result != ^uint(0) && i != result {
			t.Errorf("case (%016x, %d): got %d, want %d", x, n, i, result)
		}
	}
	for i := uint(1); i <= 64; i++ {
		check(^uint64(0), i, 0)
	}
	for i := uint(1); i <= 64; i++ {
		check(0, i, ^uint(0))
	}
	check(0x8000000000000000, 1, 63)
	check(0xc000010001010000, 2, 62)
	check(0xc000010001030000, 2, 16)
	check(0xe000030001030000, 3, 61)
	check(0xe000030001070000, 3, 16)
	check(0xffff03ff01070000, 16, 48)
	check(0xffff03ff0107ffff, 16, 0)
	check(0x0fff03ff01079fff, 16, ^uint(0))
}

func BenchmarkFindBitRange64(b *testing.B) {
	patterns := []uint64{
		0,
		^uint64(0),
		0xaa,
		0xaaaaaaaaaaaaaaaa,
		0x80000000aaaaaaaa,
		0xaaaaaaaa00000001,
		0xbbbbbbbbbbbbbbbb,
		0x80000000bbbbbbbb,
		0xbbbbbbbb00000001,
		0xcccccccccccccccc,
		0x4444444444444444,
		0x4040404040404040,
		0x4000400040004000,
	}
	sizes := []uint{
		2, 8, 32,
	}
	for _, pattern := range patterns {
		for _, size := range sizes {
			b.Run(fmt.Sprintf("Pattern%02XSize%d", pattern, size), func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					FindBitRange64(pattern, size)
				}
			})
		}
	}
}
