// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The AILERON Gateway Authors

package network

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"net"
	"regexp"
	"syscall"
	"testing"
	"time"

	k "github.com/aileron-gateway/aileron-gateway/apis/kernel"
	"github.com/aileron-gateway/aileron-gateway/kernel/er"
	"github.com/aileron-gateway/aileron-gateway/kernel/testutil"
	"github.com/aileron-projects/go/zsyscall"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

// ServerCert is a self-signed server certification.
// This cert was generated by the command below and checked
// by the command `openssl x509 -text -in server.crt -noout`.
//   - openssl req -newkey rsa:4096 -nodes -keyout server.key -x509 -days 36500 -out server.crt -addext 'subjectAltName = IP:127.0.0.1' -subj '/CN=127.0.0.1'
var serverCert = `
-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIUNhWCv7RXQBkf51MlQC1Ul7nEBCYwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMCAXDTI0MDIyMTE1NTkxOFoYDzIxMjQw
MTI4MTU1OTE4WjAUMRIwEAYDVQQDDAkxMjcuMC4wLjEwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCyK9mMtXCE+N1BRbR6eHz9JRirOL/ccsZ32J9Oiz4L
OKYK3F3qG+dVHDsPBeMnuVbtzXKWoq5hK6zvZjxq3lvK1PLEc2jpytgDyAoc3Sdq
Q8kEN8JgPQSLJvhHTuGkIGeVUdnH6rxgy1G9hYa80spBQGkiaZ+ARoDWQeDkbIJv
3UzGVzd6OpKmZvs35LdXXFu4/8bGirawpGSYQDLi2B9f153aMdieFR7YBfvb2T4P
L/DqFbVGiLqmyCcKQPXXj280dCpJ8llhu6D9z7erNfQYE3klCymmR5/3iftN8E0Q
I3t/Ad18yDXdmdAt4jB4rN0YXRxpEmtRgYpOj7HM0iiROU4vba6G8V4xDAGGIQ6h
q4uNMMW9yC6Z6CJ4bbRvZWFfGBA3ElHF+zL1vpKBDylIxeJzKU7rWnNRVY+sSiaR
ajZh7MXb32qJy7HEbfrkc9hXHE2O86Yq9yHVhAI1gZ6YUshFxOuoXTjjfzZjZVi8
3vIMWPe1cilKEnv+nErfH2/J+3VYaU9Pr8sQqhXqJGcIShIwZnvtoqcqnNBAd7WC
AviGJD04r6NsIVFagJx9bZ9wtwDlhT4Qrthg1yQ0y92q5W8J/sVfj9y7LJJScdKs
s3cEpoH9f2xWtPeLRZZGM4Kj9EBC9p588mHneZdsfATE7NE78AnqXOMi1i+1XcNO
5QIDAQABo2QwYjAdBgNVHQ4EFgQUHeASzEifz8NRCCGNqatthYyzancwHwYDVR0j
BBgwFoAUHeASzEifz8NRCCGNqatthYyzancwDwYDVR0TAQH/BAUwAwEB/zAPBgNV
HREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQCNoPOI6rpOjtBkN3fWRgVT
cMVP9xbHANhwdvQPen5euRpngEibnbWWjmnAblGGlrBfF7/IWmTts1nc5SMjyed7
4AM7ENj7QSG//fN1riznVtSrFsQYJyU2BFAFcyBT72GYMzHhyJNYg4WnKl9TVRYw
6PLON/As1H/4jCaRha+hpvH4CkCd75zuwiHnPTdJuLm6U88WjTyt3L/5TUvdBybp
+OAoLlNwipO8heVoROTIPSetxfUqIYRegHTw9HZi0HJsDtPTrLJ6a/sp//eb9huS
UT8vGNgFy0kbJVl1ohG0m1jGKm3jC83X0CKZzTEYWsb6l0jqGUBS5JzLBuRvuLCq
6ZzpMZhtC66hZTRtSc4QoJf/FaMSpx3o2qFA+1tJTz052puw7CSxFbUJAmJ0Fs4Z
TFQBlBHE+qOdR5FlQUx81km4XkRh77cH8+p2Yp/PnksxvQRJ014bxglKsUKr8P3j
70CqJPN80/vqBU1blhvw4ctFu1R18IJGgvtWnpWKrHRw2H+pxIBq6uRPF0eXjJIW
drsp0RUtCaKjIfV0D6Fm7yDaeN9omEbS0EIW15Czn1LwZG+N6RU1Nx0hWWeGV99f
hb64a5WN4eXR4uAqAm65bH1ohs5MO8eXhMkijz3WghjjFYt6vXiAOLJwNNHTQR7C
JSs+6+YFJ+2bPxRX9CwW/g==
-----END CERTIFICATE-----
`

// serverKey is the signing key for the serverCert.
// This key was generated by the command below.
//   - openssl req -newkey rsa:4096 -nodes -keyout server.key -x509 -days 36500 -out server.crt -addext 'subjectAltName = IP:127.0.0.1' -subj '/CN=127.0.0.1'
var serverKey = `
-----BEGIN PRIVATE KEY-----
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
JYbIsn4f6ooL1VdpNpXOaAAr67EsHrc=
-----END PRIVATE KEY-----
`

// x509KeyPair is the X509 cert and key pair.
var x509KeyPair, _ = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))

// rootCAs returns root CAs corresponding to the
// x509KeyPair created above.
var rootCAs = func() *x509.CertPool {
	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM([]byte(serverCert))
	return pool
}

var (
	_ Dialer = struct{ *net.Dialer }{}
)

type recordTargetDialer struct {
	Dialer
	network string
	address string
}

func (d *recordTargetDialer) Dial(network string, address string) (net.Conn, error) {
	return d.DialContext(context.Background(), network, address)
}

func (d *recordTargetDialer) DialContext(ctx context.Context, network string, address string) (net.Conn, error) {
	d.network = network
	d.address = address
	if d.Dialer == nil {
		return nil, nil
	}
	return d.Dialer.DialContext(ctx, network, address)
}

func TestReplaceTargetDialer_Dial(t *testing.T) {
	type condition struct {
		dialer  *replaceTargetDialer
		network string // in
		address string // in
	}

	type action struct {
		network string // Out
		address string // Out
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()
	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"empty replaces",
			[]string{},
			[]string{},
			&condition{
				dialer: &replaceTargetDialer{
					fromNet:  []string{},
					fromAddr: []string{},
					toNet:    []string{},
					toAddr:   []string{},
				},
				network: "tcp",
				address: "example.com",
			},
			&action{
				network: "tcp",
				address: "example.com",
			},
		),
		gen(
			"match replace",
			[]string{},
			[]string{},
			&condition{
				dialer: &replaceTargetDialer{
					fromNet:  []string{"dummy", "tcp"},
					fromAddr: []string{"dummy", "example.com"},
					toNet:    []string{"dummy", "unix"},
					toAddr:   []string{"dummy", "/var/run/example.sock"},
				},
				network: "tcp",
				address: "example.com",
			},
			&action{
				network: "unix",
				address: "/var/run/example.sock",
			},
		),
		gen(
			"network not match",
			[]string{},
			[]string{},
			&condition{
				dialer: &replaceTargetDialer{
					fromNet:  []string{"dummy", "tcp"},
					fromAddr: []string{"dummy", "example.com"},
					toNet:    []string{"dummy", "unix"},
					toAddr:   []string{"dummy", "/var/run/example.sock"},
				},
				network: "udp",
				address: "example.com",
			},
			&action{
				network: "udp",
				address: "example.com",
			},
		),
		gen(
			"address not match",
			[]string{},
			[]string{},
			&condition{
				dialer: &replaceTargetDialer{
					fromNet:  []string{"dummy", "tcp"},
					fromAddr: []string{"dummy", "example.com"},
					toNet:    []string{"dummy", "unix"},
					toAddr:   []string{"dummy", "/var/run/example.sock"},
				},
				network: "tcp",
				address: "foobar.com",
			},
			&action{
				network: "tcp",
				address: "foobar.com",
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			record := &recordTargetDialer{}
			tt.C().dialer.Dialer = record

			tt.C().dialer.Dial(tt.C().network, tt.C().address)
			testutil.Diff(t, tt.A().network, record.network)
			testutil.Diff(t, tt.A().address, record.address)
		})
	}
}

func TestReplaceTargetDialer(t *testing.T) {
	type condition struct {
		replaces []string
	}

	type action struct {
		dialer     Dialer
		errPattern *regexp.Regexp
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()
	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"empty replaces",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{},
			},
			&action{
				dialer: &recordTargetDialer{},
			},
		),
		gen(
			"single valid replaces",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{
					"(tcp|localhost:80) (unix|/var/run/example.sock)",
				},
			},
			&action{
				dialer: &replaceTargetDialer{
					Dialer:   &recordTargetDialer{},
					fromNet:  []string{"tcp"},
					fromAddr: []string{"localhost:80"},
					toNet:    []string{"unix"},
					toAddr:   []string{"/var/run/example.sock"},
				},
			},
		),
		gen(
			"multiple valid replaces",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{
					"(tcp|localhost:80) (unix|/var/run/example.sock)",
					"(udp|localhost:81) (unixgram|@foobar)",
				},
			},
			&action{
				dialer: &replaceTargetDialer{
					Dialer:   &recordTargetDialer{},
					fromNet:  []string{"tcp", "udp"},
					fromAddr: []string{"localhost:80", "localhost:81"},
					toNet:    []string{"unix", "unixgram"},
					toAddr:   []string{"/var/run/example.sock", "@foobar"},
				},
			},
		),
		gen(
			"invalid format",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{
					"(tcp#example.com:80) (unix|/var/run/example.sock)",
				},
			},
			&action{
				dialer:     nil,
				errPattern: regexp.MustCompile(`^invalid replace pattern. Pattern must be`),
			},
		),
		gen(
			"duplicate replaces",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{
					"(tcp|localhost:80) (unix|/var/run/example.sock)",
					"(tcp|localhost:80) (unixgram|@foobar)",
				},
			},
			&action{
				dialer:     nil,
				errPattern: regexp.MustCompile(`^replace source duplication`),
			},
		),
		gen(
			"invalid address",
			[]string{},
			[]string{},
			&condition{
				replaces: []string{
					"(tcp|example.com) (unix|/var/run/example.sock)",
				},
			},
			&action{
				dialer:     nil,
				errPattern: regexp.MustCompile(`^address example.com: missing port in address`),
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			record := &recordTargetDialer{}
			d, err := newReplaceTargetDialer(tt.C().replaces, record)
			if tt.A().errPattern == nil {
				testutil.Diff(t, nil, err)
			} else {
				t.Log(err.Error())
				testutil.Diff(t, true, tt.A().errPattern.MatchString(err.Error()))
			}

			opts := []cmp.Option{
				cmp.AllowUnexported(replaceTargetDialer{}),
				cmp.AllowUnexported(recordTargetDialer{}),
			}
			testutil.Diff(t, tt.A().dialer, d, opts...)
		})
	}
}

func TestNetworkType(t *testing.T) {
	type condition struct {
		t []k.NetworkType
	}

	type action struct {
		t []string
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()
	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"http",
			[]string{},
			[]string{},
			&condition{
				t: []k.NetworkType{k.NetworkType_HTTP},
			},
			&action{
				t: []string{"tcp"},
			},
		),
		gen(
			"tcp",
			[]string{},
			[]string{},
			&condition{
				t: []k.NetworkType{
					k.NetworkType_TCP,
					k.NetworkType_TCP4,
					k.NetworkType_TCP6,
				},
			},
			&action{
				t: []string{"tcp", "tcp4", "tcp6"},
			},
		),
		gen(
			"udp",
			[]string{},
			[]string{},
			&condition{
				t: []k.NetworkType{
					k.NetworkType_UDP,
					k.NetworkType_UDP4,
					k.NetworkType_UDP6,
				},
			},
			&action{
				t: []string{"udp", "udp4", "udp6"},
			},
		),
		gen(
			"unix",
			[]string{},
			[]string{},
			&condition{
				t: []k.NetworkType{
					k.NetworkType_Unix,
					k.NetworkType_UnixGram,
					k.NetworkType_UnixPacket,
				},
			},
			&action{
				t: []string{"unix", "unixgram", "unixpacket"},
			},
		),
		gen(
			"unknown",
			[]string{},
			[]string{},
			&condition{
				t: []k.NetworkType{
					k.NetworkType(999),
				},
			},
			&action{
				t: []string{""},
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			for i := range tt.C().t {
				result := NetworkType(tt.C().t[i])
				testutil.Diff(t, tt.A().t[i], result)
			}
		})
	}
}

func TestResolveAddr(t *testing.T) {
	type condition struct {
		network string
		address string
	}

	type action struct {
		addr net.Addr
		err  error
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()
	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"ip",
			[]string{},
			[]string{},
			&condition{
				network: "ip:0",
				address: "127.0.0.1",
			},
			&action{
				addr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")},
			},
		),
		gen(
			"ip wo addr",
			[]string{},
			[]string{},
			&condition{
				network: "ip:0",
				address: "",
			},
			&action{
				addr: &net.IPAddr{IP: net.ParseIP("")},
			},
		),
		gen(
			"tcp",
			[]string{},
			[]string{},
			&condition{
				network: "tcp",
				address: "127.0.0.1:8080",
			},
			&action{
				addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
			},
		),
		gen(
			"tcp wo addr",
			[]string{},
			[]string{},
			&condition{
				network: "tcp",
				address: "",
			},
			&action{
				addr: &net.TCPAddr{IP: net.ParseIP(""), Port: 0},
			},
		),
		gen(
			"udp",
			[]string{},
			[]string{},
			&condition{
				network: "udp",
				address: "127.0.0.1:8080",
			},
			&action{
				addr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
			},
		),
		gen(
			"udp wo addr",
			[]string{},
			[]string{},
			&condition{
				network: "udp",
				address: "",
			},
			&action{
				addr: &net.UDPAddr{IP: net.ParseIP(""), Port: 0},
			},
		),
		gen(
			"unix",
			[]string{},
			[]string{},
			&condition{
				network: "unix",
				address: "@test",
			},
			&action{
				addr: &net.UnixAddr{Name: "@test", Net: "unix"},
			},
		),
		gen(
			"unix wo addr",
			[]string{},
			[]string{},
			&condition{
				network: "unix",
				address: "",
			},
			&action{
				addr: &net.UnixAddr{Name: "", Net: "unix"},
			},
		),
		gen(
			"empty",
			[]string{},
			[]string{},
			&condition{
				network: "",
				address: "",
			},
			&action{
				addr: nil,
			},
		),
		gen(
			"unknown",
			[]string{},
			[]string{},
			&condition{
				network: "unknown",
				address: "",
			},
			&action{
				addr: nil,
				err:  errors.New("invalid network `unknown`"),
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			addr, err := resolveAddr(tt.C().network, tt.C().address)
			testutil.Diff(t, tt.A().addr, addr)
			if tt.A().err == nil {
				testutil.Diff(t, nil, err)
			} else {
				testutil.Diff(t, tt.A().err.Error(), err.Error())
			}
		})
	}
}

func TestNewDialerFromSpec(t *testing.T) {
	type condition struct {
		spec *k.DialConfig
	}

	type action struct {
		dialer Dialer
		err    error
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()
	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"nil spec",
			[]string{},
			[]string{},
			&condition{
				spec: nil,
			},
			&action{
				dialer: nil,
				err:    nil,
			},
		),
		gen(
			"zero spec",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{},
			},
			&action{
				dialer: &net.Dialer{},
			},
		),
		gen(
			"with tls configs",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					TLSConfig: &k.TLSConfig{},
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{},
					Config: &tls.Config{
						RootCAs:      x509.NewCertPool(),
						ClientCAs:    x509.NewCertPool(),
						Certificates: []tls.Certificate{},
					},
				},
			},
		),
		gen(
			"with local networks",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					LocalAddress: "tcp://127.0.0.1:8080",
				},
			},
			&action{
				dialer: &net.Dialer{
					LocalAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
				},
			},
		),
		gen(
			"with replaces",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					ReplaceTargets: []string{
						`(tcp|localhost:80) (unix|/var/run/test.sock)`,
						`(udp|localhost:81) > (unix|@test)`,
					},
				},
			},
			&action{
				dialer: &replaceTargetDialer{
					fromNet:  []string{"tcp", "udp"},
					fromAddr: []string{"localhost:80", "localhost:81"},
					toNet:    []string{"unix", "unix"},
					toAddr:   []string{"/var/run/test.sock", "@test"},
					Dialer:   &net.Dialer{},
				},
			},
		),
		gen(
			"invalid replaces",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					ReplaceTargets: []string{
						`(tcp example.com:80) (unix|/var/run/test.sock)`,
					},
				},
			},
			&action{
				dialer: nil,
				err: &er.Error{
					Package:     ErrPkg,
					Type:        ErrTypeDialer,
					Description: ErrDscDialer,
				},
			},
		),
		gen(
			"with all dialer configs",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					LocalAddress:   "tcp://127.0.0.1:8080",
					ReplaceTargets: []string{`(tcp|localhost:80) (unix|/var/run/test.sock)`},
					Timeout:        1,
					FallbackDelay:  2,
					TLSConfig:      &k.TLSConfig{},
				},
			},
			&action{
				dialer: &replaceTargetDialer{
					fromNet:  []string{"tcp"},
					fromAddr: []string{"localhost:80"},
					toNet:    []string{"unix"},
					toAddr:   []string{"/var/run/test.sock"},
					Dialer: &tls.Dialer{
						NetDialer: &net.Dialer{
							Timeout:       1 * time.Millisecond,
							Deadline:      time.Time{},
							LocalAddr:     &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
							FallbackDelay: 2 * time.Millisecond,
						},
						Config: &tls.Config{
							RootCAs:      x509.NewCertPool(),
							ClientCAs:    x509.NewCertPool(),
							Certificates: []tls.Certificate{},
						},
					},
				},
			},
		),
		gen(
			"invalid tls spec",
			[]string{},
			[]string{},
			&condition{
				spec: &k.DialConfig{
					TLSConfig: &k.TLSConfig{
						ClientAuth: k.ClientAuthType(9999), // Invalid value.
					},
				},
			},
			&action{
				dialer: nil,
				err: &er.Error{
					Package:     ErrPkg,
					Type:        ErrTypeDialer,
					Description: ErrDscDialer,
				},
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			d, err := NewDialerFromSpec(tt.C().spec)
			testutil.Diff(t, tt.A().err, err, cmpopts.EquateErrors())

			opts := []cmp.Option{
				cmp.AllowUnexported(replaceTargetDialer{}),
				cmpopts.IgnoreUnexported(net.Dialer{}, tls.Dialer{}, tls.Config{}),
				cmpopts.IgnoreFields(tls.Config{}, "RootCAs", "ClientCAs"),
				cmp.Comparer(testutil.ComparePointer[func(string, string, syscall.RawConn) error]),
			}
			testutil.Diff(t, tt.A().dialer, d, opts...)
		})
	}
}

func TestNewDialer(t *testing.T) {
	type condition struct {
		c *DialConfig
	}

	type action struct {
		dialer Dialer
		err    error
	}

	tb := testutil.NewTableBuilder[*condition, *action]()
	tb.Name(t.Name())
	table := tb.Build()

	gen := testutil.NewCase[*condition, *action]
	testCases := []*testutil.Case[*condition, *action]{
		gen(
			"nil config",
			[]string{},
			[]string{},
			&condition{
				c: nil,
			},
			&action{
				dialer: nil,
				err: &er.Error{
					Package:     ErrPkg,
					Type:        ErrTypeDialer,
					Description: ErrDscDialer,
				},
			},
		),
		gen(
			"zero config",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{},
			},
			&action{
				dialer: &net.Dialer{},
			},
		),
		gen(
			"zero tls config",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					TLSConfig: &tls.Config{},
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{},
					Config:    &tls.Config{},
				},
			},
		),
		gen(
			"with local addr",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					LocalAddress: "tcp://127.0.0.2:8080",
				},
			},
			&action{
				dialer: &net.Dialer{
					LocalAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.2"), Port: 8080},
				},
			},
		),
		gen(
			"tls with local addr",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					TLSConfig:    &tls.Config{},
					LocalAddress: "unix://test.sock",
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{
						LocalAddr: &net.UnixAddr{Name: "test.sock", Net: "unix"},
					},
					Config: &tls.Config{},
				},
			},
		),
		gen(
			"invalid network",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					LocalAddress: "tcp://127.0.0.2", // Port missing.
				},
			},
			&action{
				dialer: nil,
				err: &er.Error{
					Package:     ErrPkg,
					Type:        ErrTypeDialer,
					Description: ErrDscDialer,
				},
			},
		),
		gen(
			"with side opts",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					Timeout:       2 * time.Second,
					FallbackDelay: 3 * time.Second,
				},
			},
			&action{
				dialer: &net.Dialer{
					Timeout:       2 * time.Second,
					FallbackDelay: 3 * time.Second,
				},
			},
		),
		gen(
			"tls with side opts",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					TLSConfig:     &tls.Config{},
					Timeout:       2 * time.Second,
					FallbackDelay: 3 * time.Second,
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{
						Timeout:       2 * time.Second,
						FallbackDelay: 3 * time.Second,
					},
					Config: &tls.Config{},
				},
			},
		),
		gen(
			"with socket opts",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					SockOption: &zsyscall.SockOption{
						SO:   &zsyscall.SockSOOption{ReceiveBuffer: 5000},
						IP:   &zsyscall.SockIPOption{TTL: 20},
						IPV6: &zsyscall.SockIPV6Option{},
						TCP:  &zsyscall.SockTCPOption{NoDelay: true},
					},
				},
			},
			&action{
				dialer: &net.Dialer{
					Control: (&zsyscall.SockOption{SO: &zsyscall.SockSOOption{KeepAlive: true}}).ControlFunc(zsyscall.SockOptSO),
				},
			},
		),
		gen(
			"tls with socket opts",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					TLSConfig: &tls.Config{},
					SockOption: &zsyscall.SockOption{
						SO:   &zsyscall.SockSOOption{ReceiveBuffer: 5000},
						IP:   &zsyscall.SockIPOption{TTL: 20},
						IPV6: &zsyscall.SockIPV6Option{},
						TCP:  &zsyscall.SockTCPOption{NoDelay: true},
					},
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{
						Control: (&zsyscall.SockOption{SO: &zsyscall.SockSOOption{KeepAlive: true}}).ControlFunc(zsyscall.SockOptSO),
					},
					Config: &tls.Config{},
				},
			},
		),
		gen(
			"with all dialer configs",
			[]string{},
			[]string{},
			&condition{
				c: &DialConfig{
					TLSConfig:     &tls.Config{},
					LocalAddress:  "tcp://127.0.0.2:8080",
					Timeout:       1 * time.Second,
					FallbackDelay: 2 * time.Second,
					SockOption: &zsyscall.SockOption{
						SO:   &zsyscall.SockSOOption{ReceiveBuffer: 5000},
						IP:   &zsyscall.SockIPOption{TTL: 20},
						IPV6: &zsyscall.SockIPV6Option{},
						TCP:  &zsyscall.SockTCPOption{NoDelay: true},
					},
				},
			},
			&action{
				dialer: &tls.Dialer{
					NetDialer: &net.Dialer{
						Timeout:       1 * time.Second,
						LocalAddr:     &net.TCPAddr{IP: net.ParseIP("127.0.0.2"), Port: 8080},
						FallbackDelay: 2 * time.Second,
						Control:       (&zsyscall.SockOption{SO: &zsyscall.SockSOOption{KeepAlive: true}}).ControlFunc(zsyscall.SockOptSO),
					},
					Config: &tls.Config{},
				},
			},
		),
	}

	testutil.Register(table, testCases...)

	for _, tt := range table.Entries() {
		tt := tt
		t.Run(tt.Name(), func(t *testing.T) {
			d, err := NewDialer(tt.C().c)
			testutil.Diff(t, tt.A().err, err, cmpopts.EquateErrors())

			opts := []cmp.Option{
				cmpopts.IgnoreUnexported(net.Dialer{}, tls.Dialer{}, tls.Config{}),
				cmp.Comparer(testutil.ComparePointer[func(string, string, syscall.RawConn) error]),
			}
			testutil.Diff(t, tt.A().dialer, d, opts...)
		})
	}
}
