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

//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || solaris || windows

package net

import (
	"runtime"
	"testing"
)

func TestTCPConnKeepAliveConfigDialer(t *testing.T) {
	maybeSkipKeepAliveTest(t)

	t.Cleanup(func() {
		testPreHookSetKeepAlive = func(*netFD) {}
	})
	var (
		errHook error
		oldCfg  KeepAliveConfig
	)
	testPreHookSetKeepAlive = func(nfd *netFD) {
		oldCfg, errHook = getCurrentKeepAliveSettings(nfd.pfd.Sysfd)
	}

	handler := func(ls *localServer, ln Listener) {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			c.Close()
		}
	}
	ln := newLocalListener(t, "tcp", &ListenConfig{
		KeepAlive: -1, // prevent calling hook from accepting
	})
	ls := (&streamListener{Listener: ln}).newLocalServer()
	defer ls.teardown()
	if err := ls.buildup(handler); err != nil {
		t.Fatal(err)
	}

	for _, cfg := range testConfigs {
		d := Dialer{
			KeepAlive:       defaultTCPKeepAliveIdle, // should be ignored
			KeepAliveConfig: cfg}
		c, err := d.Dial("tcp", ls.Listener.Addr().String())
		if err != nil {
			t.Fatal(err)
		}
		defer c.Close()

		if errHook != nil {
			t.Fatal(errHook)
		}

		sc, err := c.(*TCPConn).SyscallConn()
		if err != nil {
			t.Fatal(err)
		}
		if err := sc.Control(func(fd uintptr) {
			verifyKeepAliveSettings(t, fdType(fd), oldCfg, cfg)
		}); err != nil {
			t.Fatal(err)
		}
	}
}

func TestTCPConnKeepAliveConfigListener(t *testing.T) {
	maybeSkipKeepAliveTest(t)

	t.Cleanup(func() {
		testPreHookSetKeepAlive = func(*netFD) {}
	})
	var (
		errHook error
		oldCfg  KeepAliveConfig
	)
	testPreHookSetKeepAlive = func(nfd *netFD) {
		oldCfg, errHook = getCurrentKeepAliveSettings(nfd.pfd.Sysfd)
	}

	ch := make(chan Conn, 1)
	handler := func(ls *localServer, ln Listener) {
		c, err := ln.Accept()
		if err != nil {
			return
		}
		ch <- c
	}
	for _, cfg := range testConfigs {
		ln := newLocalListener(t, "tcp", &ListenConfig{
			KeepAlive:       defaultTCPKeepAliveIdle, // should be ignored
			KeepAliveConfig: cfg})
		ls := (&streamListener{Listener: ln}).newLocalServer()
		defer ls.teardown()
		if err := ls.buildup(handler); err != nil {
			t.Fatal(err)
		}
		d := Dialer{KeepAlive: -1} // prevent calling hook from dialing
		c, err := d.Dial("tcp", ls.Listener.Addr().String())
		if err != nil {
			t.Fatal(err)
		}
		defer c.Close()

		cc := <-ch
		defer cc.Close()
		if errHook != nil {
			t.Fatal(errHook)
		}
		sc, err := cc.(*TCPConn).SyscallConn()
		if err != nil {
			t.Fatal(err)
		}
		if err := sc.Control(func(fd uintptr) {
			verifyKeepAliveSettings(t, fdType(fd), oldCfg, cfg)
		}); err != nil {
			t.Fatal(err)
		}
	}
}

func TestTCPConnKeepAliveConfig(t *testing.T) {
	maybeSkipKeepAliveTest(t)

	handler := func(ls *localServer, ln Listener) {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			c.Close()
		}
	}
	ls := newLocalServer(t, "tcp")
	defer ls.teardown()
	if err := ls.buildup(handler); err != nil {
		t.Fatal(err)
	}
	for _, cfg := range testConfigs {
		d := Dialer{KeepAlive: -1} // avoid setting default values before the test
		c, err := d.Dial("tcp", ls.Listener.Addr().String())
		if err != nil {
			t.Fatal(err)
		}
		defer c.Close()

		sc, err := c.(*TCPConn).SyscallConn()
		if err != nil {
			t.Fatal(err)
		}

		var (
			errHook error
			oldCfg  KeepAliveConfig
		)
		if err := sc.Control(func(fd uintptr) {
			oldCfg, errHook = getCurrentKeepAliveSettings(fdType(fd))
		}); err != nil {
			t.Fatal(err)
		}
		if errHook != nil {
			t.Fatal(errHook)
		}

		err = c.(*TCPConn).SetKeepAliveConfig(cfg)
		if err != nil {
			if runtime.GOOS == "solaris" {
				// Solaris prior to 11.4 does not support TCP_KEEPINTVL and TCP_KEEPCNT,
				// so it will return syscall.ENOPROTOOPT when only one of Interval and Count
				// is negative. This is expected, so skip the error check in this case.
				if cfg.Interval >= 0 && cfg.Count >= 0 {
					t.Fatal(err)
				}
			} else {
				t.Fatal(err)
			}
		}

		if err := sc.Control(func(fd uintptr) {
			verifyKeepAliveSettings(t, fdType(fd), oldCfg, cfg)
		}); err != nil {
			t.Fatal(err)
		}
	}
}
