package services

import (
	"log/slog"
	"path/filepath"
	"testing"
	"time"
)

func TestBanServiceMatchesIPAndCIDR(t *testing.T) {
	bans := newTestBanService(t)
	now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
	ipBan, err := bans.createBan("203.0.113.5", "single IP", BanSourceManual, "test", now.Add(time.Hour), now)
	if err != nil {
		t.Fatalf("createBan IP returned error: %v", err)
	}
	cidrBan, err := bans.createBan("198.51.100.0/24", "CIDR", BanSourceManual, "test", now.Add(time.Hour), now)
	if err != nil {
		t.Fatalf("createBan CIDR returned error: %v", err)
	}

	if matched, ok, err := bans.Match("203.0.113.5", now); err != nil || !ok || matched.Ban.ID != ipBan.ID {
		t.Fatalf("Match IP = %+v, %v, %v", matched, ok, err)
	}
	if matched, ok, err := bans.Match("198.51.100.42", now); err != nil || !ok || matched.Ban.ID != cidrBan.ID {
		t.Fatalf("Match CIDR = %+v, %v, %v", matched, ok, err)
	}
	if _, ok, err := bans.Match("192.0.2.1", now); err != nil || ok {
		t.Fatalf("Match unrelated = %v, %v", ok, err)
	}
}

func TestBanServiceIgnoresExpiredAndUnbanned(t *testing.T) {
	bans := newTestBanService(t)
	now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
	expired, err := bans.createBan("203.0.113.6", "expired", BanSourceManual, "test", now.Add(time.Hour), now)
	if err != nil {
		t.Fatalf("createBan expired returned error: %v", err)
	}
	if _, ok, err := bans.Match("203.0.113.6", now.Add(2*time.Hour)); err != nil || ok {
		t.Fatalf("expired Match = %v, %v", ok, err)
	}
	active, err := bans.createBan("203.0.113.7", "active", BanSourceManual, "test", now.Add(time.Hour), now)
	if err != nil {
		t.Fatalf("createBan active returned error: %v", err)
	}
	if err := bans.Unban(active.ID, now.Add(time.Minute)); err != nil {
		t.Fatalf("Unban returned error: %v", err)
	}
	if _, ok, err := bans.Match("203.0.113.7", now.Add(2*time.Minute)); err != nil || ok {
		t.Fatalf("unbanned Match = %v, %v", ok, err)
	}
	if expired.Status(now.Add(2*time.Hour)) != "expired" {
		t.Fatalf("expired status = %q", expired.Status(now.Add(2*time.Hour)))
	}
}

func TestBanServiceAutoBanThresholdsAndDisabled(t *testing.T) {
	bans := newTestBanService(t)
	now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
	if result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now); err != nil || result.Enabled {
		t.Fatalf("disabled RecordAbuse = %+v, %v", result, err)
	}
	settings, err := bans.Settings()
	if err != nil {
		t.Fatalf("Settings returned error: %v", err)
	}
	settings.AutoBanEnabled = true
	if err := bans.UpdateSettings(settings); err != nil {
		t.Fatalf("UpdateSettings returned error: %v", err)
	}
	for i := 0; i < 2; i++ {
		result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(time.Duration(i)*time.Minute))
		if err != nil || result.Triggered {
			t.Fatalf("RecordAbuse before threshold = %+v, %v", result, err)
		}
	}
	result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(3*time.Minute))
	if err != nil || !result.Triggered || result.Ban.ID == "" {
		t.Fatalf("RecordAbuse threshold = %+v, %v", result, err)
	}
	again, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(4*time.Minute))
	if err != nil || !again.Triggered || again.Ban.ID != result.Ban.ID {
		t.Fatalf("RecordAbuse duplicate = %+v, %v", again, err)
	}
	records, err := bans.ListBans()
	if err != nil {
		t.Fatalf("ListBans returned error: %v", err)
	}
	if len(records) != 1 {
		t.Fatalf("ban count = %d, want 1", len(records))
	}
}

func TestBanServiceMaliciousPathRules(t *testing.T) {
	bans := newTestBanService(t)
	if pattern, err := bans.MaliciousPattern("/foo/.ENV"); err != nil || pattern == "" {
		t.Fatalf("MaliciousPattern .env = %q, %v", pattern, err)
	}
	if pattern, err := bans.MaliciousPattern("/static/.env"); err != nil || pattern != "" {
		t.Fatalf("MaliciousPattern static = %q, %v", pattern, err)
	}
	if err := bans.SaveRules([]string{"/custom-probe"}, time.Now().UTC()); err != nil {
		t.Fatalf("SaveRules returned error: %v", err)
	}
	if pattern, err := bans.MaliciousPattern("/x/CUSTOM-probe"); err != nil || pattern != "/custom-probe" {
		t.Fatalf("MaliciousPattern custom = %q, %v", pattern, err)
	}
}

func newTestBanService(t *testing.T) *BanService {
	t.Helper()
	root := t.TempDir()
	upload, err := NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default())
	if err != nil {
		t.Fatalf("NewUploadService returned error: %v", err)
	}
	t.Cleanup(func() {
		if err := upload.Close(); err != nil {
			t.Fatalf("Close returned error: %v", err)
		}
	})
	bans, err := NewBanService(upload.DB())
	if err != nil {
		t.Fatalf("NewBanService returned error: %v", err)
	}
	return bans
}
