// Copyright 2017 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.

//go:build solaris

package unix_test

import (
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

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

// getOneRetry wraps EventPort.GetOne which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getOneRetry(t *testing.T, p *unix.EventPort, timeout *unix.Timespec) (e *unix.PortEvent, err error) {
	t.Helper()
	for {
		e, err = p.GetOne(timeout)
		if err != unix.EINTR {
			break
		}
	}
	return e, err
}

// getRetry wraps EventPort.Get which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getRetry(t *testing.T, p *unix.EventPort, s []unix.PortEvent, min int, timeout *unix.Timespec) (n int, err error) {
	t.Helper()
	for {
		n, err = p.Get(s, min, timeout)
		if err != unix.EINTR {
			break
		}
		// If we did get EINTR, make sure we got 0 events
		if n != 0 {
			t.Fatalf("EventPort.Get returned events on EINTR.\ngot: %d\nexpected: 0", n)
		}
	}
	return n, err
}

func TestStatvfs(t *testing.T) {
	if err := unix.Statvfs("", nil); err == nil {
		t.Fatal(`Statvfs("") expected failure`)
	}

	statvfs := unix.Statvfs_t{}
	if err := unix.Statvfs("/", &statvfs); err != nil {
		t.Errorf(`Statvfs("/") failed: %v`, err)
	}

	if t.Failed() {
		mount, err := exec.Command("mount").CombinedOutput()
		if err != nil {
			t.Logf("mount: %v\n%s", err, mount)
		} else {
			t.Logf("mount: %s", mount)
		}
	}
}

func TestSysconf(t *testing.T) {
	n, err := unix.Sysconf(3 /* SC_CLK_TCK */)
	if err != nil {
		t.Fatalf("Sysconf: %v", err)
	}
	t.Logf("Sysconf(SC_CLK_TCK) = %d", n)
}

// Event Ports

func TestBasicEventPort(t *testing.T) {
	tmpfile, err := os.Create(filepath.Join(t.TempDir(), "eventport"))
	if err != nil {
		t.Fatal(err)
	}
	defer tmpfile.Close()
	path := tmpfile.Name()

	stat, err := os.Stat(path)
	if err != nil {
		t.Fatalf("Failed to stat %s: %v", path, err)
	}
	port, err := unix.NewEventPort()
	if err != nil {
		t.Fatalf("NewEventPort failed: %v", err)
	}
	defer port.Close()
	cookie := stat.Mode()
	err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, cookie)
	if err != nil {
		t.Errorf("AssociatePath failed: %v", err)
	}
	if !port.PathIsWatched(path) {
		t.Errorf("PathIsWatched unexpectedly returned false")
	}
	err = port.DissociatePath(path)
	if err != nil {
		t.Errorf("DissociatePath failed: %v", err)
	}
	err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, cookie)
	if err != nil {
		t.Errorf("AssociatePath failed: %v", err)
	}
	bs := []byte{42}
	tmpfile.Write(bs)
	timeout := new(unix.Timespec)
	timeout.Nsec = 100
	pevent, err := getOneRetry(t, port, timeout)
	if err == unix.ETIME {
		t.Errorf("GetOne timed out: %v", err)
	}
	if err != nil {
		t.Fatalf("GetOne failed: %v", err)
	}
	if pevent.Path != path {
		t.Errorf("Path mismatch: %v != %v", pevent.Path, path)
	}
	err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, cookie)
	if err != nil {
		t.Errorf("AssociatePath failed: %v", err)
	}
	err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, cookie)
	if err == nil {
		t.Errorf("Unexpected success associating already associated path")
	}
}

func TestEventPortFds(t *testing.T) {
	_, path, _, _ := runtime.Caller(0)
	stat, err := os.Stat(path)
	cookie := stat.Mode()
	port, err := unix.NewEventPort()
	if err != nil {
		t.Errorf("NewEventPort failed: %v", err)
	}
	defer port.Close()
	r, w, err := os.Pipe()
	if err != nil {
		t.Errorf("unable to create a pipe: %v", err)
	}
	defer w.Close()
	defer r.Close()
	fd := r.Fd()

	port.AssociateFd(fd, unix.POLLIN, cookie)
	if !port.FdIsWatched(fd) {
		t.Errorf("FdIsWatched unexpectedly returned false")
	}
	err = port.DissociateFd(fd)
	err = port.AssociateFd(fd, unix.POLLIN, cookie)
	bs := []byte{42}
	w.Write(bs)
	n, err := port.Pending()
	if n != 1 {
		t.Errorf("Pending() failed: %v, %v", n, err)
	}
	timeout := new(unix.Timespec)
	timeout.Nsec = 100
	pevent, err := getOneRetry(t, port, timeout)
	if err == unix.ETIME {
		t.Errorf("GetOne timed out: %v", err)
	}
	if err != nil {
		t.Fatalf("GetOne failed: %v", err)
	}
	if pevent.Fd != fd {
		t.Errorf("Fd mismatch: %v != %v", pevent.Fd, fd)
	}
	var c = pevent.Cookie
	if c == nil {
		t.Errorf("Cookie missing: %v != %v", cookie, c)
		return
	}
	if c != cookie {
		t.Errorf("Cookie mismatch: %v != %v", cookie, c)
	}
	port.AssociateFd(fd, unix.POLLIN, cookie)
	err = port.AssociateFd(fd, unix.POLLIN, cookie)
	if err == nil {
		t.Errorf("unexpected success associating already associated fd")
	}
}

func TestEventPortErrors(t *testing.T) {
	tmpfile, err := os.CreateTemp("", "eventport")
	if err != nil {
		t.Errorf("unable to create a tempfile: %v", err)
	}
	path := tmpfile.Name()
	stat, _ := os.Stat(path)
	os.Remove(path)
	port, _ := unix.NewEventPort()
	defer port.Close()
	err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, nil)
	if err == nil {
		t.Errorf("unexpected success associating nonexistent file")
	}
	err = port.DissociatePath(path)
	if err == nil {
		t.Errorf("unexpected success dissociating unassociated path")
	}
	timeout := new(unix.Timespec)
	timeout.Nsec = 1
	_, err = getOneRetry(t, port, timeout)
	if err != unix.ETIME {
		t.Errorf("port.GetOne(%v) returned error %v, want %v", timeout, err, unix.ETIME)
	}
	err = port.DissociateFd(uintptr(0))
	if err == nil {
		t.Errorf("unexpected success dissociating unassociated fd")
	}
	events := make([]unix.PortEvent, 4)
	_, err = getRetry(t, port, events, 5, nil)
	if err == nil {
		t.Errorf("unexpected success calling Get with min greater than len of slice")
	}
	_, err = getRetry(t, port, nil, 1, nil)
	if err == nil {
		t.Errorf("unexpected success calling Get with nil slice")
	}
	_, err = getRetry(t, port, nil, 0, nil)
	if err == nil {
		t.Errorf("unexpected success calling Get with nil slice")
	}
}

func ExamplePortEvent() {
	type MyCookie struct {
		Name string
	}
	cookie := MyCookie{"Cookie Monster"}
	port, err := unix.NewEventPort()
	if err != nil {
		fmt.Printf("NewEventPort failed: %v\n", err)
		return
	}
	defer port.Close()
	r, w, err := os.Pipe()
	if err != nil {
		fmt.Printf("os.Pipe() failed: %v\n", err)
		return
	}
	defer w.Close()
	defer r.Close()
	fd := r.Fd()

	port.AssociateFd(fd, unix.POLLIN, cookie)

	bs := []byte{42}
	w.Write(bs)
	timeout := new(unix.Timespec)
	timeout.Sec = 1
	var pevent *unix.PortEvent
	for {
		pevent, err = port.GetOne(timeout)
		if err != unix.EINTR {
			break
		}
	}
	if err != nil {
		fmt.Printf("didn't get the expected event: %v\n", err)
	}

	// Use a type assertion to convert the received cookie back to its original type
	c := pevent.Cookie.(MyCookie)
	fmt.Printf("%s", c.Name)
	//Output: Cookie Monster
}

func TestPortEventSlices(t *testing.T) {
	port, err := unix.NewEventPort()
	if err != nil {
		t.Fatalf("NewEventPort failed: %v", err)
	}
	// Create, associate, and delete 6 files
	for i := 0; i < 6; i++ {
		tmpfile, err := os.CreateTemp("", "eventport")
		if err != nil {
			t.Fatalf("unable to create tempfile: %v", err)
		}
		path := tmpfile.Name()
		stat, err := os.Stat(path)
		if err != nil {
			t.Fatalf("unable to stat tempfile: %v", err)
		}
		err = port.AssociatePath(path, stat, unix.FILE_MODIFIED, nil)
		if err != nil {
			t.Fatalf("unable to AssociatePath tempfile: %v", err)
		}
		err = os.Remove(path)
		if err != nil {
			t.Fatalf("unable to Remove tempfile: %v", err)
		}
	}
	n, err := port.Pending()
	if err != nil {
		t.Errorf("Pending failed: %v", err)
	}
	if n != 6 {
		t.Errorf("expected 6 pending events, got %d", n)
	}
	timeout := new(unix.Timespec)
	timeout.Nsec = 1
	events := make([]unix.PortEvent, 4)
	n, err = getRetry(t, port, events, 3, timeout)
	if err != nil {
		t.Errorf("Get failed: %v", err)
	}
	if n != 4 {
		t.Errorf("expected 4 events, got %d", n)
	}
	e := events[:n]
	for _, p := range e {
		if p.Events != unix.FILE_DELETE {
			t.Errorf("unexpected event. got %v, expected %v", p.Events, unix.FILE_DELETE)
		}
	}
	n, err = getRetry(t, port, events, 3, timeout)
	if err != unix.ETIME {
		t.Errorf("unexpected error. got %v, expected %v", err, unix.ETIME)
	}
	if n != 2 {
		t.Errorf("expected 2 events, got %d", n)
	}
	e = events[:n]
	for _, p := range e {
		if p.Events != unix.FILE_DELETE {
			t.Errorf("unexpected event. got %v, expected %v", p.Events, unix.FILE_DELETE)
		}
	}

	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("unable to create a pipe: %v", err)
	}
	port.AssociateFd(r.Fd(), unix.POLLIN, nil)
	port.AssociateFd(w.Fd(), unix.POLLOUT, nil)
	bs := []byte{41}
	w.Write(bs)

	n, err = getRetry(t, port, events, 1, timeout)
	if err != nil {
		t.Errorf("Get failed: %v", err)
	}
	if n != 2 {
		t.Errorf("expected 2 events, got %d", n)
	}
	err = w.Close()
	if err != nil {
		t.Errorf("w.Close() failed: %v", err)
	}
	err = r.Close()
	if err != nil {
		t.Errorf("r.Close() failed: %v", err)
	}
	err = port.Close()
	if err != nil {
		t.Errorf("port.Close() failed: %v", err)
	}
}

func TestLifreqSetName(t *testing.T) {
	var l unix.Lifreq
	err := l.SetName("12345678901234356789012345678901234567890")
	if err == nil {
		t.Fatal(`Lifreq.SetName should reject names that are too long`)
	}
	err = l.SetName("tun0")
	if err != nil {
		t.Errorf(`Lifreq.SetName("tun0") failed: %v`, err)
	}
}

func TestLifreqGetMTU(t *testing.T) {
	// Find links and their MTU using CLI tooling
	// $ dladm show-link -p -o link,mtu
	// net0:1500
	out, err := exec.Command("dladm", "show-link", "-p", "-o", "link,mtu").Output()
	if err != nil {
		t.Fatalf("unable to use dladm to find data links: %v", err)
	}
	lines := strings.Split(string(out), "\n")
	tc := make(map[string]string)
	for _, line := range lines {
		v := strings.Split(line, ":")
		if len(v) == 2 {
			tc[v[0]] = v[1]
		}
	}
	ip_fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
	if err != nil {
		t.Fatalf("could not open udp socket: %v", err)
	}
	var l unix.Lifreq
	for link, mtu := range tc {
		err = l.SetName(link)
		if err != nil {
			t.Fatalf("Lifreq.SetName(%q) failed: %v", link, err)
		}
		if err = unix.IoctlLifreq(ip_fd, unix.SIOCGLIFMTU, &l); err != nil {
			t.Fatalf("unable to SIOCGLIFMTU: %v", err)
		}
		m := l.GetLifruUint()
		if fmt.Sprintf("%d", m) != mtu {
			t.Errorf("unable to read MTU correctly: expected %s, got %d", mtu, m)
		}
	}
}

func TestUcredGet(t *testing.T) {
	euid := unix.Geteuid()
	creds, err := unix.UcredGet(unix.Getpid())
	if err != nil {
		t.Fatalf("unix.UcredGet failed: %v", err)
	}
	if euid != creds.Geteuid() {
		t.Fatalf("mismatched euid")
	}
}

func TestGetPeerUcred(t *testing.T) {
	d := t.TempDir()
	path := filepath.Join(d, "foo.sock")
	sock, err := net.Listen("unix", path)
	if err != nil {
		t.Fatalf("net.Listen: %v", err)
	}
	defer sock.Close()

	c1, err := net.Dial("unix", path)
	if err != nil {
		t.Error(err)
		return
	}
	defer c1.Close()

	c2, err := sock.Accept()
	if err != nil {
		t.Fatalf("Accept: %v", err)
	}
	defer c2.Close()

	switch unixconn := c2.(type) {
	case *net.UnixConn:
		raw, err := unixconn.SyscallConn()
		if err != nil {
			t.Fatalf("SyscallConn failed: %v", err)
		}

		var creds *unix.Ucred
		cerr := raw.Control(func(fd uintptr) {
			creds, err = unix.GetPeerUcred(fd)
			if err != nil {
				err = fmt.Errorf("unix.GetPeerUcred: %w", err)
				return
			}
		})
		if cerr != nil {
			t.Fatalf("raw.Control failed: %v", err)
		}
		if creds == nil {
			t.Fatalf("Got a nil Ucred response")
		}
		euid := unix.Geteuid()
		if euid != creds.Geteuid() {
			t.Fatalf("mismatched euid")
		}
	default:
		t.Fatalf("Somehow didn't get a UnixConn")
	}
}
