package dnsresolver

import (
	"net"
	"net/url"
	"testing"

	"github.com/miekg/dns"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
	"google.golang.org/grpc/resolver"
)

func TestBuildDNSBuilder_withBuiltInResolver(t *testing.T) {
	t.Parallel()

	for _, tc := range []struct {
		desc   string
		target string
	}{
		{desc: "with triple slashes", target: "dns:///localhost:50051"},
		{desc: "without triple slashes", target: "dns:localhost:50051"},
	} {
		tc := tc
		t.Run(tc.desc, func(t *testing.T) {
			t.Parallel()

			// Actually resolve with built-in resolver. Localhost may return different results
			// between machines.
			expectedIPs, err := net.DefaultResolver.LookupHost(testhelper.Context(t), "localhost")
			require.NoError(t, err)

			builder := newTestDNSBuilder(t)
			conn := newFakeClientConn(1, 0)

			targetURL, err := url.Parse(tc.target)
			require.NoError(t, err)

			r, err := builder.Build(resolver.Target{URL: *targetURL}, conn, resolver.BuildOptions{})
			require.NoError(t, err)
			defer r.Close()

			conn.Wait()

			// Compare the recorded state with real DNS resolution
			require.Equal(t, 1, len(conn.states))
			state := conn.states[0]

			var actualIPs []string
			for _, addr := range state.Addresses {
				host, port, err := net.SplitHostPort(addr.Addr)
				require.NoError(t, err)
				require.Equal(t, port, "50051")
				actualIPs = append(actualIPs, host)
			}
			require.ElementsMatch(t, expectedIPs, actualIPs)
		})
	}
}

func TestBuildDNSBuilder_customAuthorityResolver(t *testing.T) {
	t.Parallel()

	fakeServer := testhelper.NewFakeDNSServer(t).WithHandler(dns.TypeA, func(host string) []string {
		if host == "grpc.test." {
			return []string{"1.2.3.4"}
		}
		return nil
	}).Start()

	builder := NewBuilder(&BuilderConfig{
		RefreshRate: 0,
		Logger:      testhelper.NewDiscardingLogger(t),
		Backoff:     &fakeBackoff{},
	})

	conn := newFakeClientConn(1, 0)
	r, err := builder.Build(buildResolverTarget(fakeServer, "grpc.test:50051"), conn, resolver.BuildOptions{})
	require.NoError(t, err)
	defer r.Close()

	conn.Wait()
	require.Equal(t, []resolver.State{{Addresses: []resolver.Address{{
		Addr: "1.2.3.4:50051",
	}}}}, conn.states)
}

func TestBuildDNSBuilder_staticIPAddress(t *testing.T) {
	t.Parallel()

	for _, tc := range []struct {
		desc string
		addr string
	}{
		{
			desc: "IPv4",
			addr: "4.3.2.1:50051",
		},
		{
			desc: "Full IPv6",
			addr: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:50051",
		},
		{
			desc: "Shortened IPv6",
			addr: "[::3]:50051",
		},
	} {
		tc := tc
		t.Run(tc.desc, func(t *testing.T) {
			t.Parallel()

			fakeServer := testhelper.NewFakeDNSServer(t).WithHandler(dns.TypeA, func(_ string) []string {
				require.FailNow(t, "resolving IP address should not result in a real DNS resolution")
				return nil
			}).Start()

			builder := NewBuilder(&BuilderConfig{
				RefreshRate: 0,
				Logger:      testhelper.NewDiscardingLogger(t),
				Backoff:     &fakeBackoff{},
			})

			conn := newFakeClientConn(1, 0)
			r, err := builder.Build(buildResolverTarget(fakeServer, tc.addr), conn, resolver.BuildOptions{})
			require.NoError(t, err)
			defer r.Close()

			conn.Wait()
			require.Equal(t, []resolver.State{{Addresses: []resolver.Address{{
				Addr: tc.addr,
			}}}}, conn.states)

			require.IsType(t, &noopResolver{}, r, "building a resolver for IP address should return a no-op resolver")
		})
	}
}

func TestSchemeDNSBuilder(t *testing.T) {
	t.Parallel()

	d := &Builder{}
	require.Equal(t, d.Scheme(), "dns")
}
