// Copyright 2023 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 (
	"runtime"
	"testing"
	"time"
	"unsafe"
)

type obj struct {
	x int64
	y int64
	z int64
}

type objWith[T any] struct {
	x int64
	y int64
	z int64
	o T
}

var (
	globalUintptr                uintptr
	globalPtrToObj               = &obj{}
	globalPtrToObjWithPtr        = &objWith[*uintptr]{}
	globalPtrToRuntimeObj        = func() *obj { return &obj{} }()
	globalPtrToRuntimeObjWithPtr = func() *objWith[*uintptr] { return &objWith[*uintptr]{} }()
)

func assertDidPanic(t *testing.T) {
	if recover() == nil {
		t.Fatal("did not panic")
	}
}

func assertCgoCheckPanics(t *testing.T, p any) {
	defer func() {
		if recover() == nil {
			t.Fatal("cgoCheckPointer() did not panic, make sure the tests run with cgocheck=1")
		}
	}()
	runtime.CgoCheckPointer(p, true)
}

func TestPinnerSimple(t *testing.T) {
	var pinner runtime.Pinner
	p := new(obj)
	addr := unsafe.Pointer(p)
	if runtime.IsPinned(addr) {
		t.Fatal("already marked as pinned")
	}
	pinner.Pin(p)
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	if runtime.GetPinCounter(addr) != nil {
		t.Fatal("pin counter should not exist")
	}
	pinner.Unpin()
	if runtime.IsPinned(addr) {
		t.Fatal("still marked as pinned")
	}
}

func TestPinnerPinKeepsAliveAndReleases(t *testing.T) {
	var pinner runtime.Pinner
	p := new(obj)
	done := make(chan struct{})
	runtime.SetFinalizer(p, func(any) {
		done <- struct{}{}
	})
	pinner.Pin(p)
	p = nil
	runtime.GC()
	runtime.GC()
	select {
	case <-done:
		t.Fatal("Pin() didn't keep object alive")
	case <-time.After(time.Millisecond * 10):
		break
	}
	pinner.Unpin()
	runtime.GC()
	runtime.GC()
	select {
	case <-done:
		break
	case <-time.After(time.Second):
		t.Fatal("Unpin() didn't release object")
	}
}

func TestPinnerMultiplePinsSame(t *testing.T) {
	const N = 100
	var pinner runtime.Pinner
	p := new(obj)
	addr := unsafe.Pointer(p)
	if runtime.IsPinned(addr) {
		t.Fatal("already marked as pinned")
	}
	for i := 0; i < N; i++ {
		pinner.Pin(p)
	}
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	if cnt := runtime.GetPinCounter(addr); cnt == nil || *cnt != N-1 {
		t.Fatalf("pin counter incorrect: %d", *cnt)
	}
	pinner.Unpin()
	if runtime.IsPinned(addr) {
		t.Fatal("still marked as pinned")
	}
	if runtime.GetPinCounter(addr) != nil {
		t.Fatal("pin counter was not deleted")
	}
}

func TestPinnerTwoPinner(t *testing.T) {
	var pinner1, pinner2 runtime.Pinner
	p := new(obj)
	addr := unsafe.Pointer(p)
	if runtime.IsPinned(addr) {
		t.Fatal("already marked as pinned")
	}
	pinner1.Pin(p)
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	if runtime.GetPinCounter(addr) != nil {
		t.Fatal("pin counter should not exist")
	}
	pinner2.Pin(p)
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	if cnt := runtime.GetPinCounter(addr); cnt == nil || *cnt != 1 {
		t.Fatalf("pin counter incorrect: %d", *cnt)
	}
	pinner1.Unpin()
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	if runtime.GetPinCounter(addr) != nil {
		t.Fatal("pin counter should not exist")
	}
	pinner2.Unpin()
	if runtime.IsPinned(addr) {
		t.Fatal("still marked as pinned")
	}
	if runtime.GetPinCounter(addr) != nil {
		t.Fatal("pin counter was not deleted")
	}
}

func TestPinnerPinZerosizeObj(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := new(struct{})
	pinner.Pin(p)
	if !runtime.IsPinned(unsafe.Pointer(p)) {
		t.Fatal("not marked as pinned")
	}
}

func TestPinnerPinGlobalPtr(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	pinner.Pin(globalPtrToObj)
	pinner.Pin(globalPtrToObjWithPtr)
	pinner.Pin(globalPtrToRuntimeObj)
	pinner.Pin(globalPtrToRuntimeObjWithPtr)
}

func TestPinnerPinTinyObj(t *testing.T) {
	var pinner runtime.Pinner
	const N = 64
	var addr [N]unsafe.Pointer
	for i := 0; i < N; i++ {
		p := new(bool)
		addr[i] = unsafe.Pointer(p)
		pinner.Pin(p)
		pinner.Pin(p)
		if !runtime.IsPinned(addr[i]) {
			t.Fatalf("not marked as pinned: %d", i)
		}
		if cnt := runtime.GetPinCounter(addr[i]); cnt == nil || *cnt == 0 {
			t.Fatalf("pin counter incorrect: %d, %d", *cnt, i)
		}
	}
	pinner.Unpin()
	for i := 0; i < N; i++ {
		if runtime.IsPinned(addr[i]) {
			t.Fatal("still marked as pinned")
		}
		if runtime.GetPinCounter(addr[i]) != nil {
			t.Fatal("pin counter should not exist")
		}
	}
}

func TestPinnerInterface(t *testing.T) {
	var pinner runtime.Pinner
	o := new(obj)
	ifc := any(o)
	pinner.Pin(&ifc)
	if !runtime.IsPinned(unsafe.Pointer(&ifc)) {
		t.Fatal("not marked as pinned")
	}
	if runtime.IsPinned(unsafe.Pointer(o)) {
		t.Fatal("marked as pinned")
	}
	pinner.Unpin()
	pinner.Pin(ifc)
	if !runtime.IsPinned(unsafe.Pointer(o)) {
		t.Fatal("not marked as pinned")
	}
	if runtime.IsPinned(unsafe.Pointer(&ifc)) {
		t.Fatal("marked as pinned")
	}
	pinner.Unpin()
}

func TestPinnerPinNonPtrPanics(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	var i int
	defer assertDidPanic(t)
	pinner.Pin(i)
}

func TestPinnerReuse(t *testing.T) {
	var pinner runtime.Pinner
	p := new(obj)
	p2 := &p
	assertCgoCheckPanics(t, p2)
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, true)
	pinner.Unpin()
	assertCgoCheckPanics(t, p2)
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, true)
	pinner.Unpin()
}

func TestPinnerEmptyUnpin(t *testing.T) {
	var pinner runtime.Pinner
	pinner.Unpin()
	pinner.Unpin()
}

func TestPinnerLeakPanics(t *testing.T) {
	old := runtime.GetPinnerLeakPanic()
	func() {
		defer assertDidPanic(t)
		old()
	}()
	done := make(chan struct{})
	runtime.SetPinnerLeakPanic(func() {
		done <- struct{}{}
	})
	func() {
		var pinner runtime.Pinner
		p := new(obj)
		pinner.Pin(p)
	}()
	runtime.GC()
	runtime.GC()
	select {
	case <-done:
		break
	case <-time.After(time.Second):
		t.Fatal("leak didn't make GC to panic")
	}
	runtime.SetPinnerLeakPanic(old)
}

func TestPinnerCgoCheckPtr2Ptr(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := new(obj)
	p2 := &objWith[*obj]{o: p}
	assertCgoCheckPanics(t, p2)
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, true)
}

func TestPinnerCgoCheckPtr2UnsafePtr(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := unsafe.Pointer(new(obj))
	p2 := &objWith[unsafe.Pointer]{o: p}
	assertCgoCheckPanics(t, p2)
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, true)
}

func TestPinnerCgoCheckPtr2UnknownPtr(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := unsafe.Pointer(new(obj))
	p2 := &p
	func() {
		defer assertDidPanic(t)
		runtime.CgoCheckPointer(p2, nil)
	}()
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, nil)
}

func TestPinnerCgoCheckInterface(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	var ifc any
	var o obj
	ifc = &o
	p := &ifc
	assertCgoCheckPanics(t, p)
	pinner.Pin(&o)
	runtime.CgoCheckPointer(p, true)
}

func TestPinnerCgoCheckSlice(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	sl := []int{1, 2, 3}
	assertCgoCheckPanics(t, &sl)
	pinner.Pin(&sl[0])
	runtime.CgoCheckPointer(&sl, true)
}

func TestPinnerCgoCheckString(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	b := []byte("foobar")
	str := unsafe.String(&b[0], 6)
	assertCgoCheckPanics(t, &str)
	pinner.Pin(&b[0])
	runtime.CgoCheckPointer(&str, true)
}

func TestPinnerCgoCheckPinned2UnpinnedPanics(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := new(obj)
	p2 := &objWith[*obj]{o: p}
	assertCgoCheckPanics(t, p2)
	pinner.Pin(p2)
	assertCgoCheckPanics(t, p2)
}

func TestPinnerCgoCheckPtr2Pinned2Unpinned(t *testing.T) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	p := new(obj)
	p2 := &objWith[*obj]{o: p}
	p3 := &objWith[*objWith[*obj]]{o: p2}
	assertCgoCheckPanics(t, p2)
	assertCgoCheckPanics(t, p3)
	pinner.Pin(p2)
	assertCgoCheckPanics(t, p2)
	assertCgoCheckPanics(t, p3)
	pinner.Pin(p)
	runtime.CgoCheckPointer(p2, true)
	runtime.CgoCheckPointer(p3, true)
}

func BenchmarkPinnerPinUnpinBatch(b *testing.B) {
	const Batch = 1000
	var data [Batch]*obj
	for i := 0; i < Batch; i++ {
		data[i] = new(obj)
	}
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		for i := 0; i < Batch; i++ {
			pinner.Pin(data[i])
		}
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpinBatchDouble(b *testing.B) {
	const Batch = 1000
	var data [Batch]*obj
	for i := 0; i < Batch; i++ {
		data[i] = new(obj)
	}
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		for i := 0; i < Batch; i++ {
			pinner.Pin(data[i])
			pinner.Pin(data[i])
		}
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpinBatchTiny(b *testing.B) {
	const Batch = 1000
	var data [Batch]*bool
	for i := 0; i < Batch; i++ {
		data[i] = new(bool)
	}
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		for i := 0; i < Batch; i++ {
			pinner.Pin(data[i])
		}
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpin(b *testing.B) {
	p := new(obj)
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		pinner.Pin(p)
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpinTiny(b *testing.B) {
	p := new(bool)
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		pinner.Pin(p)
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpinDouble(b *testing.B) {
	p := new(obj)
	for n := 0; n < b.N; n++ {
		var pinner runtime.Pinner
		pinner.Pin(p)
		pinner.Pin(p)
		pinner.Unpin()
	}
}

func BenchmarkPinnerPinUnpinParallel(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		p := new(obj)
		for pb.Next() {
			var pinner runtime.Pinner
			pinner.Pin(p)
			pinner.Unpin()
		}
	})
}

func BenchmarkPinnerPinUnpinParallelTiny(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		p := new(bool)
		for pb.Next() {
			var pinner runtime.Pinner
			pinner.Pin(p)
			pinner.Unpin()
		}
	})
}

func BenchmarkPinnerPinUnpinParallelDouble(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		p := new(obj)
		for pb.Next() {
			var pinner runtime.Pinner
			pinner.Pin(p)
			pinner.Pin(p)
			pinner.Unpin()
		}
	})
}

func BenchmarkPinnerIsPinnedOnPinned(b *testing.B) {
	var pinner runtime.Pinner
	ptr := new(obj)
	pinner.Pin(ptr)
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		runtime.IsPinned(unsafe.Pointer(ptr))
	}
	pinner.Unpin()
}

func BenchmarkPinnerIsPinnedOnUnpinned(b *testing.B) {
	ptr := new(obj)
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		runtime.IsPinned(unsafe.Pointer(ptr))
	}
}

func BenchmarkPinnerIsPinnedOnPinnedParallel(b *testing.B) {
	var pinner runtime.Pinner
	ptr := new(obj)
	pinner.Pin(ptr)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			runtime.IsPinned(unsafe.Pointer(ptr))
		}
	})
	pinner.Unpin()
}

func BenchmarkPinnerIsPinnedOnUnpinnedParallel(b *testing.B) {
	ptr := new(obj)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			runtime.IsPinned(unsafe.Pointer(ptr))
		}
	})
}

// const string data is not in span.
func TestPinnerConstStringData(t *testing.T) {
	var pinner runtime.Pinner
	str := "test-const-string"
	p := unsafe.StringData(str)
	addr := unsafe.Pointer(p)
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
	pinner.Pin(p)
	pinner.Unpin()
	if !runtime.IsPinned(addr) {
		t.Fatal("not marked as pinned")
	}
}
