package geodata

import (
	"net"
	"path/filepath"
	"reflect"
	"slices"
	"testing"

	"github.com/xtls/xray-core/common"
	xnet "github.com/xtls/xray-core/common/net"
)

func buildIPMatcher(rawRules ...string) IPMatcher {
	rules, err := ParseIPRules(rawRules)
	common.Must(err)

	matcher, err := newIPRegistry().BuildIPMatcher(rules)
	common.Must(err)

	return matcher
}

func sortIPStrings(ips []net.IP) []string {
	output := make([]string, 0, len(ips))
	for _, ip := range ips {
		output = append(output, ip.String())
	}
	slices.Sort(output)
	return output
}

func TestIPMatcher(t *testing.T) {
	matcher := buildIPMatcher(
		"0.0.0.0/8",
		"10.0.0.0/8",
		"100.64.0.0/10",
		"127.0.0.0/8",
		"169.254.0.0/16",
		"172.16.0.0/12",
		"192.0.0.0/24",
		"192.0.2.0/24",
		"192.168.0.0/16",
		"192.18.0.0/15",
		"198.51.100.0/24",
		"203.0.113.0/24",
		"8.8.8.8/32",
		"91.108.4.0/16",
	)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "192.168.1.1",
			Output: true,
		},
		{
			Input:  "192.0.0.0",
			Output: true,
		},
		{
			Input:  "192.0.1.0",
			Output: false,
		},
		{
			Input:  "0.1.0.0",
			Output: true,
		},
		{
			Input:  "1.0.0.1",
			Output: false,
		},
		{
			Input:  "8.8.8.7",
			Output: false,
		},
		{
			Input:  "8.8.8.8",
			Output: true,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: false,
		},
		{
			Input:  "91.108.255.254",
			Output: true,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPMatcherFullCIDR4(t *testing.T) {
	matcher := buildIPMatcher(
		"0.0.0.0/0",
	)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "192.168.1.1",
			Output: true,
		},
		{
			Input:  "0.0.0.0",
			Output: true,
		},
		{
			Input:  "255.255.255.255",
			Output: true,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: false,
		},
		{
			Input:  "::0",
			Output: false,
		},
		{
			Input:  "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
			Output: false,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPMatcherFullCIDR6(t *testing.T) {
	matcher := buildIPMatcher(
		"::0/0",
	)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "192.168.1.1",
			Output: false,
		},
		{
			Input:  "0.0.0.0",
			Output: false,
		},
		{
			Input:  "255.255.255.255",
			Output: false,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: true,
		},
		{
			Input:  "::0",
			Output: true,
		},
		{
			Input:  "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
			Output: true,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPMatcherRegression(t *testing.T) {
	matcher := buildIPMatcher(
		"98.108.20.0/22",
		"98.108.20.0/23",
	)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "98.108.22.11",
			Output: true,
		},
		{
			Input:  "98.108.25.0",
			Output: false,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPReverseMatcher(t *testing.T) {
	matcher := buildIPMatcher(
		"8.8.8.8/32",
		"91.108.4.0/16",
	)
	matcher.SetReverse(true)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "8.8.8.8",
			Output: false,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: false,
		},
		{
			Input:  "91.108.255.254",
			Output: false,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPReverseMatcher2(t *testing.T) {
	matcher := buildIPMatcher(
		"8.8.8.8/32",
		"91.108.4.0/16",
		"fe80::", // Keep IPv6 family non-empty so reverse matching can evaluate IPv6 input.
	)
	matcher.SetReverse(true)

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "8.8.8.8",
			Output: false,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: true,
		},
		{
			Input:  "91.108.255.254",
			Output: false,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPCustomReverseMatcher(t *testing.T) {
	matcher := buildIPMatcher("!8.8.8.8/32")

	testCases := []struct {
		Input  string
		Output bool
	}{
		{
			Input:  "8.8.8.8",
			Output: false,
		},
		{
			Input:  "1.1.1.1",
			Output: true,
		},
		{
			Input:  "2001:cdba::3257:9652",
			Output: false,
		},
	}

	for _, test := range testCases {
		if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
			t.Error("unexpected output: ", v, " for test case ", test)
		}
	}
}

func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
	matcher := buildIPMatcher(
		"8.8.8.8/32",
		"2001:4860:4860::8888/128",
	)
	ip := func(raw string) net.IP {
		return xnet.ParseAddress(raw).IP()
	}

	if matcher.AnyMatch(nil) {
		t.Fatal("expect AnyMatch(nil) to be false")
	}

	if !matcher.AnyMatch([]net.IP{
		net.IP{},
		ip("1.1.1.1"),
		ip("8.8.8.8"),
	}) {
		t.Fatal("expect AnyMatch to ignore invalid IPs and return true when one valid IP matches")
	}

	if matcher.AnyMatch([]net.IP{
		ip("1.1.1.1"),
		ip("2001:db8::1"),
	}) {
		t.Fatal("expect AnyMatch to be false when no valid IP matches")
	}

	if !matcher.Matches([]net.IP{
		ip("8.8.8.8"),
		ip("2001:4860:4860::8888"),
	}) {
		t.Fatal("expect Matches to be true when all valid IPs match")
	}

	if matcher.Matches([]net.IP{
		ip("8.8.8.8"),
		ip("1.1.1.1"),
	}) {
		t.Fatal("expect Matches to be false when one valid IP does not match")
	}

	if matcher.Matches([]net.IP{
		ip("8.8.8.8"),
		net.IP{},
	}) {
		t.Fatal("expect Matches to be false when any IP is invalid")
	}
}

func TestIPMatcherFilterIPs(t *testing.T) {
	matcher := buildIPMatcher(
		"8.8.8.8/32",
		"91.108.4.0/16",
		"2001:4860:4860::8888/128",
	)
	ip := func(raw string) net.IP {
		return xnet.ParseAddress(raw).IP()
	}

	matched, unmatched := matcher.FilterIPs([]net.IP{
		net.IP{},
		ip("8.8.8.8"),
		ip("91.108.255.254"),
		ip("1.1.1.1"),
		ip("2001:4860:4860::8888"),
		ip("2001:db8::1"),
	})

	wantMatched := []string{
		"2001:4860:4860::8888",
		"8.8.8.8",
		"91.108.255.254",
	}
	slices.Sort(wantMatched)
	if v := sortIPStrings(matched); !reflect.DeepEqual(v, wantMatched) {
		t.Error("unexpected output: ", v, " want ", wantMatched)
	}

	wantUnmatched := []string{
		"1.1.1.1",
		"2001:db8::1",
	}
	slices.Sort(wantUnmatched)
	if v := sortIPStrings(unmatched); !reflect.DeepEqual(v, wantUnmatched) {
		t.Error("unexpected output: ", v, " want ", wantUnmatched)
	}
}

func TestIPMatcher4CN(t *testing.T) {
	t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))

	matcher := buildIPMatcher("geoip:cn")

	if matcher.Match([]byte{8, 8, 8, 8}) {
		t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
	}
}

func TestIPMatcher6US(t *testing.T) {
	t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))

	matcher := buildIPMatcher("geoip:us")

	if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
		t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
	}
}

func BenchmarkIPMatcher4CN(b *testing.B) {
	b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))

	matcher := buildIPMatcher("geoip:cn")
	ip := net.IP{8, 8, 8, 8}

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = matcher.Match(ip)
	}
}

func BenchmarkIPMatcher6US(b *testing.B) {
	b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))

	matcher := buildIPMatcher("geoip:us")
	ip := xnet.ParseAddress("2001:4860:4860::8888").IP()

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = matcher.Match(ip)
	}
}
