//go:build nocover

package swarm

import (
	"context"
	"crypto/rand"
	"net"
	"syscall"
	"testing"
	"time"

	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/network"
	ma "github.com/multiformats/go-multiaddr"

	mrand "math/rand"

	"github.com/stretchr/testify/require"
)

func BenchmarkMetricsConnOpen(b *testing.B) {
	b.ReportAllocs()
	quicConnState := network.ConnectionState{Transport: "quic"}
	tcpConnState := network.ConnectionState{
		StreamMultiplexer: "yamux",
		Security:          "tls",
		Transport:         "tcp",
	}
	_, pub, err := crypto.GenerateEd25519Key(rand.Reader)
	require.NoError(b, err)
	quicAddr := ma.StringCast("/ip4/1.2.3.4/udp/1/quic")
	tcpAddr := ma.StringCast("/ip4/1.2.3.4/tcp/1/")
	tr := NewMetricsTracer()
	for i := 0; i < b.N; i++ {
		switch i % 2 {
		case 0:
			tr.OpenedConnection(network.DirInbound, pub, quicConnState, quicAddr)
		case 1:
			tr.OpenedConnection(network.DirInbound, pub, tcpConnState, tcpAddr)
		}
	}
}

func randItem[T any](items []T) T {
	return items[mrand.Intn(len(items))]
}

func TestMetricsNoAllocNoCover(t *testing.T) {
	mt := NewMetricsTracer()

	connections := []network.ConnectionState{
		{StreamMultiplexer: "yamux", Security: "tls", Transport: "tcp", UsedEarlyMuxerNegotiation: true},
		{StreamMultiplexer: "yamux", Security: "noise", Transport: "tcp", UsedEarlyMuxerNegotiation: false},
		{StreamMultiplexer: "", Security: "", Transport: "quic"},
		{StreamMultiplexer: "mplex", Security: "noise", Transport: "tcp"},
	}

	directions := []network.Direction{network.DirInbound, network.DirOutbound}

	_, pub1, err := crypto.GenerateEd25519Key(rand.Reader)
	require.NoError(t, err)
	_, pub2, err := crypto.GenerateEd25519Key(rand.Reader)
	require.NoError(t, err)
	_, pub3, err := crypto.GenerateEd25519Key(rand.Reader)
	require.NoError(t, err)
	keys := []crypto.PubKey{pub1, pub2, pub3}

	errors := []error{
		context.Canceled,
		context.DeadlineExceeded,
		&net.OpError{Err: syscall.ETIMEDOUT},
	}

	addrs := []ma.Multiaddr{
		ma.StringCast("/ip4/1.2.3.4/tcp/1"),
		ma.StringCast("/ip4/1.2.3.4/tcp/2"),
		ma.StringCast("/ip4/1.2.3.4/udp/2345"),
	}

	bhfNames := []string{"udp", "ipv6", "tcp", "icmp"}
	bhfState := []blackHoleState{blackHoleStateAllowed, blackHoleStateBlocked}

	tests := map[string]func(){
		"OpenedConnection": func() {
			mt.OpenedConnection(randItem(directions), randItem(keys), randItem(connections), randItem(addrs))
		},
		"ClosedConnection": func() {
			mt.ClosedConnection(randItem(directions), time.Duration(mrand.Intn(100))*time.Second, randItem(connections), randItem(addrs))
		},
		"CompletedHandshake": func() {
			mt.CompletedHandshake(time.Duration(mrand.Intn(100))*time.Second, randItem(connections), randItem(addrs))
		},
		"FailedDialing":    func() { mt.FailedDialing(randItem(addrs), randItem(errors)) },
		"DialCompleted":    func() { mt.DialCompleted(mrand.Intn(2) == 1, mrand.Intn(10)) },
		"DialRankingDelay": func() { mt.DialRankingDelay(time.Duration(mrand.Intn(1e10))) },
		"UpdatedBlackHoleFilterState": func() {
			mt.UpdatedBlackHoleFilterState(
				randItem(bhfNames),
				randItem(bhfState),
				mrand.Intn(100),
				mrand.Float64(),
			)
		},
	}

	for method, f := range tests {
		allocs := testing.AllocsPerRun(1000, f)
		if allocs > 0 {
			t.Fatalf("Alloc Test: %s, got: %0.2f, expected: 0 allocs", method, allocs)
		}
	}
}