package main

import (
	"testing"
	"unsafe"

	"golang.org/x/sys/unix"
)

func putCmsg(buf []byte, off int, level, typ int32, dataLen int) int {
	h := (*unix.Cmsghdr)(unsafe.Pointer(&buf[off]))
	h.SetLen(unix.CmsgLen(dataLen))
	h.Level = level
	h.Type = typ
	return off + unix.CmsgSpace(dataLen)
}

func putStamp(buf []byte, off int, raw unix.Timespec) int {
	next := putCmsg(buf, off, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen)
	ts := (*[3]unix.Timespec)(unsafe.Pointer(&buf[off+cmsgDataOff]))
	ts[2] = raw
	return next
}

func TestHwTimestampReadsRawHardwareClock(t *testing.T) {
	buf := make([]byte, cmsgLen)
	n := putStamp(buf, 0, unix.Timespec{Sec: 5, Nsec: 7})

	got, ok := hwTimestamp(buf[:n])
	if !ok || got != 5e9+7 {
		t.Errorf("hwTimestamp = %d, %v; want 5000000007, true", got, ok)
	}
}

func TestHwTimestampSkipsOtherMessages(t *testing.T) {
	buf := make([]byte, cmsgLen)
	off := putCmsg(buf, 0, unix.SOL_SOCKET, unix.SCM_RIGHTS, 4)
	n := putStamp(buf, off, unix.Timespec{Sec: 1, Nsec: 2})

	got, ok := hwTimestamp(buf[:n])
	if !ok || got != 1e9+2 {
		t.Errorf("hwTimestamp = %d, %v; want 1000000002, true", got, ok)
	}
}

func TestHwTimestampRejectsZeroStamp(t *testing.T) {
	buf := make([]byte, cmsgLen)
	n := putStamp(buf, 0, unix.Timespec{})

	if got, ok := hwTimestamp(buf[:n]); ok {
		t.Errorf("hwTimestamp = %d, %v; want a zero stamp refused", got, ok)
	}
}

func TestHwTimestampWithoutAStamp(t *testing.T) {
	buf := make([]byte, cmsgLen)
	n := putCmsg(buf, 0, unix.SOL_SOCKET, unix.SCM_RIGHTS, 4)

	if _, ok := hwTimestamp(buf[:n]); ok {
		t.Error("a buffer carrying no timestamp yielded one")
	}
	if _, ok := hwTimestamp(nil); ok {
		t.Error("an empty buffer yielded a timestamp")
	}
}

func TestHwTimestampRefusesBadLengths(t *testing.T) {
	buf := make([]byte, cmsgLen)
	n := putStamp(buf, 0, unix.Timespec{Sec: 1})
	if _, ok := hwTimestamp(buf[:n-1]); ok {
		t.Error("a message running past the buffer yielded a timestamp")
	}

	short := make([]byte, cmsgLen)
	putCmsg(short, 0, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen)
	(*unix.Cmsghdr)(unsafe.Pointer(&short[0])).SetLen(unix.SizeofCmsghdr - 1)
	if _, ok := hwTimestamp(short); ok {
		t.Error("a message shorter than its own header yielded a timestamp")
	}

	trunc := make([]byte, cmsgLen)
	tn := putCmsg(trunc, 0, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen/3)
	if _, ok := hwTimestamp(trunc[:tn]); ok {
		t.Error("a truncated stamp yielded a timestamp")
	}
}

func BenchmarkHwTimestamp(b *testing.B) {
	buf := make([]byte, cmsgLen)
	n := putStamp(buf, 0, unix.Timespec{Sec: 5, Nsec: 7})
	oob := buf[:n]

	b.ReportAllocs()
	for b.Loop() {
		if _, ok := hwTimestamp(oob); !ok {
			b.Fatal("no timestamp")
		}
	}
}
