package dnsforward

import (
	"cmp"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"net/netip"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"testing/fstest"
	"time"

	"github.com/AdguardTeam/AdGuardHome/internal/agh"
	"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
	"github.com/AdguardTeam/AdGuardHome/internal/aghos"
	"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
	"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
	"github.com/AdguardTeam/AdGuardHome/internal/client"
	"github.com/AdguardTeam/AdGuardHome/internal/filtering"
	"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
	"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
	"github.com/AdguardTeam/AdGuardHome/internal/schedule"
	"github.com/AdguardTeam/dnsproxy/proxy"
	"github.com/AdguardTeam/dnsproxy/upstream"
	"github.com/AdguardTeam/golibs/logutil/slogutil"
	"github.com/AdguardTeam/golibs/netutil"
	"github.com/AdguardTeam/golibs/testutil"
	"github.com/AdguardTeam/golibs/timeutil"
	"github.com/miekg/dns"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// testLogger is a logger used in tests.
var testLogger = slogutil.NewDiscardLogger()

// testTLSConfigProvider is an empty TLS config provider for tests.
var testTLSConfigProvider = aghtls.EmptyTLSConfigProvider{}

func TestMain(m *testing.M) {
	testutil.DiscardLogOutput(m)
}

// testTimeout is the common timeout for tests.
//
// TODO(a.garipov): Use more.
const testTimeout = 1 * time.Second

// testQuestionTarget is the common question target for tests.
//
// TODO(a.garipov): Use more.
const testQuestionTarget = "target.example"

const (
	tlsServerName     = "testdns.adguard.com"
	testMessagesCount = 10
)

// testClientAddrPort is the common net.Addr for tests.
//
// TODO(a.garipov): Use more.
var testClientAddrPort = netip.MustParseAddrPort("1.2.3.4:12345")

// type check
var _ ClientsContainer = (*clientsContainer)(nil)

// clientsContainer is a mock [ClientsContainer] implementation for tests.
type clientsContainer struct {
	OnCustomUpstreamConfig func(
		clientID string,
		cliAddr netip.Addr,
	) (conf *proxy.CustomUpstreamConfig)

	OnUpdateCommonUpstreamConfig func(conf *client.CommonUpstreamConfig)

	OnClearUpstreamCache func()
}

// CustomUpstreamConfig implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) CustomUpstreamConfig(
	clientID string,
	cliAddr netip.Addr,
) (conf *proxy.CustomUpstreamConfig) {
	return c.OnCustomUpstreamConfig(clientID, cliAddr)
}

// UpdateCommonUpstreamConfig implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig) {
	c.OnUpdateCommonUpstreamConfig(conf)
}

// ClearUpstreamCache implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) ClearUpstreamCache() {
	c.OnClearUpstreamCache()
}

// startDeferStop starts the server and stops it when the test ends.
//
// TODO(e.burkov):  Replace with [servicetest.RequireRun].
func startDeferStop(tb testing.TB, s *Server) {
	tb.Helper()

	err := s.Start(testutil.ContextWithTimeout(tb, testTimeout))
	require.NoError(tb, err)
	testutil.CleanupAndRequireSuccess(tb, func() (err error) {
		return s.Stop(testutil.ContextWithTimeout(tb, testTimeout))
	})
}

// applyEmptyClientFiltering is a helper function for tests with
// [filtering.Config] that does nothing.
func applyEmptyClientFiltering(_ string, _ netip.Addr, _ *filtering.Settings) {}

// emptyFilteringBlockedServices is a helper function that returns an empty
// filtering blocked services for tests.
func emptyFilteringBlockedServices() (bsvc *filtering.BlockedServices) {
	return &filtering.BlockedServices{
		Schedule: schedule.EmptyWeekly(),
	}
}

// createTestServer is a helper function that returns a properly initialized
// *Server for use in tests, given the provided parameters.  It also populates
// the filtering configuration with default parameters.
func createTestServer(
	tb testing.TB,
	filterConf *filtering.Config,
	forwardConf ServerConfig,
	tlsConfigProvider aghtls.TLSConfigProvider,
) (s *Server) {
	tb.Helper()

	filterConf.Logger = cmp.Or(filterConf.Logger, testLogger)

	rules := `||nxdomain.example.org
||NULL.example.org^
127.0.0.1	host.example.org
@@||whitelist.example.org^
||127.0.0.255`
	filters := []filtering.Filter{{
		ID:   0,
		Data: []byte(rules),
	}}

	filterConf.BlockedServices = cmp.Or(filterConf.BlockedServices, emptyFilteringBlockedServices())

	if filterConf.ApplyClientFiltering == nil {
		filterConf.ApplyClientFiltering = applyEmptyClientFiltering
	}

	f, err := filtering.New(filterConf, filters)
	require.NoError(tb, err)

	f.SetEnabled(true)

	dhcp := &testDHCP{
		OnEnabled:  func() (ok bool) { return false },
		OnHostByIP: func(ip netip.Addr) (host string) { return "" },
		OnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },
	}
	s, err = NewServer(DNSCreateParams{
		DHCPServer:        dhcp,
		DNSFilter:         f,
		PrivateNets:       netutil.SubnetSetFunc(netutil.IsLocallyServed),
		Logger:            testLogger,
		TLSConfigProvider: tlsConfigProvider,
	})
	require.NoError(tb, err)

	err = s.Prepare(testutil.ContextWithTimeout(tb, testTimeout), &forwardConf)
	require.NoError(tb, err)

	return s
}

func createServerTLSConfig(tb testing.TB) (*tls.Config, []byte, []byte) {
	tb.Helper()

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoErrorf(tb, err, "cannot generate RSA key: %s", err)

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	require.NoErrorf(tb, err, "failed to generate serial number: %s", err)

	notBefore := time.Now()
	notAfter := notBefore.Add(5 * 365 * timeutil.Day)

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"AdGuard Tests"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		IsCA:                  true,
	}
	template.DNSNames = append(template.DNSNames, tlsServerName)
	template.IPAddresses = append(template.IPAddresses, netutil.IPv4Localhost().AsSlice())

	derBytes, err := x509.CreateCertificate(
		rand.Reader,
		&template,
		&template,
		publicKey(privateKey),
		privateKey,
	)
	require.NoErrorf(tb, err, "failed to create certificate: %s", err)

	certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	keyPem := pem.EncodeToMemory(
		&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)},
	)

	cert, err := tls.X509KeyPair(certPem, keyPem)
	require.NoErrorf(tb, err, "failed to create certificate: %s", err)

	getCert := func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
		return &cert, nil
	}

	return &tls.Config{
		GetCertificate: getCert,
		ServerName:     tlsServerName,
		MinVersion:     tls.VersionTLS12,
	}, certPem, keyPem
}

func createTestTLS(
	tb testing.TB,
	tlsConf *TLSConfig,
) (s *Server, confProvider aghtls.TLSConfigProvider) {
	tb.Helper()

	tlsConfig, certPem, _ := createServerTLSConfig(tb)

	// Add our self-signed generated config to roots.
	roots := x509.NewCertPool()
	roots.AppendCertsFromPEM(certPem)

	tlsConfig.RootCAs = roots

	tlsConfProvider := &aghtest.TLSConfigProvider{}

	tlsConfProvider.OnTLSConfig = func() (conf *tls.Config) { return tlsConfig.Clone() }
	tlsConfProvider.OnRootCAs = func() (pool *x509.CertPool) { return roots }
	tlsConfProvider.OnHasIPAddrs = func() (ok bool) { return true }

	s = createTestServer(
		tb,
		&filtering.Config{
			BlockingMode: filtering.BlockingModeDefault,
		},
		ServerConfig{
			UDPListenAddrs: []*net.UDPAddr{{}},
			TCPListenAddrs: []*net.TCPAddr{{}},
			TLSConf:        tlsConf,
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		},
		tlsConfProvider,
	)

	err := s.Prepare(testutil.ContextWithTimeout(tb, testTimeout), &s.conf)
	require.NoErrorf(tb, err, "failed to prepare server: %s", err)

	return s, tlsConfProvider
}

const googleDomainName = "google-public-dns-a.google.com."

func createGoogleATestMessage() *dns.Msg {
	return createTestMessage(googleDomainName)
}

func newGoogleUpstream() (u upstream.Upstream) {
	return &aghtest.UpstreamMock{
		OnAddress: func() (addr string) { return "google.upstream.example" },
		OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
			return cmp.Or(
				aghtest.MatchedResponse(req, dns.TypeA, googleDomainName, "8.8.8.8"),
				new(dns.Msg).SetRcode(req, dns.RcodeNameError),
			), nil
		},
		OnClose: func() (err error) { return nil },
	}
}

func createTestMessage(host string) *dns.Msg {
	return &dns.Msg{
		MsgHdr: dns.MsgHdr{
			Id:               dns.Id(),
			RecursionDesired: true,
		},
		Question: []dns.Question{{
			Name:   host,
			Qtype:  dns.TypeA,
			Qclass: dns.ClassINET,
		}},
	}
}

func createTestMessageWithType(host string, qtype uint16) *dns.Msg {
	req := createTestMessage(host)
	req.Question[0].Qtype = qtype

	return req
}

// newResp returns the new DNS response with response code set to rcode, req
// used as request, and rrs added.
func newResp(rcode int, req *dns.Msg, ans []dns.RR) (resp *dns.Msg) {
	resp = (&dns.Msg{}).SetRcode(req, rcode)
	resp.RecursionAvailable = true
	resp.Compress = true
	resp.Answer = ans

	return resp
}

func assertGoogleAResponse(tb testing.TB, reply *dns.Msg) {
	tb.Helper()

	assertResponse(tb, reply, netip.AddrFrom4([4]byte{8, 8, 8, 8}))
}

func assertResponse(tb testing.TB, reply *dns.Msg, ip netip.Addr) {
	tb.Helper()

	if !assert.Len(tb, reply.Answer, 1, "wrong number of answers") {
		return
	}

	a, ok := reply.Answer[0].(*dns.A)
	if !assert.True(tb, ok, "wrong answer type instead of A: %T", reply.Answer[0]) {
		return
	}

	assert.Equal(tb, net.IP(ip.AsSlice()), a.A)
}

// sendTestMessagesAsync sends messages in parallel to check for race issues.
//
//lint:ignore U1000 it's called from the function which is skipped for now.
func sendTestMessagesAsync(tb testing.TB, conn *dns.Conn) {
	tb.Helper()

	wg := &sync.WaitGroup{}

	for range testMessagesCount {
		msg := createGoogleATestMessage()

		wg.Go(func() {
			err := conn.WriteMsg(msg)
			require.NoErrorf(tb, err, "cannot write message: %s", err)

			res, err := conn.ReadMsg()
			require.NoErrorf(tb, err, "cannot read response to message: %s", err)

			assertGoogleAResponse(tb, res)
		})
	}

	wg.Wait()

	if tb.Failed() {
		tb.FailNow()
	}
}

func sendTestMessages(tb testing.TB, conn *dns.Conn) {
	tb.Helper()

	for i := range testMessagesCount {
		req := createGoogleATestMessage()
		err := conn.WriteMsg(req)
		assert.NoErrorf(tb, err, "cannot write message #%d: %s", i, err)

		res, err := conn.ReadMsg()
		assert.NoErrorf(tb, err, "cannot read response to message #%d: %s", i, err)
		assertGoogleAResponse(tb, res)
	}
}

func TestServer(t *testing.T) {
	s := createTestServer(
		t,
		&filtering.Config{
			BlockingMode: filtering.BlockingModeDefault,
		},
		ServerConfig{
			UDPListenAddrs: []*net.UDPAddr{{}},
			TCPListenAddrs: []*net.TCPAddr{{}},
			TLSConf:        &TLSConfig{},
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		},
		testTLSConfigProvider,
	)
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
	startDeferStop(t, s)

	testCases := []struct {
		name  string
		net   string
		proto proxy.Proto
	}{{
		name:  "message_over_udp",
		net:   "",
		proto: proxy.ProtoUDP,
	}, {
		name:  "message_over_tcp",
		net:   "tcp",
		proto: proxy.ProtoTCP,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			addr := s.dnsProxy.Addr(tc.proto)
			client := dns.Client{Net: tc.net}

			reply, _, err := client.Exchange(createGoogleATestMessage(), addr.String())
			require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)

			assertGoogleAResponse(t, reply)
		})
	}
}

func TestServer_timeout(t *testing.T) {
	t.Run("custom", func(t *testing.T) {
		srvConf := &ServerConfig{
			UpstreamTimeout: testTimeout,
			TLSConf:         &TLSConfig{},
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		}

		s, err := NewServer(DNSCreateParams{
			DNSFilter:         createTestDNSFilter(t),
			Logger:            testLogger,
			TLSConfigProvider: testTLSConfigProvider,
		})
		require.NoError(t, err)

		err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), srvConf)
		require.NoError(t, err)

		assert.Equal(t, testTimeout, s.conf.UpstreamTimeout)
	})

	t.Run("default", func(t *testing.T) {
		s, err := NewServer(DNSCreateParams{
			DNSFilter:         createTestDNSFilter(t),
			Logger:            testLogger,
			TLSConfigProvider: testTLSConfigProvider,
		})
		require.NoError(t, err)

		s.conf.TLSConf = &TLSConfig{}
		s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
		s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{
			Enabled: false,
		}
		s.conf.Config.ClientsContainer = EmptyClientsContainer{}
		err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)
		require.NoError(t, err)

		assert.Equal(t, DefaultTimeout, s.conf.UpstreamTimeout)
	})
}

func TestServer_Prepare_fallbacks(t *testing.T) {
	srvConf := &ServerConfig{
		TLSConf: &TLSConfig{},
		Config: Config{
			FallbackDNS: []string{
				"#tls://1.1.1.1",
				"8.8.8.8",
			},
			UpstreamMode:     UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}

	s, err := NewServer(DNSCreateParams{
		Logger:            testLogger,
		TLSConfigProvider: testTLSConfigProvider,
	})
	require.NoError(t, err)

	err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), srvConf)
	require.NoError(t, err)
	require.NotNil(t, s.dnsProxy.Fallbacks)

	assert.Len(t, s.dnsProxy.Fallbacks.Upstreams, 1)
}

func TestServerWithProtectionDisabled(t *testing.T) {
	s := createTestServer(
		t,
		&filtering.Config{
			BlockingMode: filtering.BlockingModeDefault,
		},
		ServerConfig{
			UDPListenAddrs: []*net.UDPAddr{{}},
			TCPListenAddrs: []*net.TCPAddr{{}},
			TLSConf:        &TLSConfig{},
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		},
		testTLSConfigProvider,
	)

	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
	startDeferStop(t, s)

	// Message over UDP.
	req := createGoogleATestMessage()
	addr := s.dnsProxy.Addr(proxy.ProtoUDP)
	client := &dns.Client{}

	reply, _, err := client.Exchange(req, addr.String())
	require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
	assertGoogleAResponse(t, reply)
}

func TestDoTServer(t *testing.T) {
	s, tlsConfProvider := createTestTLS(t, &TLSConfig{
		TLSListenAddrs: []*net.TCPAddr{{}},
	})
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
	startDeferStop(t, s)

	tlsConfig := tlsConfProvider.TLSConfig()

	// Create a DNS-over-TLS client connection.
	addr := s.dnsProxy.Addr(proxy.ProtoTLS)
	conn, err := dns.DialWithTLS("tcp-tls", addr.String(), tlsConfig)
	require.NoErrorf(t, err, "cannot connect to the proxy: %s", err)

	sendTestMessages(t, conn)
}

func TestDoQServer(t *testing.T) {
	s, _ := createTestTLS(t, &TLSConfig{
		QUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}},
	})
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
	startDeferStop(t, s)

	// Create a DNS-over-QUIC upstream.
	addr := s.dnsProxy.Addr(proxy.ProtoQUIC)
	opts := &upstream.Options{
		Logger:             testLogger,
		InsecureSkipVerify: true,
	}
	u, err := upstream.AddressToUpstream(fmt.Sprintf("%s://%s", proxy.ProtoQUIC, addr), opts)
	require.NoError(t, err)

	// Send the test message.
	req := createGoogleATestMessage()
	res, err := u.Exchange(req)
	require.NoError(t, err)

	assertGoogleAResponse(t, res)
}

func TestServerRace(t *testing.T) {
	t.Skip("TODO(e.burkov): inspect the golibs/cache package for locks")

	filterConf := &filtering.Config{
		SafeBrowsingEnabled:   true,
		SafeBrowsingCacheSize: 1000,
		SafeSearchConf:        filtering.SafeSearchConfig{Enabled: true},
		SafeSearchCacheSize:   1000,
		ParentalCacheSize:     1000,
		CacheTime:             30,
	}
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			UpstreamDNS:  []string{"8.8.8.8:53", "8.8.4.4:53"},
		},
		ConfModifier:  agh.EmptyConfigModifier{},
		ServePlainDNS: true,
	}
	s := createTestServer(t, filterConf, forwardConf, testTLSConfigProvider)
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
	startDeferStop(t, s)

	// Message over UDP.
	addr := s.dnsProxy.Addr(proxy.ProtoUDP)
	conn, err := dns.Dial("udp", addr.String())
	require.NoErrorf(t, err, "cannot connect to the proxy: %s", err)

	sendTestMessagesAsync(t, conn)
}

func TestSafeSearch(t *testing.T) {
	const (
		googleSafeSearch = "forcesafesearch.google.com."
	)

	safeSearchConf := filtering.SafeSearchConfig{
		Enabled: true,
		Google:  true,
		Yandex:  true,
	}

	filterConf := &filtering.Config{
		Logger:              testLogger,
		BlockingMode:        filtering.BlockingModeDefault,
		ProtectionEnabled:   true,
		SafeSearchConf:      safeSearchConf,
		SafeSearchCacheSize: 1000,
		CacheTime:           30,
	}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	safeSearch, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{
		Logger:         testLogger,
		ServicesConfig: safeSearchConf,
		CacheSize:      filterConf.SafeSearchCacheSize,
		CacheTTL:       time.Minute * time.Duration(filterConf.CacheTime),
	})
	require.NoError(t, err)

	filterConf.SafeSearch = safeSearch
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(t, filterConf, forwardConf, testTLSConfigProvider)

	pt := testutil.NewPanicT(t)
	ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
		assert.Equal(pt, googleSafeSearch, req.Question[0].Name)

		return aghtest.MatchedResponse(req, dns.TypeA, googleSafeSearch, "1.2.3.4"), nil
	})
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}

	startDeferStop(t, s)
	addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()

	yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})

	testCases := []struct {
		host      string
		want      netip.Addr
		wantCNAME string
	}{{
		host:      "yandex.com.",
		want:      yandexIP,
		wantCNAME: "",
	}, {
		host:      "yandex.by.",
		want:      yandexIP,
		wantCNAME: "",
	}, {
		host:      "yandex.kz.",
		want:      yandexIP,
		wantCNAME: "",
	}, {
		host:      "yandex.ru.",
		want:      yandexIP,
		wantCNAME: "",
	}, {
		host:      "www.google.com.",
		want:      netip.Addr{},
		wantCNAME: "forcesafesearch.google.com.",
	}, {
		host:      "www.google.com.af.",
		want:      netip.Addr{},
		wantCNAME: "forcesafesearch.google.com.",
	}, {
		host:      "www.google.be.",
		want:      netip.Addr{},
		wantCNAME: "forcesafesearch.google.com.",
	}, {
		host:      "www.google.by.",
		want:      netip.Addr{},
		wantCNAME: "forcesafesearch.google.com.",
	}}

	for _, tc := range testCases {
		t.Run(tc.host, func(t *testing.T) {
			req := createTestMessage(tc.host)

			var reply *dns.Msg
			reply, err = dns.Exchange(req, addr)
			require.NoError(t, err)

			if tc.wantCNAME != "" {
				require.Len(t, reply.Answer, 2)

				cname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])
				assert.Equal(t, tc.wantCNAME, cname.Target)

				a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[1])
				assert.NotEmpty(t, a.A)
			} else {
				require.Len(t, reply.Answer, 1)

				a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])
				assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
			}
		})
	}
}

func TestInvalidRequest(t *testing.T) {
	s := createTestServer(
		t,
		&filtering.Config{
			BlockingMode: filtering.BlockingModeDefault,
		},
		ServerConfig{
			UDPListenAddrs: []*net.UDPAddr{{}},
			TCPListenAddrs: []*net.TCPAddr{{}},
			TLSConf:        &TLSConfig{},
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		},
		testTLSConfigProvider,
	)
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
	req := dns.Msg{
		MsgHdr: dns.MsgHdr{
			Id:               dns.Id(),
			RecursionDesired: true,
		},
	}

	// Send a DNS request without question.
	_, _, err := (&dns.Client{
		Timeout: testTimeout,
	}).Exchange(&req, addr)

	assert.NoErrorf(t, err, "got a response to an invalid query")
}

func TestBlockedRequest(t *testing.T) {
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(
		t,
		&filtering.Config{
			ProtectionEnabled: true,
			BlockingMode:      filtering.BlockingModeDefault,
		},
		forwardConf,
		testTLSConfigProvider,
	)
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// Default blocking.
	req := createTestMessage("nxdomain.example.org.")

	reply, err := dns.Exchange(req, addr.String())
	require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)

	assert.Equal(t, dns.RcodeSuccess, reply.Rcode)

	require.Len(t, reply.Answer, 1)
	assert.True(t, reply.Answer[0].(*dns.A).A.IsUnspecified())
}

func TestServerCustomClientUpstream(t *testing.T) {
	const defaultCacheSize = 1024 * 1024

	var upsCalledCounter atomic.Uint32

	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			CacheSize:    defaultCacheSize,
			UpstreamMode: UpstreamModeLoadBalance,
			EnableDNSSEC: true,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(
		t,
		&filtering.Config{BlockingMode: filtering.BlockingModeDefault},
		forwardConf,
		testTLSConfigProvider,
	)

	ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
		upsCalledCounter.Add(1)

		return cmp.Or(
			aghtest.MatchedResponse(req, dns.TypeA, "host", "192.168.0.1"),
			new(dns.Msg).SetRcode(req, dns.RcodeNameError),
		), nil
	})

	customUpsConf := proxy.NewCustomUpstreamConfig(
		&proxy.UpstreamConfig{
			Upstreams: []upstream.Upstream{ups},
		},
		true,
		defaultCacheSize,
		forwardConf.EDNSClientSubnet.Enabled,
	)

	s.conf.ClientsContainer = &clientsContainer{
		OnCustomUpstreamConfig: func(
			_ string,
			_ netip.Addr,
		) (conf *proxy.CustomUpstreamConfig) {
			return customUpsConf
		},
	}

	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()

	// Send test request.
	req := createTestMessage("host.")

	reply, err := dns.Exchange(req, addr)
	require.NoError(t, err)
	require.NotEmpty(t, reply.Answer)
	require.Len(t, reply.Answer, 1)

	assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
	assert.Equal(t, net.IP{192, 168, 0, 1}, reply.Answer[0].(*dns.A).A)
	assert.Equal(t, uint32(1), upsCalledCounter.Load())

	_, err = dns.Exchange(req, addr)
	require.NoError(t, err)
	assert.Equal(t, uint32(1), upsCalledCounter.Load())
}

// testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work.
var testCNAMEs = map[string][]string{
	"badhost.":               {"NULL.example.org."},
	"whitelist.example.org.": {"NULL.example.org."},
}

// testIPv4 is a map of names and IPv4s necessary for the TestUpstream work.
var testIPv4 = map[string][]net.IP{
	"NULL.example.org.": {{1, 2, 3, 4}},
	"example.org.":      {{127, 0, 0, 255}},
}

func TestBlockCNAMEProtectionEnabled(t *testing.T) {
	s := createTestServer(
		t,
		&filtering.Config{
			BlockingMode: filtering.BlockingModeDefault,
		},
		ServerConfig{
			UDPListenAddrs: []*net.UDPAddr{{}},
			TCPListenAddrs: []*net.TCPAddr{{}},
			TLSConf:        &TLSConfig{},
			Config: Config{
				UpstreamMode:     UpstreamModeLoadBalance,
				EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
				ClientsContainer: EmptyClientsContainer{},
			},
			ServePlainDNS: true,
		},
		testTLSConfigProvider,
	)
	testUpstm := &aghtest.Upstream{
		CName: testCNAMEs,
		IPv4:  testIPv4,
	}

	s.dnsProxy.UpstreamConfig = &proxy.UpstreamConfig{
		Upstreams: []upstream.Upstream{testUpstm},
	}
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// 'badhost' has a canonical name 'NULL.example.org' which should be
	// blocked by filters, but protection is disabled so it is not.
	req := createTestMessage("badhost.")

	reply, err := dns.Exchange(req, addr.String())
	require.NoError(t, err)

	assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
}

func TestBlockCNAME(t *testing.T) {
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(
		t,
		&filtering.Config{ProtectionEnabled: true, BlockingMode: filtering.BlockingModeDefault},
		forwardConf,
		testTLSConfigProvider,
	)
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
		&aghtest.Upstream{
			CName: testCNAMEs,
			IPv4:  testIPv4,
		},
	}
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()

	testCases := []struct {
		name string
		host string
		want bool
	}{{
		name: "block_request",
		host: "badhost.",
		// 'badhost' has a canonical name 'NULL.example.org' which is
		// blocked by filters: response is blocked.
		want: true,
	}, {
		name: "allowed",
		host: "whitelist.example.org.",
		// 'whitelist.example.org' has a canonical name
		// 'NULL.example.org' which is blocked by filters
		// but 'whitelist.example.org' is in a whitelist:
		// response isn't blocked.
		want: false,
	}, {
		name: "block_response",
		host: "example.org.",
		// 'example.org' has a canonical name 'cname1' with IP
		// 127.0.0.255 which is blocked by filters: response is blocked.
		want: true,
	}}

	for _, tc := range testCases {
		req := createTestMessage(tc.host)

		t.Run(tc.name, func(t *testing.T) {
			reply, err := dns.Exchange(req, addr)
			require.NoError(t, err)

			assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
			if tc.want {
				require.Len(t, reply.Answer, 1)

				ans := reply.Answer[0]
				a, ok := ans.(*dns.A)
				require.True(t, ok)

				assert.True(t, a.A.IsUnspecified())
			}
		})
	}
}

func TestClientRulesForCNAMEMatching(t *testing.T) {
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(
		t,
		&filtering.Config{BlockingMode: filtering.BlockingModeDefault},
		forwardConf,
		testTLSConfigProvider,
	)
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
		&aghtest.Upstream{
			CName: testCNAMEs,
			IPv4:  testIPv4,
		},
	}
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// 'badhost' has a canonical name 'NULL.example.org' which is blocked by
	// filters: response is blocked.
	req := dns.Msg{
		MsgHdr: dns.MsgHdr{
			Id: dns.Id(),
		},
		Question: []dns.Question{{
			Name:   "badhost.",
			Qtype:  dns.TypeA,
			Qclass: dns.ClassINET,
		}},
	}

	// However, in our case it should not be blocked as filtering is
	// disabled on the client level.
	reply, err := dns.Exchange(&req, addr.String())
	require.NoError(t, err)

	assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
}

func TestNullBlockedRequest(t *testing.T) {
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(
		t,
		&filtering.Config{ProtectionEnabled: true, BlockingMode: filtering.BlockingModeNullIP},
		forwardConf,
		testTLSConfigProvider,
	)
	startDeferStop(t, s)
	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// Nil filter blocking.
	req := dns.Msg{
		MsgHdr: dns.MsgHdr{
			Id:               dns.Id(),
			RecursionDesired: true,
		},
		Question: []dns.Question{{
			Name:   "NULL.example.org.",
			Qtype:  dns.TypeA,
			Qclass: dns.ClassINET,
		}},
	}

	reply, err := dns.Exchange(&req, addr.String())
	require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
	require.Lenf(
		t,
		reply.Answer,
		1,
		"dns server %s returned reply with wrong number of answers - %d",
		addr,
		len(reply.Answer),
	)
	a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])
	assert.Truef(
		t,
		a.A.IsUnspecified(),
		"dns server %s returned wrong answer instead of 0.0.0.0: %v",
		addr,
		a.A,
	)
}

func TestBlockedCustomIP(t *testing.T) {
	rules := "||nxdomain.example.org^\n||NULL.example.org^\n127.0.0.1	host.example.org\n@@||whitelist.example.org^\n||127.0.0.255\n"
	filters := []filtering.Filter{{
		ID:   0,
		Data: []byte(rules),
	}}

	f, err := filtering.New(&filtering.Config{
		Logger:               testLogger,
		ProtectionEnabled:    true,
		ApplyClientFiltering: applyEmptyClientFiltering,
		BlockedServices:      emptyFilteringBlockedServices(),
		BlockingMode:         filtering.BlockingModeCustomIP,
		BlockingIPv4:         netip.Addr{},
		BlockingIPv6:         netip.Addr{},
	}, filters)
	require.NoError(t, err)

	dhcp := &testDHCP{
		OnEnabled:  func() (ok bool) { return false },
		OnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },
		OnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },
	}
	s, err := NewServer(DNSCreateParams{
		DHCPServer:        dhcp,
		DNSFilter:         f,
		PrivateNets:       netutil.SubnetSetFunc(netutil.IsLocallyServed),
		Logger:            testLogger,
		TLSConfigProvider: testTLSConfigProvider,
	})
	require.NoError(t, err)

	conf := &ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamDNS:  []string{"8.8.8.8:53", "8.8.4.4:53"},
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}

	// Invalid BlockingIPv4.
	err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), conf)
	assert.Error(t, err)

	s.dnsFilter.SetBlockingMode(
		filtering.BlockingModeCustomIP,
		netip.AddrFrom4([4]byte{0, 0, 0, 1}),
		netip.MustParseAddr("::1"),
	)

	err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), conf)
	require.NoError(t, err)

	f.SetEnabled(true)
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	req := createTestMessageWithType("NULL.example.org.", dns.TypeA)
	reply, err := dns.Exchange(req, addr.String())
	require.NoError(t, err)

	require.Len(t, reply.Answer, 1)

	a, ok := reply.Answer[0].(*dns.A)
	require.True(t, ok)

	assert.True(t, net.IP{0, 0, 0, 1}.Equal(a.A))

	req = createTestMessageWithType("NULL.example.org.", dns.TypeAAAA)
	reply, err = dns.Exchange(req, addr.String())
	require.NoError(t, err)

	require.Len(t, reply.Answer, 1)

	a6, ok := reply.Answer[0].(*dns.AAAA)
	require.True(t, ok)

	assert.Equal(t, "::1", a6.AAAA.String())
}

func TestBlockedByHosts(t *testing.T) {
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}

	s := createTestServer(
		t,
		&filtering.Config{ProtectionEnabled: true, BlockingMode: filtering.BlockingModeDefault},
		forwardConf,
		testTLSConfigProvider,
	)
	startDeferStop(t, s)
	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// Hosts blocking.
	req := createTestMessage("host.example.org.")

	reply, err := dns.Exchange(req, addr.String())
	require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
	require.Lenf(
		t,
		reply.Answer,
		1,
		"dns server %s returned reply with wrong number of answers - %d",
		addr,
		len(reply.Answer),
	)
	a, ok := reply.Answer[0].(*dns.A)
	require.Truef(
		t,
		ok,
		"dns server %s returned wrong answer type instead of A: %v",
		addr,
		reply.Answer[0],
	)
	assert.Equalf(
		t,
		net.IP{127, 0, 0, 1},
		a.A,
		"dns server %s returned wrong answer instead of 8.8.8.8: %v",
		addr,
		a.A,
	)
}

func TestBlockedBySafeBrowsing(t *testing.T) {
	const (
		hostname  = "wmconvirus.narod.ru"
		cacheTime = 10 * time.Minute
		cacheSize = 10000
	)

	sbChecker := hashprefix.New(&hashprefix.Config{
		Logger:    testLogger,
		CacheTime: cacheTime,
		CacheSize: cacheSize,
		Upstream:  aghtest.NewBlockUpstream(hostname, true),
	})

	ans4, _ := aghtest.HostToIPs(hostname)

	filterConf := &filtering.Config{
		BlockingMode:          filtering.BlockingModeDefault,
		ProtectionEnabled:     true,
		SafeBrowsingEnabled:   true,
		SafeBrowsingChecker:   sbChecker,
		SafeBrowsingBlockHost: ans4.String(),
	}
	forwardConf := ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}
	s := createTestServer(t, filterConf, forwardConf, testTLSConfigProvider)
	startDeferStop(t, s)
	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	// SafeBrowsing blocking.
	req := createTestMessage(hostname + ".")

	reply, err := dns.Exchange(req, addr.String())
	require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
	require.Lenf(
		t,
		reply.Answer,
		1,
		"dns server %s returned reply with wrong number of answers - %d",
		addr,
		len(reply.Answer),
	)

	assertResponse(t, reply, ans4)
}

func TestRewrite(t *testing.T) {
	c := &filtering.Config{
		Logger:               testLogger,
		ApplyClientFiltering: applyEmptyClientFiltering,
		BlockedServices:      emptyFilteringBlockedServices(),
		BlockingMode:         filtering.BlockingModeDefault,
		Rewrites: []*filtering.LegacyRewrite{{
			Domain:  "test.com",
			Answer:  "1.2.3.4",
			Type:    dns.TypeA,
			Enabled: true,
		}, {
			Domain:  "alias.test.com",
			Answer:  "test.com",
			Type:    dns.TypeCNAME,
			Enabled: true,
		}, {
			Domain:  "my.alias.example.org",
			Answer:  "example.org",
			Type:    dns.TypeCNAME,
			Enabled: true,
		}},
		RewritesEnabled: true,
	}
	f, err := filtering.New(c, nil)
	require.NoError(t, err)

	f.SetEnabled(true)

	dhcp := &testDHCP{
		OnEnabled:  func() (ok bool) { return false },
		OnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },
		OnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },
	}
	s, err := NewServer(DNSCreateParams{
		DHCPServer:        dhcp,
		DNSFilter:         f,
		PrivateNets:       netutil.SubnetSetFunc(netutil.IsLocallyServed),
		Logger:            testLogger,
		TLSConfigProvider: testTLSConfigProvider,
	})
	require.NoError(t, err)

	assert.NoError(t, s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &ServerConfig{
		UDPListenAddrs: []*net.UDPAddr{{}},
		TCPListenAddrs: []*net.TCPAddr{{}},
		TLSConf:        &TLSConfig{},
		Config: Config{
			UpstreamDNS:  []string{"8.8.8.8:53"},
			UpstreamMode: UpstreamModeLoadBalance,
			EDNSClientSubnet: &EDNSClientSubnet{
				Enabled: false,
			},
			ClientsContainer: EmptyClientsContainer{},
		},
		ServePlainDNS: true,
	}))

	ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
		return cmp.Or(
			aghtest.MatchedResponse(req, dns.TypeA, "example.org", "4.3.2.1"),
			new(dns.Msg).SetRcode(req, dns.RcodeNameError),
		), nil
	})
	s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}
	startDeferStop(t, s)

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)

	subTestFunc := func(t *testing.T) {
		req := createTestMessageWithType("test.com.", dns.TypeA)
		reply, eerr := dns.Exchange(req, addr.String())
		require.NoError(t, eerr)

		require.Len(t, reply.Answer, 1)

		a, ok := reply.Answer[0].(*dns.A)
		require.True(t, ok)

		assert.True(t, net.IP{1, 2, 3, 4}.Equal(a.A))

		req = createTestMessageWithType("test.com.", dns.TypeAAAA)
		reply, eerr = dns.Exchange(req, addr.String())
		require.NoError(t, eerr)

		assert.Empty(t, reply.Answer)

		req = createTestMessageWithType("alias.test.com.", dns.TypeA)
		reply, eerr = dns.Exchange(req, addr.String())
		require.NoError(t, eerr)

		require.Len(t, reply.Answer, 2)

		assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
		assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))

		req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
		reply, eerr = dns.Exchange(req, addr.String())
		require.NoError(t, eerr)

		// The original question is restored.
		require.Len(t, reply.Question, 1)

		assert.Equal(t, "my.alias.example.org.", reply.Question[0].Name)

		require.Len(t, reply.Answer, 2)

		assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
		assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
	}

	for _, protect := range []bool{true, false} {
		val := protect
		conf := s.getDNSConfig(testutil.ContextWithTimeout(t, testTimeout))
		conf.ProtectionEnabled = &val
		s.setConfig(conf)

		t.Run(fmt.Sprintf("protection_is_%t", val), subTestFunc)
	}
}

func publicKey(priv any) any {
	switch k := priv.(type) {
	case *rsa.PrivateKey:
		return &k.PublicKey

	case *ecdsa.PrivateKey:
		return &k.PublicKey

	default:
		return nil
	}
}

// testDHCP is a mock implementation of the [DHCP] interface.
type testDHCP struct {
	OnHostByIP func(ip netip.Addr) (host string)
	OnIPByHost func(host string) (ip netip.Addr)
	OnEnabled  func() (ok bool)
}

// type check
var _ DHCP = (*testDHCP)(nil)

// HostByIP implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) HostByIP(ip netip.Addr) (host string) { return d.OnHostByIP(ip) }

// IPByHost implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) IPByHost(host string) (ip netip.Addr) { return d.OnIPByHost(host) }

// IsClientHost implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) Enabled() (ok bool) { return d.OnEnabled() }

func TestPTRResponseFromDHCPLeases(t *testing.T) {
	const localDomain = "lan"

	flt, err := filtering.New(&filtering.Config{
		Logger:               testLogger,
		ApplyClientFiltering: applyEmptyClientFiltering,
		BlockedServices:      emptyFilteringBlockedServices(),
		BlockingMode:         filtering.BlockingModeDefault,
	}, nil)
	require.NoError(t, err)

	s, err := NewServer(DNSCreateParams{
		DNSFilter: flt,
		DHCPServer: &testDHCP{
			OnEnabled:  func() (ok bool) { return true },
			OnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },
			OnHostByIP: func(ip netip.Addr) (host string) {
				return "myhost"
			},
		},
		PrivateNets:       netutil.SubnetSetFunc(netutil.IsLocallyServed),
		Logger:            testLogger,
		LocalDomain:       localDomain,
		TLSConfigProvider: testTLSConfigProvider,
	})
	require.NoError(t, err)

	s.conf.UDPListenAddrs = []*net.UDPAddr{{}}
	s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
	s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
	s.conf.TLSConf = &TLSConfig{}
	s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
	s.conf.Config.ClientsContainer = EmptyClientsContainer{}
	s.conf.Config.UpstreamMode = UpstreamModeLoadBalance

	err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)
	require.NoError(t, err)

	err = s.Start(testutil.ContextWithTimeout(t, testTimeout))
	require.NoError(t, err)
	t.Cleanup(func() { s.Close(testutil.ContextWithTimeout(t, testTimeout)) })

	addr := s.dnsProxy.Addr(proxy.ProtoUDP)
	req := createTestMessageWithType("34.12.168.192.in-addr.arpa.", dns.TypePTR)

	resp, err := dns.Exchange(req, addr.String())
	require.NoErrorf(t, err, "%s", addr)

	require.Len(t, resp.Answer, 1)

	ans := resp.Answer[0]
	assert.Equal(t, dns.TypePTR, ans.Header().Rrtype)
	assert.Equal(t, "34.12.168.192.in-addr.arpa.", ans.Header().Name)

	ptr := testutil.RequireTypeAssert[*dns.PTR](t, ans)

	assert.Equal(t, dns.Fqdn("myhost."+localDomain), ptr.Ptr)
}

func TestPTRResponseFromHosts(t *testing.T) {
	// Prepare test hosts file.

	const hostsFilename = "hosts"

	testFS := fstest.MapFS{
		hostsFilename: &fstest.MapFile{Data: []byte(`
		127.0.0.1   host # comment
		::1         localhost#comment
	`)},
	}

	dhcp := &testDHCP{
		OnEnabled:  func() (ok bool) { return false },
		OnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },
		OnHostByIP: func(ip netip.Addr) (host string) { return "" },
	}

	var eventsCalledCounter atomic.Uint32
	watcher := aghtest.NewFSWatcher()
	watcher.OnEvents = func() (e <-chan aghos.Event) {
		assert.Equal(t, uint32(1), eventsCalledCounter.Add(1))

		return nil
	}
	watcher.OnAdd = func(name string) (err error) {
		assert.Equal(t, filepath.Join(aghos.RootDir(), hostsFilename), name)

		return nil
	}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	hc, err := aghnet.NewHostsContainer(ctx, testLogger, testFS, watcher, hostsFilename)
	require.NoError(t, err)
	t.Cleanup(func() {
		assert.Equal(t, uint32(1), eventsCalledCounter.Load())
	})

	flt, err := filtering.New(&filtering.Config{
		Logger:               testLogger,
		ApplyClientFiltering: applyEmptyClientFiltering,
		BlockedServices:      emptyFilteringBlockedServices(),
		BlockingMode:         filtering.BlockingModeDefault,
		EtcHosts:             hc,
	}, nil)
	require.NoError(t, err)

	flt.SetEnabled(true)

	var s *Server
	s, err = NewServer(DNSCreateParams{
		DHCPServer:        dhcp,
		DNSFilter:         flt,
		PrivateNets:       netutil.SubnetSetFunc(netutil.IsLocallyServed),
		Logger:            testLogger,
		TLSConfigProvider: testTLSConfigProvider,
	})
	require.NoError(t, err)

	s.conf.UDPListenAddrs = []*net.UDPAddr{{}}
	s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
	s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
	s.conf.TLSConf = &TLSConfig{}
	s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
	s.conf.Config.ClientsContainer = EmptyClientsContainer{}
	s.conf.Config.UpstreamMode = UpstreamModeLoadBalance

	err = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)
	require.NoError(t, err)

	err = s.Start(testutil.ContextWithTimeout(t, testTimeout))
	require.NoError(t, err)
	t.Cleanup(func() { s.Close(testutil.ContextWithTimeout(t, testTimeout)) })

	subTestFunc := func(t *testing.T) {
		addr := s.dnsProxy.Addr(proxy.ProtoUDP)
		req := createTestMessageWithType("1.0.0.127.in-addr.arpa.", dns.TypePTR)

		resp, eerr := dns.Exchange(req, addr.String())
		require.NoError(t, eerr)

		require.Len(t, resp.Answer, 1)

		assert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype)
		assert.Equal(t, "1.0.0.127.in-addr.arpa.", resp.Answer[0].Header().Name)

		ptr, ok := resp.Answer[0].(*dns.PTR)
		require.True(t, ok)
		assert.Equal(t, "host.", ptr.Ptr)
	}

	for _, protect := range []bool{true, false} {
		val := protect
		conf := s.getDNSConfig(testutil.ContextWithTimeout(t, testTimeout))
		conf.ProtectionEnabled = &val
		s.setConfig(conf)

		t.Run(fmt.Sprintf("protection_is_%t", val), subTestFunc)
	}
}

func TestNewServer(t *testing.T) {
	// TODO(a.garipov): Consider moving away from the text-based error
	// checks and onto a more structured approach.
	testCases := []struct {
		name       string
		in         DNSCreateParams
		wantErrMsg string
	}{{
		name: "success",
		in: DNSCreateParams{
			Logger: testLogger,
		},
		wantErrMsg: "",
	}, {
		name: "success_local_tld",
		in: DNSCreateParams{
			Logger:      testLogger,
			LocalDomain: "mynet",
		},
		wantErrMsg: "",
	}, {
		name: "success_local_domain",
		in: DNSCreateParams{
			Logger:      testLogger,
			LocalDomain: "my.local.net",
		},
		wantErrMsg: "",
	}, {
		name: "bad_local_domain",
		in: DNSCreateParams{
			Logger:      testLogger,
			LocalDomain: "!!!",
		},
		wantErrMsg: `local domain: bad domain name "!!!": ` +
			`bad top-level domain name label "!!!": ` +
			`bad top-level domain name label rune '!'`,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := NewServer(tc.in)
			testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
		})
	}
}

// doubleTTL is a helper function that returns a clone of DNS PTR with appended
// copy of first answer record with doubled TTL.
func doubleTTL(msg *dns.Msg) (resp *dns.Msg) {
	if msg == nil {
		return nil
	}

	if len(msg.Answer) == 0 {
		return msg
	}

	rec := msg.Answer[0]
	ptr, ok := rec.(*dns.PTR)
	if !ok {
		return msg
	}

	clone := *ptr
	clone.Hdr.Ttl *= 2
	msg.Answer = append(msg.Answer, &clone)

	return msg
}

func TestServer_Exchange(t *testing.T) {
	const (
		onesHost        = "one.one.one.one"
		twosHost        = "two.two.two.two"
		localDomainHost = "local.domain"

		defaultTTL = time.Second * 60
	)

	var (
		onesIP  = netip.MustParseAddr("1.1.1.1")
		twosIP  = netip.MustParseAddr("2.2.2.2")
		localIP = netip.MustParseAddr("192.168.1.1")
	)

	pt := testutil.NewPanicT(t)

	onesRevExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())
	require.NoError(t, err)

	twosRevExtIPv4, err := netutil.IPToReversedAddr(twosIP.AsSlice())
	require.NoError(t, err)

	extUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		resp := cmp.Or(
			aghtest.MatchedResponse(req, dns.TypePTR, onesRevExtIPv4, dns.Fqdn(onesHost)),
			doubleTTL(
				aghtest.MatchedResponse(req, dns.TypePTR, twosRevExtIPv4, dns.Fqdn(twosHost)),
			),
			new(dns.Msg).SetRcode(req, dns.RcodeNameError),
		)

		require.NoError(pt, w.WriteMsg(resp))
	})
	upsAddr := aghtest.StartLocalhostUpstream(t, extUpsHdlr).String()

	revLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
	require.NoError(t, err)

	locUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		resp := cmp.Or(
			aghtest.MatchedResponse(req, dns.TypePTR, revLocIPv4, dns.Fqdn(localDomainHost)),
			new(dns.Msg).SetRcode(req, dns.RcodeNameError),
		)

		require.NoError(pt, w.WriteMsg(resp))
	})

	errUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		require.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)))
	})

	nonPtrHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		hash := sha256.Sum256([]byte("some-host"))
		resp := (&dns.Msg{
			Answer: []dns.RR{&dns.TXT{
				Hdr: dns.RR_Header{
					Name:   req.Question[0].Name,
					Rrtype: dns.TypeTXT,
					Class:  dns.ClassINET,
					Ttl:    60,
				},
				Txt: []string{hex.EncodeToString(hash[:])},
			}},
		}).SetReply(req)

		require.NoError(pt, w.WriteMsg(resp))
	})
	refusingHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		require.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused)))
	})

	zeroTTLHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
		resp := (&dns.Msg{
			Answer: []dns.RR{&dns.PTR{
				Hdr: dns.RR_Header{
					Name:   req.Question[0].Name,
					Rrtype: dns.TypePTR,
					Class:  dns.ClassINET,
					Ttl:    0,
				},
				Ptr: dns.Fqdn(localDomainHost),
			}},
		}).SetReply(req)

		require.NoError(pt, w.WriteMsg(resp))
	})

	testCases := []struct {
		req         netip.Addr
		wantErr     error
		locUpstream dns.Handler
		name        string
		want        string
		wantTTL     time.Duration
	}{{
		name:        "external_good",
		want:        onesHost,
		wantErr:     nil,
		locUpstream: nil,
		req:         onesIP,
		wantTTL:     defaultTTL,
	}, {
		name:        "local_good",
		want:        localDomainHost,
		wantErr:     nil,
		locUpstream: locUpsHdlr,
		req:         localIP,
		wantTTL:     defaultTTL,
	}, {
		name:        "upstream_error",
		want:        "",
		wantErr:     ErrRDNSFailed,
		locUpstream: errUpsHdlr,
		req:         localIP,
		wantTTL:     0,
	}, {
		name:        "empty_answer_error",
		want:        "",
		wantErr:     ErrRDNSNoData,
		locUpstream: locUpsHdlr,
		req:         netip.MustParseAddr("192.168.1.2"),
		wantTTL:     0,
	}, {
		name:        "invalid_answer",
		want:        "",
		wantErr:     ErrRDNSNoData,
		locUpstream: nonPtrHdlr,
		req:         localIP,
		wantTTL:     0,
	}, {
		name:        "refused",
		want:        "",
		wantErr:     ErrRDNSFailed,
		locUpstream: refusingHdlr,
		req:         localIP,
		wantTTL:     0,
	}, {
		name:        "longest_ttl",
		want:        twosHost,
		wantErr:     nil,
		locUpstream: nil,
		req:         twosIP,
		wantTTL:     defaultTTL * 2,
	}, {
		name:        "zero_ttl",
		want:        localDomainHost,
		wantErr:     nil,
		locUpstream: zeroTTLHdlr,
		req:         localIP,
		wantTTL:     0,
	}}

	for _, tc := range testCases {
		localUpsAddr := aghtest.StartLocalhostUpstream(t, tc.locUpstream).String()

		t.Run(tc.name, func(t *testing.T) {
			srv := createTestServer(
				t,
				&filtering.Config{
					BlockingMode: filtering.BlockingModeDefault,
				},
				ServerConfig{
					TLSConf: &TLSConfig{},
					Config: Config{
						UpstreamDNS:      []string{upsAddr},
						UpstreamMode:     UpstreamModeLoadBalance,
						EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
						ClientsContainer: EmptyClientsContainer{},
					},
					LocalPTRResolvers: []string{localUpsAddr},
					UsePrivateRDNS:    true,
					ServePlainDNS:     true,
				},
				testTLSConfigProvider,
			)

			ctx := testutil.ContextWithTimeout(t, testTimeout)
			host, ttl, eerr := srv.Exchange(ctx, tc.req)

			require.ErrorIs(t, eerr, tc.wantErr)
			assert.Equal(t, tc.want, host)
			assert.Equal(t, tc.wantTTL, ttl)
		})
	}

	t.Run("resolving_disabled", func(t *testing.T) {
		srv := createTestServer(
			t,
			&filtering.Config{
				BlockingMode: filtering.BlockingModeDefault,
			},
			ServerConfig{
				TLSConf: &TLSConfig{},
				Config: Config{
					UpstreamDNS:      []string{upsAddr},
					UpstreamMode:     UpstreamModeLoadBalance,
					EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
					ClientsContainer: EmptyClientsContainer{},
				},
				LocalPTRResolvers: []string{},
				ServePlainDNS:     true,
			},
			testTLSConfigProvider,
		)

		ctx := testutil.ContextWithTimeout(t, testTimeout)
		host, _, eerr := srv.Exchange(ctx, localIP)

		require.NoError(t, eerr)
		assert.Empty(t, host)
	})
}
