package filtering

import (
	"net"
	"net/netip"
	"testing"

	"github.com/AdguardTeam/golibs/testutil"
	"github.com/miekg/dns"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TODO(e.burkov): All the tests in this file may and should me merged together.

func TestRewrites(t *testing.T) {
	d, _ := newForTest(t, nil, nil)
	t.Cleanup(d.Close)

	var (
		addr1v4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})
		addr2v4 = netip.AddrFrom4([4]byte{1, 2, 3, 5})
		addr3v4 = netip.AddrFrom4([4]byte{1, 2, 3, 6})
		addr4v4 = netip.AddrFrom4([4]byte{1, 2, 3, 7})

		addr1v6 = netip.MustParseAddr("1:2:3::4")
		addr2v6 = netip.MustParseAddr("1234::5678")
	)

	d.conf.Rewrites = []*LegacyRewrite{{
		// This one and below are about CNAME, A and AAAA.
		Domain: "somecname",
		Answer: "somehost.com",
	}, {
		Domain: "somehost.com",
		Answer: netip.IPv4Unspecified().String(),
	}, {
		Domain: "host.com",
		Answer: addr1v4.String(),
	}, {
		Domain: "host.com",
		Answer: addr2v4.String(),
	}, {
		Domain: "host.com",
		Answer: addr1v6.String(),
	}, {
		Domain: "www.host.com",
		Answer: "host.com",
	}, {
		// This one is a wildcard.
		Domain: "*.host.com",
		Answer: addr2v4.String(),
	}, {
		// This one and below are about wildcard overriding.
		Domain: "a.host.com",
		Answer: addr1v4.String(),
	}, {
		// This one is about CNAME and wildcard interacting.
		Domain: "*.host2.com",
		Answer: "host.com",
	}, {
		// This one and below are about 2 level CNAME.
		Domain: "b.host.com",
		Answer: "somecname",
	}, {
		// This one and below are about 2 level CNAME and wildcard.
		Domain: "b.host3.com",
		Answer: "a.host3.com",
	}, {
		Domain: "a.host3.com",
		Answer: "x.host.com",
	}, {
		Domain: "*.hostboth.com",
		Answer: addr3v4.String(),
	}, {
		Domain: "*.hostboth.com",
		Answer: addr2v6.String(),
	}, {
		Domain: "BIGHOST.COM",
		Answer: addr4v4.String(),
	}, {
		Domain: "*.issue4016.com",
		Answer: "sub.issue4016.com",
	}, {
		Domain: "*.sub.issue6226.com",
		Answer: addr2v4.String(),
	}, {
		Domain: "*.issue6226.com",
		Answer: addr1v4.String(),
	}}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	require.NoError(t, d.prepareRewrites(ctx))

	testCases := []struct {
		name       string
		host       string
		wantCName  string
		wantIPs    []netip.Addr
		wantReason Reason
		dtyp       uint16
	}{{
		name:       "not_filtered_not_found",
		host:       "hoost.com",
		wantCName:  "",
		wantIPs:    nil,
		wantReason: NotFilteredNotFound,
		dtyp:       dns.TypeA,
	}, {
		name:       "rewritten_a",
		host:       "www.host.com",
		wantCName:  "host.com",
		wantIPs:    []netip.Addr{addr1v4, addr2v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "rewritten_aaaa",
		host:       "www.host.com",
		wantCName:  "host.com",
		wantIPs:    []netip.Addr{addr1v6},
		wantReason: Rewritten,
		dtyp:       dns.TypeAAAA,
	}, {
		name:       "wildcard_match",
		host:       "abc.host.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr2v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "wildcard_override",
		host:       "a.host.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr1v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "wildcard_cname_interaction",
		host:       "www.host2.com",
		wantCName:  "host.com",
		wantIPs:    []netip.Addr{addr1v4, addr2v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "two_cnames",
		host:       "b.host.com",
		wantCName:  "somehost.com",
		wantIPs:    []netip.Addr{netip.IPv4Unspecified()},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "two_cnames_and_wildcard",
		host:       "b.host3.com",
		wantCName:  "x.host.com",
		wantIPs:    []netip.Addr{addr2v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "issue3343",
		host:       "www.hostboth.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr2v6},
		wantReason: Rewritten,
		dtyp:       dns.TypeAAAA,
	}, {
		name:       "issue3351",
		host:       "bighost.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr4v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "issue4008",
		host:       "somehost.com",
		wantCName:  "",
		wantIPs:    nil,
		wantReason: Rewritten,
		dtyp:       dns.TypeHTTPS,
	}, {
		name:       "issue4016",
		host:       "www.issue4016.com",
		wantCName:  "sub.issue4016.com",
		wantIPs:    nil,
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "issue4016_self",
		host:       "sub.issue4016.com",
		wantCName:  "",
		wantIPs:    nil,
		wantReason: NotFilteredNotFound,
		dtyp:       dns.TypeA,
	}, {
		name:       "issue6226",
		host:       "www.issue6226.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr1v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}, {
		name:       "issue6226_sub",
		host:       "www.sub.issue6226.com",
		wantCName:  "",
		wantIPs:    []netip.Addr{addr2v4},
		wantReason: Rewritten,
		dtyp:       dns.TypeA,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			r := d.processRewrites(tc.host, tc.dtyp)
			require.Equalf(t, tc.wantReason, r.Reason, "got %s", r.Reason)

			if tc.wantCName != "" {
				assert.Equal(t, tc.wantCName, r.CanonName)
			}

			assert.Equal(t, tc.wantIPs, r.IPList)
		})
	}
}

func TestRewritesLevels(t *testing.T) {
	d, _ := newForTest(t, nil, nil)
	t.Cleanup(d.Close)
	// Exact host, wildcard L2, wildcard L3.
	d.conf.Rewrites = []*LegacyRewrite{{
		Domain: "host.com",
		Answer: "1.1.1.1",
		Type:   dns.TypeA,
	}, {
		Domain: "*.host.com",
		Answer: "2.2.2.2",
		Type:   dns.TypeA,
	}, {
		Domain: "*.sub.host.com",
		Answer: "3.3.3.3",
		Type:   dns.TypeA,
	}}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	require.NoError(t, d.prepareRewrites(ctx))

	testCases := []struct {
		name string
		host string
		want net.IP
	}{{
		name: "exact_match",
		host: "host.com",
		want: net.IP{1, 1, 1, 1},
	}, {
		name: "l2_match",
		host: "sub.host.com",
		want: net.IP{2, 2, 2, 2},
	}, {
		name: "l3_match",
		host: "my.sub.host.com",
		want: net.IP{3, 3, 3, 3},
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			r := d.processRewrites(tc.host, dns.TypeA)
			assert.Equal(t, Rewritten, r.Reason)
			require.Len(t, r.IPList, 1)
		})
	}
}

func TestRewritesExceptionCNAME(t *testing.T) {
	d, _ := newForTest(t, nil, nil)
	t.Cleanup(d.Close)
	// Wildcard and exception for a sub-domain.
	d.conf.Rewrites = []*LegacyRewrite{{
		Domain: "*.host.com",
		Answer: "2.2.2.2",
	}, {
		Domain: "sub.host.com",
		Answer: "sub.host.com",
	}, {
		Domain: "*.sub.host.com",
		Answer: "*.sub.host.com",
	}}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	require.NoError(t, d.prepareRewrites(ctx))

	testCases := []struct {
		name string
		host string
		want netip.Addr
	}{{
		name: "match_subdomain",
		host: "my.host.com",
		want: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
	}, {
		name: "exception_cname",
		host: "sub.host.com",
		want: netip.Addr{},
	}, {
		name: "exception_wildcard",
		host: "my.sub.host.com",
		want: netip.Addr{},
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			r := d.processRewrites(tc.host, dns.TypeA)
			if tc.want == (netip.Addr{}) {
				assert.Equal(t, NotFilteredNotFound, r.Reason, "got %s", r.Reason)

				return
			}

			assert.Equal(t, Rewritten, r.Reason)
			require.Len(t, r.IPList, 1)
			assert.Equal(t, tc.want, r.IPList[0])
		})
	}
}

func TestRewritesExceptionIP(t *testing.T) {
	d, _ := newForTest(t, nil, nil)
	t.Cleanup(d.Close)
	// Exception for AAAA record.
	d.conf.Rewrites = []*LegacyRewrite{{
		Domain: "host.com",
		Answer: "1.2.3.4",
		Type:   dns.TypeA,
	}, {
		Domain: "host.com",
		Answer: "AAAA",
		Type:   dns.TypeAAAA,
	}, {
		Domain: "host2.com",
		Answer: "::1",
		Type:   dns.TypeAAAA,
	}, {
		Domain: "host2.com",
		Answer: "A",
		Type:   dns.TypeA,
	}, {
		Domain: "host3.com",
		Answer: "A",
		Type:   dns.TypeA,
	}}

	ctx := testutil.ContextWithTimeout(t, testTimeout)
	require.NoError(t, d.prepareRewrites(ctx))

	testCases := []struct {
		name       string
		host       string
		want       []netip.Addr
		dtyp       uint16
		wantReason Reason
	}{{
		name:       "match_A",
		host:       "host.com",
		want:       []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
		dtyp:       dns.TypeA,
		wantReason: Rewritten,
	}, {
		name:       "exception_AAAA_host.com",
		host:       "host.com",
		want:       nil,
		dtyp:       dns.TypeAAAA,
		wantReason: NotFilteredNotFound,
	}, {
		name:       "exception_A_host2.com",
		host:       "host2.com",
		want:       nil,
		dtyp:       dns.TypeA,
		wantReason: NotFilteredNotFound,
	}, {
		name:       "match_AAAA_host2.com",
		host:       "host2.com",
		want:       []netip.Addr{netip.MustParseAddr("::1")},
		dtyp:       dns.TypeAAAA,
		wantReason: Rewritten,
	}, {
		name:       "exception_A_host3.com",
		host:       "host3.com",
		want:       nil,
		dtyp:       dns.TypeA,
		wantReason: NotFilteredNotFound,
	}, {
		name:       "match_AAAA_host3.com",
		host:       "host3.com",
		want:       nil,
		dtyp:       dns.TypeAAAA,
		wantReason: Rewritten,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			if tc.name != "match_AAAA_host3.com" {
				t.SkipNow()
			}

			r := d.processRewrites(tc.host, tc.dtyp)
			assert.Equal(t, tc.want, r.IPList)
			assert.Equal(t, tc.wantReason, r.Reason)
		})
	}
}
