package netxlite

import (
	"context"
	"crypto/tls"
	"errors"
	"io"
	"net"
	"strings"
	"testing"

	"github.com/apex/log"
	"github.com/google/go-cmp/cmp"
	"github.com/lucas-clemente/quic-go"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/model/mocks"
)

func TestNewQUICListener(t *testing.T) {
	ql := NewQUICListener()
	qew := ql.(*quicListenerErrWrapper)
	_ = qew.QUICListener.(*quicListenerStdlib)
}

type extensionQUICDialerFirst struct {
	model.QUICDialer
}

type quicDialerWrapperFirst struct{}

func (*quicDialerWrapperFirst) WrapQUICDialer(qd model.QUICDialer) model.QUICDialer {
	return &extensionQUICDialerFirst{qd}
}

type extensionQUICDialerSecond struct {
	model.QUICDialer
}

type quicDialerWrapperSecond struct {
	model.QUICDialer
}

func (*quicDialerWrapperSecond) WrapQUICDialer(qd model.QUICDialer) model.QUICDialer {
	return &extensionQUICDialerSecond{qd}
}

func TestNewQUICDialer(t *testing.T) {
	ql := NewQUICListener()
	extensions := []model.QUICDialerWrapper{
		&quicDialerWrapperFirst{},
		nil, // explicitly test for this documented case
		&quicDialerWrapperSecond{},
	}
	dlr := NewQUICDialerWithoutResolver(ql, log.Log, extensions...)
	logger := dlr.(*quicDialerLogger)
	if logger.Logger != log.Log {
		t.Fatal("invalid logger")
	}
	resolver := logger.Dialer.(*quicDialerResolver)
	if _, okay := resolver.Resolver.(*NullResolver); !okay {
		t.Fatal("invalid resolver type")
	}
	logger = resolver.Dialer.(*quicDialerLogger)
	if logger.Logger != log.Log {
		t.Fatal("invalid logger")
	}
	ext2 := logger.Dialer.(*extensionQUICDialerSecond)
	ext1 := ext2.QUICDialer.(*extensionQUICDialerFirst)
	errWrapper := ext1.QUICDialer.(*quicDialerErrWrapper)
	handshakeCompleter := errWrapper.QUICDialer.(*quicDialerHandshakeCompleter)
	base := handshakeCompleter.Dialer.(*quicDialerQUICGo)
	if base.QUICListener != ql {
		t.Fatal("invalid quic listener")
	}
}

func TestParseUDPAddr(t *testing.T) {
	t.Run("cannot split host and port", func(t *testing.T) {
		addr, err := ParseUDPAddr("1.2.3.4")
		if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
			t.Fatal("unexpected error", err)
		}
		if addr != nil {
			t.Fatal("expected nil addr")
		}
	})

	t.Run("with invalid IP addr", func(t *testing.T) {
		addr, err := ParseUDPAddr("www.google.com:80")
		if !errors.Is(err, ErrInvalidIP) {
			t.Fatal("unexpected error", err)
		}
		if addr != nil {
			t.Fatal("expected nil addr")
		}
	})

	t.Run("with invalid port", func(t *testing.T) {
		addr, err := ParseUDPAddr("8.8.8.8:www")
		if err == nil || !strings.HasSuffix(err.Error(), "invalid syntax") {
			t.Fatal("unexpected error", err)
		}
		if addr != nil {
			t.Fatal("expected nil addr")
		}
	})

	t.Run("with valid input", func(t *testing.T) {
		addr, err := ParseUDPAddr("8.8.8.8:80")
		if err != nil {
			t.Fatal(err)
		}
		if addr.IP.String() != "8.8.8.8" {
			t.Fatal("invalid IP")
		}
		if addr.Port != 80 {
			t.Fatal("invalid port")
		}
		if addr.Zone != "" {
			t.Fatal("invalid zone")
		}
	})
}

func TestQUICDialerQUICGo(t *testing.T) {
	t.Run("DialContext", func(t *testing.T) {
		t.Run("cannot split host port", func(t *testing.T) {
			tlsConfig := &tls.Config{
				ServerName: "www.google.com",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
			}
			defer systemdialer.CloseIdleConnections() // just to see it running
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "a.b.c.d", tlsConfig, &quic.Config{})
			if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
		})

		t.Run("with invalid port", func(t *testing.T) {
			tlsConfig := &tls.Config{
				ServerName: "www.google.com",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.4.4:xyz", tlsConfig, &quic.Config{})
			if err == nil || !strings.HasSuffix(err.Error(), "invalid syntax") {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
		})

		t.Run("with invalid IP", func(t *testing.T) {
			tlsConfig := &tls.Config{
				ServerName: "www.google.com",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "a.b.c.d:0", tlsConfig, &quic.Config{})
			if !errors.Is(err, ErrInvalidIP) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
		})

		t.Run("with listen error", func(t *testing.T) {
			expected := errors.New("mocked error")
			tlsConfig := &tls.Config{
				ServerName: "www.google.com",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &mocks.QUICListener{
					MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
						return nil, expected
					},
				},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.8.8:443", tlsConfig, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
		})

		t.Run("with handshake failure", func(t *testing.T) {
			tlsConfig := &tls.Config{
				ServerName: "dns.google",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
			}
			ctx, cancel := context.WithCancel(context.Background())
			cancel() // fail immediately
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.8.8:443", tlsConfig, &quic.Config{})
			if !errors.Is(err, context.Canceled) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				log.Fatal("expected nil connection here")
			}
		})

		t.Run("TLS defaults for web", func(t *testing.T) {
			expected := errors.New("mocked error")
			var gotTLSConfig *tls.Config
			tlsConfig := &tls.Config{
				ServerName: "dns.google",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
				mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn,
					remoteAddr net.Addr, host string, tlsConfig *tls.Config,
					quicConfig *quic.Config) (quic.EarlyConnection, error) {
					gotTLSConfig = tlsConfig
					return nil, expected
				},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.8.8:443", tlsConfig, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
			if tlsConfig.RootCAs != nil {
				t.Fatal("tlsConfig.RootCAs should not have been changed")
			}
			if gotTLSConfig.RootCAs != defaultCertPool {
				t.Fatal("invalid gotTLSConfig.RootCAs")
			}
			if tlsConfig.NextProtos != nil {
				t.Fatal("tlsConfig.NextProtos should not have been changed")
			}
			if diff := cmp.Diff(gotTLSConfig.NextProtos, []string{"h3"}); diff != "" {
				t.Fatal("invalid gotTLSConfig.NextProtos", diff)
			}
			if tlsConfig.ServerName != gotTLSConfig.ServerName {
				t.Fatal("the ServerName field must match")
			}
		})

		t.Run("TLS defaults for DoQ", func(t *testing.T) {
			expected := errors.New("mocked error")
			var gotTLSConfig *tls.Config
			tlsConfig := &tls.Config{
				ServerName: "dns.google",
			}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
				mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn,
					remoteAddr net.Addr, host string, tlsConfig *tls.Config,
					quicConfig *quic.Config) (quic.EarlyConnection, error) {
					gotTLSConfig = tlsConfig
					return nil, expected
				},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.8.8:8853", tlsConfig, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
			if tlsConfig.RootCAs != nil {
				t.Fatal("tlsConfig.RootCAs should not have been changed")
			}
			if gotTLSConfig.RootCAs != defaultCertPool {
				t.Fatal("invalid gotTLSConfig.RootCAs")
			}
			if tlsConfig.NextProtos != nil {
				t.Fatal("tlsConfig.NextProtos should not have been changed")
			}
			if diff := cmp.Diff(gotTLSConfig.NextProtos, []string{"dq"}); diff != "" {
				t.Fatal("invalid gotTLSConfig.NextProtos", diff)
			}
			if tlsConfig.ServerName != gotTLSConfig.ServerName {
				t.Fatal("the ServerName field must match")
			}
		})

		t.Run("returns a quicDialerOwnConn in case of success", func(t *testing.T) {
			tlsConfig := &tls.Config{
				ServerName: "dns.google",
			}
			fakeconn := &mocks.QUICEarlyConnection{}
			systemdialer := quicDialerQUICGo{
				QUICListener: &quicListenerStdlib{},
				mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn,
					remoteAddr net.Addr, host string, tlsConfig *tls.Config,
					quicConfig *quic.Config) (quic.EarlyConnection, error) {
					return fakeconn, nil
				},
			}
			ctx := context.Background()
			qconn, err := systemdialer.DialContext(
				ctx, "8.8.8.8:443", tlsConfig, &quic.Config{})
			if err != nil {
				t.Fatal(err)
			}
			connOwner := qconn.(*quicConnectionOwnsConn)
			if connOwner.EarlyConnection != fakeconn {
				t.Fatal("invalid underlying conn")
			}
		})
	})
}

func TestQUICDialerHandshakeCompleter(t *testing.T) {
	t.Run("DialContext", func(t *testing.T) {
		t.Run("in case of failure", func(t *testing.T) {
			expected := errors.New("mocked error")
			d := &quicDialerHandshakeCompleter{
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string,
						tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return nil, expected
					},
				},
			}
			ctx := context.Background()
			conn, err := d.DialContext(ctx, "8.8.8.8:443", &tls.Config{}, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("unexpected err", err)
			}
			if conn != nil {
				t.Fatal("expected nil conn")
			}
		})

		t.Run("in case of context cancellation", func(t *testing.T) {
			handshakeCtx, handshakeCancel := context.WithCancel(context.Background())
			defer handshakeCancel()
			ctx, cancel := context.WithCancel(context.Background())
			var called bool
			expected := &mocks.QUICEarlyConnection{
				MockHandshakeComplete: func() context.Context {
					cancel()
					return handshakeCtx
				},
				MockCloseWithError: func(code quic.ApplicationErrorCode, reason string) error {
					called = true
					return nil
				},
			}
			d := &quicDialerHandshakeCompleter{
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string,
						tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return expected, nil
					},
				},
			}
			conn, err := d.DialContext(ctx, "8.8.8.8:443", &tls.Config{}, &quic.Config{})
			if !errors.Is(err, context.Canceled) {
				t.Fatal("unexpected err", err)
			}
			if conn != nil {
				t.Fatal("expected nil conn")
			}
			if !called {
				t.Fatal("not called")
			}
		})

		t.Run("in case of success", func(t *testing.T) {
			handshakeCtx, handshakeCancel := context.WithCancel(context.Background())
			defer handshakeCancel()
			expected := &mocks.QUICEarlyConnection{
				MockHandshakeComplete: func() context.Context {
					handshakeCancel()
					return handshakeCtx
				},
			}
			d := &quicDialerHandshakeCompleter{
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string,
						tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return expected, nil
					},
				},
			}
			conn, err := d.DialContext(
				context.Background(), "8.8.8.8:443", &tls.Config{}, &quic.Config{})
			if err != nil {
				t.Fatal(err)
			}
			if conn == nil {
				t.Fatal("expected non-nil conn")
			}
		})
	})

	t.Run("CloseIdleConnections", func(t *testing.T) {
		var forDialer bool
		d := &quicDialerHandshakeCompleter{
			Dialer: &mocks.QUICDialer{
				MockCloseIdleConnections: func() {
					forDialer = true
				},
			},
		}
		d.CloseIdleConnections()
		if !forDialer {
			t.Fatal("not called")
		}
	})
}

func TestQUICConnectionOwnsConn(t *testing.T) {
	var (
		quicClose bool
		udpClose  bool
	)
	qconn := &mocks.QUICEarlyConnection{
		MockCloseWithError: func(code quic.ApplicationErrorCode, reason string) error {
			quicClose = true
			return nil
		},
	}
	pconn := &mocks.UDPLikeConn{
		MockClose: func() error {
			udpClose = true
			return nil
		},
	}
	conn := newQUICConnectionOwnsConn(qconn, pconn)
	conn.CloseWithError(0, "")
	if !quicClose {
		t.Fatal("did not call qconn.CloseWithError")
	}
	if !udpClose {
		t.Fatal("did not call pconn.Close")
	}
}

func TestQUICDialerResolver(t *testing.T) {
	t.Run("CloseIdleConnections", func(t *testing.T) {
		var (
			forDialer   bool
			forResolver bool
		)
		d := &quicDialerResolver{
			Dialer: &mocks.QUICDialer{
				MockCloseIdleConnections: func() {
					forDialer = true
				},
			},
			Resolver: &mocks.Resolver{
				MockCloseIdleConnections: func() {
					forResolver = true
				},
			},
		}
		d.CloseIdleConnections()
		if !forDialer || !forResolver {
			t.Fatal("not called")
		}
	})

	t.Run("DialContext", func(t *testing.T) {
		t.Run("with missing port", func(t *testing.T) {
			tlsConfig := &tls.Config{}
			dialer := &quicDialerResolver{
				Resolver: NewStdlibResolver(log.Log),
				Dialer:   &quicDialerQUICGo{}}
			qconn, err := dialer.DialContext(
				context.Background(), "www.google.com",
				tlsConfig, &quic.Config{})
			if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
				t.Fatal("not the error we expected")
			}
			if qconn != nil {
				t.Fatal("expected a nil connection here")
			}
		})

		t.Run("with lookup host failure", func(t *testing.T) {
			tlsConfig := &tls.Config{}
			expected := errors.New("mocked error")
			dialer := &quicDialerResolver{Resolver: &mocks.Resolver{
				MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
					return nil, expected
				},
			}}
			qconn, err := dialer.DialContext(
				context.Background(), "dns.google.com:853",
				tlsConfig, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected")
			}
			if qconn != nil {
				t.Fatal("expected nil connection")
			}
		})

		t.Run("with invalid, non-numeric port)", func(t *testing.T) {
			// This test allows us to check for the case where every attempt
			// to establish a connection leads to a failure
			tlsConf := &tls.Config{}
			dialer := &quicDialerResolver{
				Resolver: NewStdlibResolver(log.Log),
				Dialer: &quicDialerQUICGo{
					QUICListener: &quicListenerStdlib{},
				}}
			qconn, err := dialer.DialContext(
				context.Background(), "8.8.4.4:x",
				tlsConf, &quic.Config{})
			if err == nil {
				t.Fatal("expected an error here")
			}
			if !strings.HasSuffix(err.Error(), "invalid syntax") {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection")
			}
		})

		t.Run("we apply TLS defaults", func(t *testing.T) {
			expected := errors.New("mocked error")
			var gotTLSConfig *tls.Config
			tlsConfig := &tls.Config{}
			dialer := &quicDialerResolver{
				Resolver: NewStdlibResolver(log.Log),
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string,
						tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						gotTLSConfig = tlsConfig
						return nil, expected
					},
				}}
			qconn, err := dialer.DialContext(
				context.Background(), "8.8.4.4:443",
				tlsConfig, &quic.Config{})
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection here")
			}
			if tlsConfig.ServerName != "" {
				t.Fatal("should not have changed tlsConfig.ServerName")
			}
			if gotTLSConfig.ServerName != "8.8.4.4" {
				t.Fatal("gotTLSConfig.ServerName has not been set")
			}
		})

		t.Run("on success", func(t *testing.T) {
			expectedQConn := &mocks.QUICEarlyConnection{}
			dialer := &quicDialerResolver{
				Resolver: NewStdlibResolver(log.Log),
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string,
						tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return expectedQConn, nil
					},
				}}
			qconn, err := dialer.DialContext(
				context.Background(), "8.8.4.4:443",
				&tls.Config{}, &quic.Config{})
			if err != nil {
				t.Fatal(err)
			}
			if qconn != expectedQConn {
				t.Fatal("unexpected underlying qconn")
			}
		})
	})

	t.Run("lookup host with address", func(t *testing.T) {
		dialer := &quicDialerResolver{Resolver: &mocks.Resolver{
			MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
				// We should not arrive here and call this function but if we do then
				// there is going to be an error that fails this test.
				return nil, errors.New("mocked error")
			},
		}}
		addrs, err := dialer.lookupHost(context.Background(), "1.1.1.1")
		if err != nil {
			t.Fatal(err)
		}
		if len(addrs) != 1 || addrs[0] != "1.1.1.1" {
			t.Fatal("not the result we expected")
		}
	})
}

func TestQUICLoggerDialer(t *testing.T) {
	t.Run("CloseIdleConnections", func(t *testing.T) {
		var forDialer bool
		d := &quicDialerLogger{
			Dialer: &mocks.QUICDialer{
				MockCloseIdleConnections: func() {
					forDialer = true
				},
			},
		}
		d.CloseIdleConnections()
		if !forDialer {
			t.Fatal("not called")
		}
	})

	t.Run("DialContext", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			var called int
			lo := &mocks.Logger{
				MockDebugf: func(format string, v ...interface{}) {
					called++
				},
			}
			d := &quicDialerLogger{
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context,
						address string, tlsConfig *tls.Config,
						quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return &mocks.QUICEarlyConnection{
							MockCloseWithError: func(
								code quic.ApplicationErrorCode, reason string) error {
								return nil
							},
						}, nil
					},
				},
				Logger: lo,
			}
			ctx := context.Background()
			tlsConfig := &tls.Config{}
			quicConfig := &quic.Config{}
			qconn, err := d.DialContext(ctx, "8.8.8.8:443", tlsConfig, quicConfig)
			if err != nil {
				t.Fatal(err)
			}
			if err := qconn.CloseWithError(0, ""); err != nil {
				t.Fatal(err)
			}
			if called != 2 {
				t.Fatal("invalid number of calls")
			}
		})

		t.Run("on failure", func(t *testing.T) {
			var called int
			lo := &mocks.Logger{
				MockDebugf: func(format string, v ...interface{}) {
					called++
				},
			}
			expected := errors.New("mocked error")
			d := &quicDialerLogger{
				Dialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context,
						address string, tlsConfig *tls.Config,
						quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return nil, expected
					},
				},
				Logger: lo,
			}
			ctx := context.Background()
			tlsConfig := &tls.Config{}
			quicConfig := &quic.Config{}
			qconn, err := d.DialContext(ctx, "8.8.8.8:443", tlsConfig, quicConfig)
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
			if qconn != nil {
				t.Fatal("expected nil connection")
			}
			if called != 2 {
				t.Fatal("invalid number of calls")
			}
		})
	})
}

func TestNewSingleUseQUICDialer(t *testing.T) {
	qconn := &mocks.QUICEarlyConnection{}
	qd := NewSingleUseQUICDialer(qconn)
	defer qd.CloseIdleConnections()
	outconn, err := qd.DialContext(
		context.Background(), "", &tls.Config{}, &quic.Config{})
	if err != nil {
		t.Fatal(err)
	}
	if qconn != outconn {
		t.Fatal("invalid outconn")
	}
	for i := 0; i < 4; i++ {
		outconn, err = qd.DialContext(
			context.Background(), "", &tls.Config{}, &quic.Config{})
		if !errors.Is(err, ErrNoConnReuse) {
			t.Fatal("not the error we expected", err)
		}
		if outconn != nil {
			t.Fatal("expected nil outconn here")
		}
	}
}

func TestQUICListenerErrWrapper(t *testing.T) {
	t.Run("Listen", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			expectedConn := &mocks.UDPLikeConn{}
			ql := &quicListenerErrWrapper{
				QUICListener: &mocks.QUICListener{
					MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
						return expectedConn, nil
					},
				},
			}
			conn, err := ql.Listen(&net.UDPAddr{})
			if err != nil {
				t.Fatal(err)
			}
			ewconn := conn.(*quicErrWrapperUDPLikeConn)
			if ewconn.UDPLikeConn != expectedConn {
				t.Fatal("unexpected conn")
			}
		})

		t.Run("on failure", func(t *testing.T) {
			expectedErr := io.EOF
			ql := &quicListenerErrWrapper{
				QUICListener: &mocks.QUICListener{
					MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
						return nil, expectedErr
					},
				},
			}
			conn, err := ql.Listen(&net.UDPAddr{})
			if err == nil || err.Error() != FailureEOFError {
				t.Fatal("unexpected err", err)
			}
			if conn != nil {
				t.Fatal("expected nil conn")
			}
		})
	})
}

func TestQUICErrWrapperUDPLikeConn(t *testing.T) {
	t.Run("ReadFrom", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			expectedAddr := &net.UDPAddr{}
			p := make([]byte, 128)
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockReadFrom: func(p []byte) (n int, addr net.Addr, err error) {
						return len(p), expectedAddr, nil
					},
				},
			}
			count, addr, err := conn.ReadFrom(p)
			if err != nil {
				t.Fatal(err)
			}
			if count != len(p) {
				t.Fatal("unexpected count")
			}
			if addr != expectedAddr {
				t.Fatal("unexpected addr")
			}
		})

		t.Run("on failure", func(t *testing.T) {
			p := make([]byte, 128)
			expectedErr := io.EOF
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockReadFrom: func(p []byte) (n int, addr net.Addr, err error) {
						return 0, nil, expectedErr
					},
				},
			}
			count, addr, err := conn.ReadFrom(p)
			if err == nil || err.Error() != FailureEOFError {
				t.Fatal("unexpected err", err)
			}
			if count != 0 {
				t.Fatal("unexpected count")
			}
			if addr != nil {
				t.Fatal("unexpected addr")
			}
		})
	})

	t.Run("WriteTo", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			p := make([]byte, 128)
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
						return len(p), nil
					},
				},
			}
			count, err := conn.WriteTo(p, &net.UDPAddr{})
			if err != nil {
				t.Fatal(err)
			}
			if count != len(p) {
				t.Fatal("unexpected count")
			}
		})

		t.Run("on failure", func(t *testing.T) {
			p := make([]byte, 128)
			expectedErr := io.EOF
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
						return 0, expectedErr
					},
				},
			}
			count, err := conn.WriteTo(p, &net.UDPAddr{})
			if err == nil || err.Error() != FailureEOFError {
				t.Fatal("unexpected err", err)
			}
			if count != 0 {
				t.Fatal("unexpected count")
			}
		})
	})

	t.Run("Close", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockClose: func() error {
						return nil
					},
				},
			}
			err := conn.Close()
			if err != nil {
				t.Fatal(err)
			}
		})

		t.Run("on failure", func(t *testing.T) {
			expectedErr := io.EOF
			conn := &quicErrWrapperUDPLikeConn{
				UDPLikeConn: &mocks.UDPLikeConn{
					MockClose: func() error {
						return expectedErr
					},
				},
			}
			err := conn.Close()
			if err == nil || err.Error() != FailureEOFError {
				t.Fatal("unexpected err", err)
			}
		})
	})
}

func TestQUICDialerErrWrapper(t *testing.T) {
	t.Run("CloseIdleConnections", func(t *testing.T) {
		var called bool
		d := &quicDialerErrWrapper{
			QUICDialer: &mocks.QUICDialer{
				MockCloseIdleConnections: func() {
					called = true
				},
			},
		}
		d.CloseIdleConnections()
		if !called {
			t.Fatal("not called")
		}
	})

	t.Run("DialContext", func(t *testing.T) {
		t.Run("on success", func(t *testing.T) {
			expectedConn := &mocks.QUICEarlyConnection{}
			d := &quicDialerErrWrapper{
				QUICDialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return expectedConn, nil
					},
				},
			}
			ctx := context.Background()
			qconn, err := d.DialContext(ctx, "", &tls.Config{}, &quic.Config{})
			if err != nil {
				t.Fatal(err)
			}
			if qconn != expectedConn {
				t.Fatal("unexpected connection")
			}
		})

		t.Run("on failure", func(t *testing.T) {
			expectedErr := io.EOF
			d := &quicDialerErrWrapper{
				QUICDialer: &mocks.QUICDialer{
					MockDialContext: func(ctx context.Context, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
						return nil, expectedErr
					},
				},
			}
			ctx := context.Background()
			qconn, err := d.DialContext(ctx, "", &tls.Config{}, &quic.Config{})
			if err == nil || err.Error() != FailureEOFError {
				t.Fatal("unexpected err", err)
			}
			if qconn != nil {
				t.Fatal("unexpected connection")
			}
		})
	})
}
