package main

import (
	"context"
	"net"
	"net/netip"
	"testing"
	"time"

	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/option"
	"github.com/sagernet/sing/common"
	F "github.com/sagernet/sing/common/format"
	"github.com/sagernet/sing/common/json/badoption"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/protocol/socks"

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

func TestSOCKSUDPTimeout(t *testing.T) {
	const testTimeout = 2 * time.Second
	udpTimeout := option.UDPTimeoutCompat(testTimeout)

	startInstance(t, option.Options{
		Inbounds: []option.Inbound{
			{
				Type: C.TypeSOCKS,
				Tag:  "socks-in",
				Options: &option.SocksInboundOptions{
					ListenOptions: option.ListenOptions{
						Listen:     common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
						ListenPort: clientPort,
						UDPTimeout: udpTimeout,
					},
				},
			},
		},
		Outbounds: []option.Outbound{
			{
				Type: C.TypeDirect,
			},
		},
	})

	testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout)
}

func TestMixedUDPTimeout(t *testing.T) {
	const testTimeout = 2 * time.Second
	udpTimeout := option.UDPTimeoutCompat(testTimeout)

	startInstance(t, option.Options{
		Inbounds: []option.Inbound{
			{
				Type: C.TypeMixed,
				Tag:  "mixed-in",
				Options: &option.HTTPMixedInboundOptions{
					ListenOptions: option.ListenOptions{
						Listen:     common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
						ListenPort: clientPort,
						UDPTimeout: udpTimeout,
					},
				},
			},
		},
		Outbounds: []option.Outbound{
			{
				Type: C.TypeDirect,
			},
		},
	})

	testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout)
}

func testUDPSessionIdleTimeout(t *testing.T, proxyPort uint16, echoPort uint16, expectedTimeout time.Duration) {
	echoServer, err := listenPacket("udp", ":"+F.ToString(echoPort))
	require.NoError(t, err)
	defer echoServer.Close()

	go func() {
		buffer := make([]byte, 1024)
		for {
			n, address, err := echoServer.ReadFrom(buffer)
			if err != nil {
				return
			}
			_, _ = echoServer.WriteTo(buffer[:n], address)
		}
	}()

	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")

	packetConn, err := dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", echoPort))
	require.NoError(t, err)
	defer packetConn.Close()

	remoteAddress := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(echoPort)}

	_, err = packetConn.WriteTo([]byte("hello"), remoteAddress)
	require.NoError(t, err)

	buffer := make([]byte, 1024)
	packetConn.SetReadDeadline(time.Now().Add(5 * time.Second))
	n, _, err := packetConn.ReadFrom(buffer)
	require.NoError(t, err, "failed to receive echo response")
	require.Equal(t, "hello", string(buffer[:n]))
	t.Log("UDP echo successful, session established")

	packetConn.SetReadDeadline(time.Time{})

	waitTime := expectedTimeout + time.Second
	t.Logf("Waiting %v for UDP session to timeout...", waitTime)
	time.Sleep(waitTime)

	_, err = packetConn.WriteTo([]byte("after-timeout"), remoteAddress)
	if err != nil {
		t.Logf("Write after timeout correctly failed: %v", err)
		return
	}

	packetConn.SetReadDeadline(time.Now().Add(3 * time.Second))
	n, _, err = packetConn.ReadFrom(buffer)
	if err != nil {
		t.Logf("Read after timeout correctly failed: %v", err)
		return
	}

	t.Fatalf("UDP session should have timed out after %v, but received response: %s",
		expectedTimeout, string(buffer[:n]))
}
