package ratelimit

import (
	"errors"
	"net"
	"strings"
	"testing"
	"time"
)

// cidrs parses CIDR strings into networks for a test fixture.
func cidrs(t *testing.T, ss ...string) []*net.IPNet {
	t.Helper()
	out := make([]*net.IPNet, 0, len(ss))
	for _, s := range ss {
		_, n, err := net.ParseCIDR(s)
		if err != nil {
			t.Fatalf("bad cidr %q: %v", s, err)
		}
		out = append(out, n)
	}
	return out
}

func TestBlocklistBlocked(t *testing.T) {
	bl := NewBlocklist(true, cidrs(t, "10.20.30.0/24")) // allowlist
	bl.SetCIDRs(cidrs(t, "1.2.3.0/24", "203.0.113.5/32", "198.51.100.0/28", "10.20.30.0/24"), time.Now())
	cases := map[string]bool{
		"1.2.3.0":       true,  // range start
		"1.2.3.255":     true,  // range end
		"1.2.4.0":       false, // just past the /24
		"203.0.113.5":   true,  // /32 exact
		"203.0.113.6":   false,
		"198.51.100.15": true,  // within /28
		"198.51.100.16": false, // past the /28
		"9.9.9.9":       false, // not listed
		"10.20.30.99":   false, // listed BUT allowlisted — never blocked
		"::1":           false, // IPv6 — never matched here
		"not-an-ip":     false,
	}
	for ip, want := range cases {
		if got := bl.Blocked(ip); got != want {
			t.Errorf("Blocked(%q) = %v, want %v", ip, got, want)
		}
	}
}

func TestBlocklistDisabledOrEmpty(t *testing.T) {
	off := NewBlocklist(false, nil)
	off.SetCIDRs(cidrs(t, "1.2.3.0/24"), time.Now())
	if off.Blocked("1.2.3.4") {
		t.Error("a disabled blocklist must not block")
	}
	empty := NewBlocklist(true, nil)
	if empty.Blocked("1.2.3.4") {
		t.Error("an empty (never-loaded) blocklist must not block")
	}
}

func TestParseDROP(t *testing.T) {
	in := "; Spamhaus DROP\n" +
		"1.2.3.0/24 ; SBL1\n" +
		"  198.51.100.0/28 ; SBL2\n" +
		"\n" +
		"203.0.113.5 ; a bare IP -> /32\n" +
		"2001:db8::/32 ; IPv6 skipped\n" +
		"garbage line\n"
	nets, err := ParseDROP(strings.NewReader(in))
	if err != nil {
		t.Fatal(err)
	}
	if len(nets) != 3 {
		t.Fatalf("got %d networks, want 3: %v", len(nets), nets)
	}
	bl := NewBlocklist(true, nil)
	bl.SetCIDRs(nets, time.Now())
	for ip, want := range map[string]bool{"1.2.3.9": true, "198.51.100.1": true, "203.0.113.5": true, "203.0.113.6": false, "2001:db8::1": false} {
		if got := bl.Blocked(ip); got != want {
			t.Errorf("Blocked(%s) = %v, want %v", ip, got, want)
		}
	}
}

func TestApplyRefresh(t *testing.T) {
	bl := NewBlocklist(true, nil)
	now := time.Now()

	if o := ApplyRefresh(bl, cidrs(t, "1.2.3.0/24"), nil, now, time.Hour); o != RefreshUpdated {
		t.Fatalf("success: want RefreshUpdated, got %v", o)
	}
	if !bl.Blocked("1.2.3.4") {
		t.Fatal("a successful refresh must apply the feed")
	}
	if o := ApplyRefresh(bl, nil, errors.New("net"), now.Add(30*time.Minute), time.Hour); o != RefreshKept {
		t.Fatalf("fresh failure: want RefreshKept, got %v", o)
	}
	if !bl.Blocked("1.2.3.4") {
		t.Error("a kept feed must still block")
	}
	if o := ApplyRefresh(bl, nil, errors.New("net"), now.Add(2*time.Hour), time.Hour); o != RefreshDropped {
		t.Fatalf("stale failure: want RefreshDropped, got %v", o)
	}
	if bl.Blocked("1.2.3.4") {
		t.Error("a stale feed must be dropped (fail-open)")
	}
}
