package tun

import (
	"fmt"
	"math"
	"math/rand"
	"net/netip"
	"sort"
	"syscall"
	"testing"
	"unsafe"

	"gvisor.dev/gvisor/pkg/tcpip"
	gvisorChecksum "gvisor.dev/gvisor/pkg/tcpip/checksum"
	"gvisor.dev/gvisor/pkg/tcpip/header"
)

type archChecksumDetails struct {
	name      string
	available bool
	f         func([]byte, uint16) uint16
}

func fillRandomBuffer(seed int64, buf []byte) {
	rng := rand.New(rand.NewSource(seed))
	n, err := rng.Read(buf)
	if err != nil {
		panic(err)
	}
	if n != len(buf) {
		panic("incomplete random buffer")
	}
}

func deterministicRandomBytes(seed int64, length int) []byte {
	buf := make([]byte, length)
	fillRandomBuffer(seed, buf)
	return buf
}

func getPageAlignedRandomBytes(seed int64, length int) []byte {
	alignment := syscall.Getpagesize()
	buf := make([]byte, length+(alignment-1))
	bufPtr := uintptr(unsafe.Pointer(&buf[0]))
	alignedBufPtr := (bufPtr + uintptr(alignment-1)) & ^uintptr(alignment-1)
	alignedStart := int(alignedBufPtr - bufPtr)

	buf = buf[alignedStart : alignedStart+length]
	fillRandomBuffer(seed, buf)
	return buf
}

func TestChecksum(t *testing.T) {
	alignedBuf := getPageAlignedRandomBytes(10, 8192)
	allOnes := make([]byte, 65535)
	for i := range allOnes {
		allOnes[i] = 0xff
	}
	allFE := make([]byte, 65535)
	for i := range allFE {
		allFE[i] = 0xfe
	}

	tests := []struct {
		name    string
		data    []byte
		initial uint16
		want    uint16
	}{
		{
			name:    "empty",
			data:    []byte{},
			initial: 0,
			want:    0,
		},
		{
			name:    "max initial",
			data:    []byte{},
			initial: math.MaxUint16,
			want:    0xffff,
		},
		{
			name:    "odd length",
			data:    []byte{0x01, 0x02, 0x01},
			initial: 0,
			want:    0x0202,
		},
		{
			name:    "tiny",
			data:    []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02},
			initial: 0,
			want:    0x0306,
		},
		{
			name:    "initial",
			data:    []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02},
			initial: 0x1000,
			want:    0x1306,
		},
		// cleanup0 through cleanup15 is 1024 (handled by large SIMD loops) +
		// 32 (handled by small SIMD loops) + n, where n ranges from 0 to 15
		// to cover all of the leftover byte sizes that are possible after small
		// SIMD loops that handle 16 bytes.
		{
			name:    "cleanup0",
			data:    deterministicRandomBytes(1, 1056),
			initial: 0,
			want:    0x11ec,
		},
		{
			name:    "cleanup1",
			data:    deterministicRandomBytes(1, 1057),
			initial: 0,
			want:    0xc5ec,
		},
		{
			name:    "cleanup2",
			data:    deterministicRandomBytes(1, 1058),
			initial: 0,
			want:    0xc6ad,
		},
		{
			name:    "cleanup3",
			data:    deterministicRandomBytes(1, 1059),
			initial: 0,
			want:    0x86ae,
		},
		{
			name:    "cleanup4",
			data:    deterministicRandomBytes(1, 1060),
			initial: 0,
			want:    0x878e,
		},
		{
			name:    "cleanup5",
			data:    deterministicRandomBytes(1, 1061),
			initial: 0,
			want:    0xdb8e,
		},
		{
			name:    "cleanup6",
			data:    deterministicRandomBytes(1, 1062),
			initial: 0,
			want:    0xdbd5,
		},
		{
			name:    "cleanup7",
			data:    deterministicRandomBytes(1, 1063),
			initial: 0,
			want:    0xcfd6,
		},
		{
			name:    "cleanup8",
			data:    deterministicRandomBytes(1, 1064),
			initial: 0,
			want:    0xd090,
		},
		{
			name:    "cleanup9",
			data:    deterministicRandomBytes(1, 1065),
			initial: 0,
			want:    0x0791,
		},
		{
			name:    "cleanup10",
			data:    deterministicRandomBytes(1, 1066),
			initial: 0,
			want:    0x079f,
		},
		{
			name:    "cleanup11",
			data:    deterministicRandomBytes(1, 1067),
			initial: 0,
			want:    0xba9f,
		},
		{
			name:    "cleanup12",
			data:    deterministicRandomBytes(1, 1068),
			initial: 0,
			want:    0xbb0c,
		},
		{
			name:    "cleanup13",
			data:    deterministicRandomBytes(1, 1069),
			initial: 0,
			want:    0x770d,
		},
		{
			name:    "cleanup14",
			data:    deterministicRandomBytes(1, 1070),
			initial: 0,
			want:    0x780a,
		},
		{
			name:    "cleanup15",
			data:    deterministicRandomBytes(1, 1071),
			initial: 0,
			want:    0x640b,
		},
		// small1 through small15 covers small sizes that are not large enough
		// to do overlapped reads.
		{
			name:    "small1",
			data:    deterministicRandomBytes(2, 1),
			initial: 0x1122,
			want:    0x4022,
		},
		{
			name:    "small2",
			data:    deterministicRandomBytes(2, 2),
			initial: 0x1122,
			want:    0x40a4,
		},
		{
			name:    "small3",
			data:    deterministicRandomBytes(2, 3),
			initial: 0x1122,
			want:    0xc2a4,
		},
		{
			name:    "small4",
			data:    deterministicRandomBytes(2, 4),
			initial: 0x1122,
			want:    0xc36f,
		},
		{
			name:    "small5",
			data:    deterministicRandomBytes(2, 5),
			initial: 0x1122,
			want:    0xa570,
		},
		{
			name:    "small6",
			data:    deterministicRandomBytes(2, 6),
			initial: 0x1122,
			want:    0xa669,
		},
		{
			name:    "small7",
			data:    deterministicRandomBytes(2, 7),
			initial: 0x1122,
			want:    0x0f6a,
		},
		{
			name:    "small8",
			data:    deterministicRandomBytes(2, 8),
			initial: 0x1122,
			want:    0x0fd9,
		},
		{
			name:    "small9",
			data:    deterministicRandomBytes(2, 9),
			initial: 0x1122,
			want:    0x40d9,
		},
		{
			name:    "small10",
			data:    deterministicRandomBytes(2, 10),
			initial: 0x1122,
			want:    0x411d,
		},
		{
			name:    "small11",
			data:    deterministicRandomBytes(2, 11),
			initial: 0x1122,
			want:    0x011e,
		},
		{
			name:    "small12",
			data:    deterministicRandomBytes(2, 12),
			initial: 0x1122,
			want:    0x01c8,
		},
		{
			name:    "small13",
			data:    deterministicRandomBytes(2, 13),
			initial: 0x1122,
			want:    0x4dc8,
		},
		{
			name:    "small14",
			data:    deterministicRandomBytes(2, 14),
			initial: 0x1122,
			want:    0x4eb5,
		},
		{
			name:    "small15",
			data:    deterministicRandomBytes(2, 15),
			initial: 0x1122,
			want:    0xa4b5,
		},
		// other small-ish sizes
		{
			name:    "small16",
			data:    deterministicRandomBytes(1, 16),
			initial: 0,
			want:    0x02fa,
		},
		{
			name:    "small32",
			data:    deterministicRandomBytes(1, 32),
			initial: 0,
			want:    0x03ee,
		},
		{
			name:    "small64",
			data:    deterministicRandomBytes(1, 64),
			initial: 0,
			want:    0x3f85,
		},
		{
			name:    "medium",
			data:    deterministicRandomBytes(1, 1400),
			initial: 0,
			want:    0xbea5,
		},
		{
			name:    "big",
			data:    deterministicRandomBytes(2, 65000),
			initial: 0,
			want:    0x3ba7,
		},
		{
			name:    "big-initial",
			data:    deterministicRandomBytes(2, 65000),
			initial: 0x1234,
			want:    0x4ddb,
		},
		{
			// big-small-loop is intended to exercise a few iterations of a big
			// initial loop of 128 bytes or larger + a smaller loop of 16 bytes
			// + some leftover
			name:    "big-small-loop",
			data:    deterministicRandomBytes(3, 1094),
			initial: 0x9999,
			want:    0xe65b,
		},
		{
			name:    "page-aligned",
			data:    alignedBuf[:4096],
			initial: 0,
			want:    0x963b,
		},
		{
			name:    "32-aligned",
			data:    alignedBuf[32:4128],
			initial: 0,
			want:    0x30c4,
		},
		{
			name:    "16-aligned",
			data:    alignedBuf[16:4112],
			initial: 0,
			want:    0xaeff,
		},
		{
			name:    "8-aligned",
			data:    alignedBuf[8:4104],
			initial: 0,
			want:    0x6c3b,
		},
		{
			name:    "4-aligned",
			data:    alignedBuf[4:4100],
			initial: 0,
			want:    0x2e4a,
		},
		{
			name:    "2-aligned",
			data:    alignedBuf[2:4098],
			initial: 0,
			want:    0xc702,
		},
		{
			name:    "unaligned",
			data:    alignedBuf[1:4097],
			initial: 0,
			want:    0x3bc7,
		},
		{
			name:    "unalignedAndOdd",
			data:    alignedBuf[1:4096],
			initial: 0,
			want:    0x3b13,
		},
		{
			name:    "fe1282",
			data:    allFE[:1282],
			initial: 0,
			want:    0x7c7c,
		},
		{
			name:    "fe",
			data:    allFE,
			initial: 0,
			want:    0x7e81,
		},
		{
			name:    "maximum",
			data:    allOnes,
			initial: 0,
			want:    0xff00,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			for _, fd := range archChecksumFuncs {
				t.Run(fd.name, func(t *testing.T) {
					if !fd.available {
						t.Skip("can not run on this system")
					}
					if got := fd.f(tt.data, tt.initial); got != tt.want {
						t.Errorf("%s checksum = %04x, want %04x", fd.name, got, tt.want)
					}
				})
			}
			t.Run("reference", func(t *testing.T) {
				if got := gvisorChecksum.Checksum(tt.data, tt.initial); got != tt.want {
					t.Errorf("reference checksum = %04x, want %04x", got, tt.want)
				}
			})
		})
	}
}

func TestPseudoHeaderChecksumNoFold(t *testing.T) {
	tests := []struct {
		name     string
		protocol uint8
		srcAddr  []byte
		dstAddr  []byte
		totalLen uint16
		want     uint16
	}{
		{
			name:     "ipv4",
			protocol: syscall.IPPROTO_TCP,
			srcAddr:  netip.MustParseAddr("192.168.1.1").AsSlice(),
			dstAddr:  netip.MustParseAddr("192.168.1.2").AsSlice(),
			totalLen: 1492,
			want:     0x892e,
		},
		{
			name:     "ipv6",
			protocol: syscall.IPPROTO_TCP,
			srcAddr:  netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(),
			dstAddr:  netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(),
			totalLen: 1492,
			want:     0x947f,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Run("pseudoHeaderChecksum32", func(t *testing.T) {
				got := pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
				if got != tt.want {
					t.Errorf("got %04x, want %04x", got, tt.want)
				}
			})
			t.Run("pseudoHeaderChecksum64", func(t *testing.T) {
				got := pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
				if got != tt.want {
					t.Errorf("got %04x, want %04x", got, tt.want)
				}
			})
			t.Run("reference", func(t *testing.T) {
				got := header.PseudoHeaderChecksum(
					tcpip.TransportProtocolNumber(tt.protocol),
					tcpip.AddrFromSlice(tt.srcAddr),
					tcpip.AddrFromSlice(tt.dstAddr),
					tt.totalLen)
				if got != tt.want {
					t.Errorf("got %04x, want %04x", got, tt.want)
				}
			})
		})
	}
}

func FuzzChecksum(f *testing.F) {
	buf := getPageAlignedRandomBytes(1234, 65536)

	f.Add([]byte{}, uint16(0))
	f.Add([]byte{}, uint16(0x1234))
	f.Add([]byte{}, uint16(0))
	f.Add(buf[:15], uint16(0x1234))
	f.Add(buf[:256], uint16(0x1234))
	f.Add(buf[:1280], uint16(0x1234))
	f.Add(buf[:1288], uint16(0x1234))
	f.Add(buf[1:1050], uint16(0x1234))

	f.Fuzz(func(t *testing.T, data []byte, initial uint16) {
		want := gvisorChecksum.Checksum(data, initial)

		for _, fd := range archChecksumFuncs {
			t.Run(fd.name, func(t *testing.T) {
				if !fd.available {
					t.Skip("can not run on this system")
				}
				if got := fd.f(data, initial); got != want {
					t.Errorf("%s checksum = %04x, want %04x", fd.name, got, want)
				}
			})
		}
	})
}

var result uint16

func BenchmarkChecksum(b *testing.B) {
	offsets := []int{ // offsets from page alignment
		0,
		1,
		2,
		4,
		8,
		16,
	}
	lengths := []int{
		0,
		7,
		15,
		16,
		31,
		64,
		90,
		95,
		128,
		256,
		512,
		1024,
		1240,
		1500,
		2048,
		4096,
		8192,
		9000,
		9001,
		16384,
		65536,
	}
	if !sort.IntsAreSorted(offsets) {
		b.Fatal("offsets are not sorted")
	}
	largestLength := lengths[len(lengths)-1]
	if !sort.IntsAreSorted(lengths) {
		b.Fatal("lengths are not sorted")
	}
	largestOffset := lengths[len(offsets)-1]
	alignedBuf := getPageAlignedRandomBytes(1, largestOffset+largestLength)
	var r uint16
	for _, offset := range offsets {
		name := fmt.Sprintf("%vAligned", offset)
		if offset == 0 {
			name = "pageAligned"
		}
		offsetBuf := alignedBuf[offset:]
		b.Run(name, func(b *testing.B) {
			for _, length := range lengths {
				b.Run(fmt.Sprintf("%d", length), func(b *testing.B) {
					for _, fd := range archChecksumFuncs {
						b.Run(fd.name, func(b *testing.B) {
							if !fd.available {
								b.Skip("can not run on this system")
							}
							b.SetBytes(int64(length))
							for i := 0; i < b.N; i++ {
								r += fd.f(offsetBuf[:length], 0)
							}
						})
					}
				})
			}
		})
	}
	result = r
}

func BenchmarkPseudoHeaderChecksum(b *testing.B) {
	tests := []struct {
		name     string
		protocol uint8
		srcAddr  []byte
		dstAddr  []byte
		totalLen uint16
		want     uint16
	}{
		{
			name:     "ipv4",
			protocol: syscall.IPPROTO_TCP,
			srcAddr:  []byte{192, 168, 1, 1},
			dstAddr:  []byte{192, 168, 1, 2},
			totalLen: 1492,
			want:     0x892e,
		},
		{
			name:     "ipv6",
			protocol: syscall.IPPROTO_TCP,
			srcAddr:  netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(),
			dstAddr:  netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(),
			totalLen: 1492,
			want:     0x892e,
		},
	}
	for _, tt := range tests {
		b.Run(tt.name, func(b *testing.B) {
			b.Run("pseudoHeaderChecksum32", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					result += pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
				}
			})
			b.Run("pseudoHeaderChecksum64", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					result += pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
				}
			})
		})
	}
}
