package self_test

import (
	"context"
	"crypto/rand"
	"sync/atomic"
	"testing"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/quic-go/quic-go/internal/synctest"
	"github.com/quic-go/quic-go/testutils/simnet"

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

func TestStatelessResets(t *testing.T) {
	t.Run("zero-length connection IDs", func(t *testing.T) {
		testStatelessReset(t, 0)
	})
	t.Run("10 byte connection IDs", func(t *testing.T) {
		testStatelessReset(t, 10)
	})
}

func testStatelessReset(t *testing.T, connIDLen int) {
	synctest.Test(t, func(t *testing.T) {
		var drop atomic.Bool
		clientPacketConn, serverPacketConn, closeFn := newSimnetLinkWithRouter(t,
			time.Millisecond,
			&droppingRouter{Drop: func(p simnet.Packet) bool { return drop.Load() }},
		)
		defer closeFn(t)

		var statelessResetKey quic.StatelessResetKey
		rand.Read(statelessResetKey[:])

		tr := &quic.Transport{
			Conn:              serverPacketConn,
			StatelessResetKey: &statelessResetKey,
		}
		defer tr.Close()

		ln, err := tr.Listen(getTLSConfig(), getQuicConfig(nil))
		require.NoError(t, err)

		serverErr := make(chan error, 1)
		go func() {
			conn, err := ln.Accept(context.Background())
			if err != nil {
				serverErr <- err
				return
			}
			str, err := conn.OpenStream()
			if err != nil {
				serverErr <- err
				return
			}
			_, err = str.Write([]byte("foobar"))
			if err != nil {
				serverErr <- err
				return
			}
			close(serverErr)
		}()

		var conn *quic.Conn
		if connIDLen > 0 {
			cl := &quic.Transport{
				Conn:               clientPacketConn,
				ConnectionIDLength: connIDLen,
			}
			defer cl.Close()
			var err error
			conn, err = cl.Dial(
				context.Background(),
				serverPacketConn.LocalAddr(),
				getTLSClientConfig(),
				getQuicConfig(&quic.Config{MaxIdleTimeout: 2 * time.Second}),
			)
			require.NoError(t, err)
		} else {
			conn, err = quic.Dial(
				context.Background(),
				clientPacketConn,
				serverPacketConn.LocalAddr(),
				getTLSClientConfig(),
				getQuicConfig(&quic.Config{MaxIdleTimeout: 2 * time.Second}),
			)
			require.NoError(t, err)
		}
		str, err := conn.AcceptStream(context.Background())
		require.NoError(t, err)
		data := make([]byte, 6)
		_, err = str.Read(data)
		require.NoError(t, err)
		require.Equal(t, []byte("foobar"), data)

		// make sure that the CONNECTION_CLOSE is dropped
		drop.Store(true)
		require.NoError(t, ln.Close())
		require.NoError(t, tr.Close())
		require.NoError(t, <-serverErr)
		time.Sleep(100 * time.Millisecond)

		// We need to create a new Transport here, since the old one is still sending out
		// CONNECTION_CLOSE packets for (recently) closed connections).
		tr2 := &quic.Transport{
			Conn:              serverPacketConn,
			StatelessResetKey: &statelessResetKey,
		}
		defer tr2.Close()
		ln2, err := tr2.Listen(getTLSConfig(), getQuicConfig(nil))
		require.NoError(t, err)
		drop.Store(false)

		// Trigger something (not too small) to be sent, so that we receive the stateless reset.
		// If the client already sent another packet, it might already have received a packet.
		_, serr := str.Write([]byte("Lorem ipsum dolor sit amet."))
		if serr == nil {
			_, serr = str.Read([]byte{0})
		}
		require.Error(t, serr)
		require.IsType(t, &quic.StatelessResetError{}, serr)
		require.NoError(t, ln2.Close())
	})
}
