package ping_test

import (
	"context"
	"errors"
	"net/netip"
	"os"
	"slices"
	"testing"
	"time"

	"github.com/sagernet/sing-tun/gtcpip/header"
	"github.com/sagernet/sing-tun/ping"
	"github.com/sagernet/sing/common/buf"
	"github.com/sagernet/sing/common/logger"

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

func TestIsClosed(t *testing.T) {
	t.Parallel()
	destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, netip.MustParseAddr("1.1.1.1"), nil, 30*time.Second)
	require.NoError(t, err)
	defer destination.Close()
	time.Sleep(1 * time.Second)
	require.False(t, destination.IsClosed())
	destination.Close()
	require.True(t, destination.IsClosed())
}

type channelWriter struct {
	packets chan []byte
}

func (w *channelWriter) WritePacket(packet []byte) error {
	select {
	case w.packets <- slices.Clone(packet):
	default:
	}
	return nil
}

type discardWriter struct{}

func (w discardWriter) WritePacket(packet []byte) error {
	return nil
}

func buildEchoRequest(source, destination netip.Addr, identifier, sequence uint16) *buf.Buffer {
	const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize
	packet := buf.NewSize(totalLen)
	ipHdr := header.IPv4(packet.Extend(totalLen))
	ipHdr.Encode(&header.IPv4Fields{
		TotalLength: totalLen,
		TTL:         64,
		Protocol:    uint8(header.ICMPv4ProtocolNumber),
		SrcAddr:     source,
		DstAddr:     destination,
	})
	ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
	icmpHdr := header.ICMPv4(ipHdr.Payload())
	icmpHdr.SetType(header.ICMPv4Echo)
	icmpHdr.SetIdent(identifier)
	icmpHdr.SetSequence(sequence)
	icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0))
	return packet
}

// darwin unprivileged ICMP sockets and Linux raw sockets receive every
// loopback ICMP packet regardless of the flow it belongs to, so this test
// cannot run parallel to TestPing.
func TestDestinationIdleExpiry(t *testing.T) {
	loopback := netip.MustParseAddr("127.0.0.1")
	writer := &channelWriter{packets: make(chan []byte, 16)}
	destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, writer, time.Second)
	if errors.Is(err, os.ErrPermission) {
		t.SkipNow()
	}
	require.NoError(t, err)
	defer destination.Close()

	err = destination.WritePacket(buildEchoRequest(loopback, loopback, 0x1111, 1))
	require.NoError(t, err)
	select {
	case packet := <-writer.packets:
		replyIPHdr := header.IPv4(packet)
		replyICMPHdr := header.ICMPv4(replyIPHdr.Payload())
		require.Equal(t, header.ICMPv4EchoReply, replyICMPHdr.Type())
		require.Equal(t, uint16(0x1111), replyICMPHdr.Ident())
	case <-time.After(3 * time.Second):
		t.Fatal("no echo reply received")
	}

	noise, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, discardWriter{}, 30*time.Second)
	require.NoError(t, err)
	defer noise.Close()

	deadline := time.Now().Add(5 * time.Second)
	var sequence uint16
	for time.Now().Before(deadline) {
		sequence++
		_ = noise.WritePacket(buildEchoRequest(loopback, loopback, 0x2222, sequence))
		if destination.IsClosed() {
			return
		}
		time.Sleep(150 * time.Millisecond)
	}
	t.Fatal("flow not closed after idle timeout despite unrelated ICMP traffic")
}
