package geo

import (
	"net"
	"testing"

	"git.difuse.io/Difuse/Mellaris/ruleset/builtins/geo/v2geo"
)

func TestParseGeoSiteName(t *testing.T) {
	tests := []struct {
		input     string
		wantBase  string
		wantAttrs []string
	}{
		{"google", "google", nil},
		{"google@ads", "google", []string{"ads"}},
		{"google@ads@news", "google", []string{"ads", "news"}},
		{"  google  ", "google", nil},
		{" google @ ads ", "google", []string{"ads"}},
		{"openai@ ads @ news ", "openai", []string{"ads", "news"}},
		{"@onlyattrs", "", []string{"onlyattrs"}},
		{"", "", nil},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			base, attrs := parseGeoSiteName(tt.input)
			if base != tt.wantBase {
				t.Errorf("parseGeoSiteName(%q) base = %q, want %q", tt.input, base, tt.wantBase)
			}
			if len(attrs) != len(tt.wantAttrs) {
				t.Fatalf("parseGeoSiteName(%q) attrs len = %d, want %d", tt.input, len(attrs), len(tt.wantAttrs))
			}
			for i, attr := range attrs {
				if attr != tt.wantAttrs[i] {
					t.Errorf("parseGeoSiteName(%q) attrs[%d] = %q, want %q", tt.input, i, attr, tt.wantAttrs[i])
				}
			}
		})
	}
}

func TestHostInfo_String(t *testing.T) {
	h := HostInfo{
		Name: "example.com",
		IPv4: net.ParseIP("1.2.3.4"),
		IPv6: net.ParseIP("::1"),
	}
	want := "example.com|1.2.3.4|::1"
	if got := h.String(); got != want {
		t.Errorf("HostInfo.String() = %q, want %q", got, want)
	}
}

func TestHostInfo_String_Partial(t *testing.T) {
	h := HostInfo{
		Name: "test.com",
		IPv4: net.ParseIP("10.0.0.1"),
	}
	want := "test.com|10.0.0.1|<nil>"
	if got := h.String(); got != want {
		t.Errorf("HostInfo.String() = %q, want %q", got, want)
	}
}

func TestGeoipMatcher_Match(t *testing.T) {
	_, n4, _ := net.ParseCIDR("10.0.0.0/8")
	_, n4_2, _ := net.ParseCIDR("192.168.0.0/16")
	m := &geoipMatcher{
		N4: []*net.IPNet{n4, n4_2},
	}

	tests := []struct {
		name string
		host HostInfo
		want bool
	}{
		{"ipv4 match", HostInfo{IPv4: net.ParseIP("10.1.2.3")}, true},
		{"ipv4 no match", HostInfo{IPv4: net.ParseIP("172.16.0.1")}, false},
		{"ipv4 match second net", HostInfo{IPv4: net.ParseIP("192.168.1.1")}, true},
		{"no ip", HostInfo{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := m.Match(tt.host); got != tt.want {
				t.Errorf("Match() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGeoipMatcher_Match_Inverse(t *testing.T) {
	_, n4, _ := net.ParseCIDR("10.0.0.0/8")
	m := &geoipMatcher{
		N4:      []*net.IPNet{n4},
		Inverse: true,
	}

	if m.Match(HostInfo{IPv4: net.ParseIP("10.1.2.3")}) {
		t.Error("Inverse: inside range should return false")
	}
	if !m.Match(HostInfo{IPv4: net.ParseIP("172.16.0.1")}) {
		t.Error("Inverse: outside range should return true")
	}
	if !m.Match(HostInfo{}) {
		t.Error("Inverse: no IP should return true")
	}
}

func TestGeoipMatcher_Match_IPv6(t *testing.T) {
	_, n6, _ := net.ParseCIDR("2001:db8::/32")
	m := &geoipMatcher{
		N6: []*net.IPNet{n6},
	}

	if !m.Match(HostInfo{IPv6: net.ParseIP("2001:db8::1")}) {
		t.Error("IPv6 match failed")
	}
	if m.Match(HostInfo{IPv6: net.ParseIP("2001:db9::1")}) {
		t.Error("IPv6 should not match")
	}
}

func TestGeositeMatcher_matchDomain_Plain(t *testing.T) {
	m := &geositeMatcher{}
	d := geositeDomain{
		Type:  geositeDomainPlain,
		Value: "openai",
	}
	if !m.matchDomain(d, HostInfo{Name: "api.openai.com"}) {
		t.Error("plain domain should match via substring")
	}
	if m.matchDomain(d, HostInfo{Name: "google.com"}) {
		t.Error("plain domain should not match unrelated host")
	}
}

func TestGeositeMatcher_matchDomain_Full(t *testing.T) {
	m := &geositeMatcher{}
	d := geositeDomain{
		Type:  geositeDomainFull,
		Value: "example.com",
	}
	if !m.matchDomain(d, HostInfo{Name: "example.com"}) {
		t.Error("full domain should match exact")
	}
	if m.matchDomain(d, HostInfo{Name: "www.example.com"}) {
		t.Error("full domain should not match subdomain")
	}
}

func TestGeositeMatcher_matchDomain_Root(t *testing.T) {
	m := &geositeMatcher{}
	d := geositeDomain{
		Type:  geositeDomainRoot,
		Value: "example.com",
	}
	if !m.matchDomain(d, HostInfo{Name: "example.com"}) {
		t.Error("root domain should match exact")
	}
	if !m.matchDomain(d, HostInfo{Name: "www.example.com"}) {
		t.Error("root domain should match subdomain")
	}
	if m.matchDomain(d, HostInfo{Name: "www.example.com.au"}) {
		t.Error("root domain should not match unrelated suffix")
	}
}

func TestGeositeMatcher_matchDomain_Attrs(t *testing.T) {
	m := &geositeMatcher{Attrs: []string{"ads"}}
	d := geositeDomain{
		Type:  geositeDomainPlain,
		Value: "google",
		Attrs: map[string]bool{"ads": true},
	}
	if !m.matchDomain(d, HostInfo{Name: "google.com"}) {
		t.Error("should match when domain has required attr")
	}

	dNoAttrs := geositeDomain{
		Type:  geositeDomainPlain,
		Value: "google",
		Attrs: map[string]bool{},
	}
	if m.matchDomain(dNoAttrs, HostInfo{Name: "google.com"}) {
		t.Error("should not match when domain lacks required attr")
	}

	dOtherAttrs := geositeDomain{
		Type:  geositeDomainPlain,
		Value: "google",
		Attrs: map[string]bool{"news": true},
	}
	if m.matchDomain(dOtherAttrs, HostInfo{Name: "google.com"}) {
		t.Error("should not match when domain has wrong attr")
	}
}

func TestGeositeMatcher_Match(t *testing.T) {
	m := &geositeMatcher{
		Domains: []geositeDomain{
			{Type: geositeDomainFull, Value: "exact.com"},
			{Type: geositeDomainPlain, Value: "partial"},
		},
	}
	if !m.Match(HostInfo{Name: "exact.com"}) {
		t.Error("should match full domain")
	}
	if !m.Match(HostInfo{Name: "www.partial.net"}) {
		t.Error("should match partial domain")
	}
	if m.Match(HostInfo{Name: "other.net"}) {
		t.Error("should not match unrelated host")
	}
}

func TestDomainAttributeToMap(t *testing.T) {
	attrs := []*v2geo.Domain_Attribute{
		{Key: "ads"},
		{Key: "news"},
	}
	got := domainAttributeToMap(attrs)
	if len(got) != 2 || !got["ads"] || !got["news"] {
		t.Errorf("domainAttributeToMap = %v, want {ads:true, news:true}", got)
	}

	got2 := domainAttributeToMap(nil)
	if len(got2) != 0 {
		t.Errorf("domainAttributeToMap(nil) = %v, want empty map", got2)
	}
}

func TestNewGeoIPMatcher(t *testing.T) {
	list := &v2geo.GeoIP{
		Cidr: []*v2geo.CIDR{
			{Ip: net.IPv4(10, 0, 0, 0).To4(), Prefix: 8},
			{Ip: net.IPv4(192, 168, 0, 0).To4(), Prefix: 16},
		},
		InverseMatch: false,
	}
	m, err := newGeoIPMatcher(list)
	if err != nil {
		t.Fatalf("newGeoIPMatcher error: %v", err)
	}
	if len(m.N4) != 2 {
		t.Errorf("expected 2 IPv4 nets, got %d", len(m.N4))
	}
	if m.Inverse {
		t.Error("Inverse should be false")
	}
	// Verify sorted order: 10.0.0.0/8 < 192.168.0.0/16
	if m.N4[0].IP.String() != "10.0.0.0" {
		t.Errorf("N4[0] = %s, want 10.0.0.0", m.N4[0].IP)
	}
	if m.N4[1].IP.String() != "192.168.0.0" {
		t.Errorf("N4[1] = %s, want 192.168.0.0", m.N4[1].IP)
	}
}

func TestNewGeoIPMatcher_IPv6(t *testing.T) {
	list := &v2geo.GeoIP{
		Cidr: []*v2geo.CIDR{
			{Ip: net.ParseIP("2001:db8::"), Prefix: 32},
		},
	}
	m, err := newGeoIPMatcher(list)
	if err != nil {
		t.Fatalf("newGeoIPMatcher error: %v", err)
	}
	if len(m.N6) != 1 {
		t.Errorf("expected 1 IPv6 net, got %d", len(m.N6))
	}
}

func TestNewGeoIPMatcher_InvalidIPLength(t *testing.T) {
	list := &v2geo.GeoIP{
		Cidr: []*v2geo.CIDR{
			{Ip: []byte{1, 2, 3}, Prefix: 24},
		},
	}
	_, err := newGeoIPMatcher(list)
	if err == nil {
		t.Error("expected error for invalid IP length")
	}
}

func TestNewGeositeMatcher(t *testing.T) {
	list := &v2geo.GeoSite{
		Domain: []*v2geo.Domain{
			{Type: v2geo.Domain_Plain, Value: "google"},
			{Type: v2geo.Domain_Full, Value: "exact.com"},
		},
	}
	m, err := newGeositeMatcher(list, nil)
	if err != nil {
		t.Fatalf("newGeositeMatcher error: %v", err)
	}
	if len(m.Domains) != 2 {
		t.Errorf("expected 2 domains, got %d", len(m.Domains))
	}
}

func TestNewGeositeMatcher_WithAttrs(t *testing.T) {
	list := &v2geo.GeoSite{
		Domain: []*v2geo.Domain{
			{
				Type:  v2geo.Domain_RootDomain,
				Value: "google.com",
				Attribute: []*v2geo.Domain_Attribute{
					{Key: "ads"},
				},
			},
		},
	}
	m, err := newGeositeMatcher(list, []string{"ads"})
	if err != nil {
		t.Fatalf("newGeositeMatcher error: %v", err)
	}
	if !m.Match(HostInfo{Name: "www.google.com"}) {
		t.Error("should match with root domain and attr")
	}
}
