package dnscrypt

import (
	"net"
	"os"
	"testing"
	"time"

	"github.com/ameshkov/dnsstamps"
	"github.com/miekg/dns"
	"github.com/stretchr/testify/require"
)

func TestParseStamp(t *testing.T) {
	// Google DoH
	stampStr := "sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA"
	stamp, err := dnsstamps.NewServerStampFromString(stampStr)

	if err != nil || stamp.ProviderName == "" {
		t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
	}

	require.Equal(t, stampStr, stamp.String())
	require.Equal(t, dnsstamps.StampProtoTypeDoH, stamp.Proto)
	require.Equal(t, "dns.google.com", stamp.ProviderName)
	require.Equal(t, "/experimental", stamp.Path)

	// AdGuard DNSCrypt
	stampStr = "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
	stamp, err = dnsstamps.NewServerStampFromString(stampStr)

	if err != nil || stamp.ProviderName == "" {
		t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
	}

	require.Equal(t, stampStr, stamp.String())
	require.Equal(t, dnsstamps.StampProtoTypeDNSCrypt, stamp.Proto)
	require.Equal(t, "2.dnscrypt.default.ns1.adguard.com", stamp.ProviderName)
	require.Equal(t, "", stamp.Path)
	require.Equal(t, "94.140.14.14:5443", stamp.ServerAddrStr)
	require.Equal(t, keySize, len(stamp.ServerPk))
}

func TestInvalidStamp(t *testing.T) {
	client := Client{}
	_, err := client.Dial("sdns://AQIAAAAAAAAAFDE")
	require.NotNil(t, err)
}

func TestTimeoutOnDialError(t *testing.T) {
	// AdGuard DNS pointing to a wrong IP
	stampStr := "sdns://AQIAAAAAAAAADDguOC44Ljg6NTQ0MyDRK0fyUtzywrv4mRCG6vec5EldixbIoMQyLlLKPzkIcyIyLmRuc2NyeXB0LmRlZmF1bHQubnMxLmFkZ3VhcmQuY29t"
	client := Client{Timeout: 300 * time.Millisecond}

	_, err := client.Dial(stampStr)
	require.NotNil(t, err)
	require.True(t, os.IsTimeout(err))
}

func TestTimeoutOnDialExchange(t *testing.T) {
	// AdGuard DNS
	stampStr := "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
	client := Client{Timeout: 300 * time.Millisecond}

	serverInfo, err := client.Dial(stampStr)
	require.NoError(t, err)

	// Point it to an IP where there's no DNSCrypt server
	serverInfo.ServerAddress = "8.8.8.8:5443"
	req := createTestMessage()

	// Do exchange
	_, err = client.Exchange(req, serverInfo)

	// Check error
	require.NotNil(t, err)
	require.ErrorIs(t, err, os.ErrDeadlineExceeded)
}

func TestFetchCertPublicResolvers(t *testing.T) {
	testCases := []struct {
		name     string
		stampStr string
	}{
		{
			name:     "AdGuard DNS",
			stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
		},
		{
			name:     "AdGuard DNS Family",
			stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
		},
		{
			name:     "AdGuard DNS Unfiltered",
			stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
		},
		{
			name:     "Cisco OpenDNS",
			stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
		},
		{
			name:     "Cisco OpenDNS Family Shield",
			stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
		},
		{
			name:     "Quad9",
			stampStr: "sdns://AQYAAAAAAAAAEzE0OS4xMTIuMTEyLjEwOjg0NDMgZ8hHuMh1jNEgJFVDvnVnRt803x2EwAuMRwNo34Idhj4ZMi5kbnNjcnlwdC1jZXJ0LnF1YWQ5Lm5ldA",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			stamp, err := dnsstamps.NewServerStampFromString(tc.stampStr)
			require.NoError(t, err)

			c := &Client{
				Net:     "udp",
				Timeout: time.Second * 5,
			}
			resolverInfo, err := c.DialStamp(stamp)
			require.NoError(t, err)
			require.NotNil(t, resolverInfo)
			require.True(t, resolverInfo.ResolverCert.VerifyDate())
			require.True(t, resolverInfo.ResolverCert.VerifySignature(stamp.ServerPk))
		})
	}
}

func TestExchangePublicResolvers(t *testing.T) {
	stamps := []struct {
		stampStr string
	}{
		{
			// AdGuard DNS
			stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
		},
		{
			// AdGuard DNS Family
			stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
		},
		{
			// AdGuard DNS Unfiltered
			stampStr: "sdns://AQMAAAAAAAAAEjk0LjE0MC4xNC4xNDA6NTQ0MyC16ETWuDo-PhJo62gfvqcN48X6aNvWiBQdvy7AZrLa-iUyLmRuc2NyeXB0LnVuZmlsdGVyZWQubnMxLmFkZ3VhcmQuY29t",
		},
		{
			// Cisco OpenDNS
			stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
		},
		{
			// Cisco OpenDNS Family Shield
			stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
		},
	}

	for _, test := range stamps {
		stamp, err := dnsstamps.NewServerStampFromString(test.stampStr)
		require.NoError(t, err)

		t.Run(stamp.ProviderName, func(t *testing.T) {
			checkDNSCryptServer(t, test.stampStr, "udp")
			checkDNSCryptServer(t, test.stampStr, "tcp")
		})
	}
}

func checkDNSCryptServer(t *testing.T, stampStr string, network string) {
	client := Client{Net: network, Timeout: 10 * time.Second}
	resolverInfo, err := client.Dial(stampStr)
	require.NoError(t, err)

	req := createTestMessage()

	reply, err := client.Exchange(req, resolverInfo)
	require.NoError(t, err)
	assertTestMessageResponse(t, reply)
}

func createTestMessage() *dns.Msg {
	req := dns.Msg{}
	req.Id = dns.Id()
	req.RecursionDesired = true
	req.Question = []dns.Question{
		{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
	}
	return &req
}

func assertTestMessageResponse(t require.TestingT, reply *dns.Msg) {
	require.NotNil(t, reply)
	require.Equal(t, 1, len(reply.Answer))
	a, ok := reply.Answer[0].(*dns.A)
	require.True(t, ok)
	require.Equal(t, net.IPv4(8, 8, 8, 8).To4(), a.A.To4())
}
