package rules

import (
	"testing"
	"time"

	"github.com/nexuscrm/sentry/internal/types"
)

// multiFakeHistory returns, for a given target time, the snapshot whose
// SnapshotTs is nearest to target within the same epoch.
type multiFakeHistory struct {
	snaps []types.Snapshot
}

func (m multiFakeHistory) FindClosest(target time.Time, epoch int64) (types.Snapshot, bool) {
	var best types.Snapshot
	var found bool
	var bestDelta time.Duration
	for _, s := range m.snaps {
		if s.EpochID != epoch {
			continue
		}
		d := s.SnapshotTs.Sub(target)
		if d < 0 {
			d = -d
		}
		if !found || d < bestDelta {
			best = s
			bestDelta = d
			found = true
		}
	}
	return best, found
}

// appSnap builds a snapshot with only AppHealth populated at ts/epoch.
func appSnap(ts time.Time, epoch int64, app types.AppHealth) types.Snapshot {
	return types.Snapshot{
		SnapshotTs: ts,
		EpochID:    epoch,
		App:        app,
	}
}

// --- goroutine_leak -------------------------------------------------------

func TestAppRule_GoroutineLeak(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
	oneHourAgo := now.Add(-1 * time.Hour)

	cases := []struct {
		name    string
		history []types.Snapshot
		cur     int
		want    bool
	}{
		{
			name: "below threshold +999",
			history: []types.Snapshot{
				appSnap(oneHourAgo, 1, types.AppHealth{Runtime: types.RuntimeStats{Goroutines: 500}}),
			},
			cur: 1499, want: false,
		},
		{
			name: "above threshold +1001",
			history: []types.Snapshot{
				appSnap(oneHourAgo, 1, types.AppHealth{Runtime: types.RuntimeStats{Goroutines: 500}}),
			},
			cur: 1501, want: true,
		},
		{
			name:    "cold start",
			history: nil,
			cur:     5000, want: false,
		},
	}

	rule := goroutineLeakRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cur := appSnap(now, 1, types.AppHealth{Runtime: types.RuntimeStats{Goroutines: tc.cur}})
			got := rule.Evaluate(cur, multiFakeHistory{snaps: tc.history})
			if (len(got) > 0) != tc.want {
				t.Fatalf("want fire=%v, got %v", tc.want, got)
			}
		})
	}
}

// --- heap_growth ----------------------------------------------------------

func TestAppRule_HeapGrowth(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
	twoHoursAgo := now.Add(-2 * time.Hour)

	cases := []struct {
		name    string
		history []types.Snapshot
		cur     uint64
		want    bool
	}{
		{
			name: "below threshold +99%",
			history: []types.Snapshot{
				appSnap(twoHoursAgo, 1, types.AppHealth{Runtime: types.RuntimeStats{HeapInuseMB: 100}}),
			},
			cur: 199, want: false,
		},
		{
			name: "above threshold +101%",
			history: []types.Snapshot{
				appSnap(twoHoursAgo, 1, types.AppHealth{Runtime: types.RuntimeStats{HeapInuseMB: 100}}),
			},
			cur: 201, want: true,
		},
		{
			name:    "cold start",
			history: nil,
			cur:     500, want: false,
		},
		{
			name: "baseline zero skip",
			history: []types.Snapshot{
				appSnap(twoHoursAgo, 1, types.AppHealth{Runtime: types.RuntimeStats{HeapInuseMB: 0}}),
			},
			cur: 100, want: false,
		},
	}

	rule := heapGrowthRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cur := appSnap(now, 1, types.AppHealth{Runtime: types.RuntimeStats{HeapInuseMB: tc.cur}})
			got := rule.Evaluate(cur, multiFakeHistory{snaps: tc.history})
			if (len(got) > 0) != tc.want {
				t.Fatalf("want fire=%v, got %v", tc.want, got)
			}
		})
	}
}

// --- db_pool_wait ---------------------------------------------------------

func TestAppRule_DBPoolWait(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
	tenMinAgo := now.Add(-10 * time.Minute)

	cases := []struct {
		name     string
		hist     int64
		cur      int64
		history  bool
		want     bool
	}{
		{name: "below threshold +500", hist: 1000, cur: 1500, history: true, want: false},
		{name: "above threshold +501", hist: 1000, cur: 1501, history: true, want: true},
		{name: "cold start", hist: 0, cur: 5000, history: false, want: false},
		{name: "counter reset current<hist", hist: 2000, cur: 100, history: true, want: false},
	}

	rule := dbPoolWaitRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var hist []types.Snapshot
			if tc.history {
				hist = []types.Snapshot{
					appSnap(tenMinAgo, 1, types.AppHealth{DBPool: types.DBPoolStats{WaitCount: tc.hist}}),
				}
			}
			cur := appSnap(now, 1, types.AppHealth{DBPool: types.DBPoolStats{WaitCount: tc.cur}})
			got := rule.Evaluate(cur, multiFakeHistory{snaps: hist})
			if (len(got) > 0) != tc.want {
				t.Fatalf("want fire=%v, got %v", tc.want, got)
			}
		})
	}
}

// --- panic_surge ----------------------------------------------------------

func TestAppRule_PanicSurge(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
	tenMinAgo := now.Add(-10 * time.Minute)

	cases := []struct {
		name    string
		hist    int64
		cur     int64
		history bool
		want    bool
	}{
		{name: "below threshold +20", hist: 5, cur: 25, history: true, want: false},
		{name: "above threshold +21", hist: 5, cur: 26, history: true, want: true},
		{name: "cold start", history: false, want: false},
		{name: "counter reset", hist: 100, cur: 5, history: true, want: false},
	}

	rule := panicSurgeRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var hist []types.Snapshot
			if tc.history {
				hist = []types.Snapshot{
					appSnap(tenMinAgo, 1, types.AppHealth{HTTP: types.HTTPStats{PanicCount: tc.hist}}),
				}
			}
			cur := appSnap(now, 1, types.AppHealth{HTTP: types.HTTPStats{PanicCount: tc.cur}})
			got := rule.Evaluate(cur, multiFakeHistory{snaps: hist})
			if (len(got) > 0) != tc.want {
				t.Fatalf("want fire=%v, got %v", tc.want, got)
			}
		})
	}
}

// --- http 5xx fraction rules ----------------------------------------------

func TestAppRule_HTTP5xx(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
	tenMinAgo := now.Add(-10 * time.Minute)

	cases := []struct {
		name      string
		histReq   int64
		histErr   int64
		curReq    int64
		curErr    int64
		history   bool
		wantWarn  bool
		wantCrit  bool
	}{
		{
			name: "2% fraction — neither",
			histReq: 10000, histErr: 100, curReq: 20000, curErr: 300,
			history: true,
			// delta: 10000 req, 200 err -> 2% -> neither
		},
		{
			name:    "5% fraction — warn only",
			histReq: 10000, histErr: 100, curReq: 20000, curErr: 600,
			history:  true,
			wantWarn: true,
		},
		{
			name:    "15% fraction — warn + crit",
			histReq: 10000, histErr: 100, curReq: 20000, curErr: 1600,
			history:  true,
			wantWarn: true, wantCrit: true,
		},
		{
			name:    "cold start",
			history: false,
		},
		{
			name:    "divide by zero (no new requests)",
			histReq: 10000, histErr: 100, curReq: 10000, curErr: 5000,
			history: true,
			// req delta = 0 -> skip
		},
		{
			name:    "counter reset on requests",
			histReq: 10000, histErr: 100, curReq: 500, curErr: 400,
			history: true,
		},
		{
			name:    "counter reset on errors",
			histReq: 10000, histErr: 500, curReq: 20000, curErr: 100,
			history: true,
		},
	}

	warnRule := http5xxWarnRule{}
	critRule := http5xxCritRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var hist []types.Snapshot
			if tc.history {
				hist = []types.Snapshot{
					appSnap(tenMinAgo, 1, types.AppHealth{HTTP: types.HTTPStats{
						RequestsTotal: tc.histReq,
						Error5xxCount: tc.histErr,
					}}),
				}
			}
			cur := appSnap(now, 1, types.AppHealth{HTTP: types.HTTPStats{
				RequestsTotal: tc.curReq,
				Error5xxCount: tc.curErr,
			}})
			h := multiFakeHistory{snaps: hist}
			w := warnRule.Evaluate(cur, h)
			c := critRule.Evaluate(cur, h)
			if (len(w) > 0) != tc.wantWarn {
				t.Errorf("warn: want=%v got=%v", tc.wantWarn, w)
			}
			if (len(c) > 0) != tc.wantCrit {
				t.Errorf("crit: want=%v got=%v", tc.wantCrit, c)
			}
		})
	}
}

// --- outbox_stall ---------------------------------------------------------

func TestAppRule_OutboxStall(t *testing.T) {
	now := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)

	// helper: build history snapshots at ~30, ~20, ~10 min ago with the given
	// pending values (oldest first in the arg list: h30, h20, h10).
	mkHist := func(h30, h20, h10 int64) []types.Snapshot {
		return []types.Snapshot{
			appSnap(now.Add(-30*time.Minute), 1, types.AppHealth{Outbox: types.OutboxStats{Pending: h30}}),
			appSnap(now.Add(-20*time.Minute), 1, types.AppHealth{Outbox: types.OutboxStats{Pending: h20}}),
			appSnap(now.Add(-10*time.Minute), 1, types.AppHealth{Outbox: types.OutboxStats{Pending: h10}}),
		}
	}

	cases := []struct {
		name    string
		history []types.Snapshot
		curPend int64
		want    bool
	}{
		{
			name:    "stall flat 10/10/10/10",
			history: mkHist(10, 10, 10),
			curPend: 10, want: true,
		},
		{
			name:    "stall growing 10/20/30/40",
			history: mkHist(10, 20, 30),
			curPend: 40, want: true,
		},
		{
			name:    "dip 10/20/15/30",
			history: mkHist(10, 20, 15),
			curPend: 30, want: false,
		},
		{
			name:    "only 2 hist snapshots",
			history: []types.Snapshot{
				appSnap(now.Add(-20*time.Minute), 1, types.AppHealth{Outbox: types.OutboxStats{Pending: 10}}),
				appSnap(now.Add(-10*time.Minute), 1, types.AppHealth{Outbox: types.OutboxStats{Pending: 20}}),
			},
			curPend: 30, want: false,
		},
		{
			name:    "pending drained current 0",
			history: mkHist(10, 10, 10),
			curPend: 0, want: false,
		},
		{
			name:    "pending drained mid-hist",
			history: mkHist(10, 0, 20),
			curPend: 30, want: false,
		},
		{
			name:    "cold start no history",
			history: nil,
			curPend: 10, want: false,
		},
	}

	rule := outboxStallRule{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cur := appSnap(now, 1, types.AppHealth{Outbox: types.OutboxStats{Pending: tc.curPend}})
			// Need multi-lookup history: multiFakeHistory returns nearest.
			// For "only 2 hist" case, the same nearest snapshot might be returned
			// twice — the rule must detect via bucket occupancy.
			got := rule.Evaluate(cur, multiFakeHistory{snaps: tc.history})
			if (len(got) > 0) != tc.want {
				t.Fatalf("want fire=%v got %v", tc.want, got)
			}
		})
	}
}

// --- factory --------------------------------------------------------------

func TestAllAppRules_SortedAndCount(t *testing.T) {
	rules := AllAppRules()
	if len(rules) != 7 {
		t.Fatalf("want 7 rules, got %d", len(rules))
	}
	for i := 1; i < len(rules); i++ {
		if rules[i-1].ID() >= rules[i].ID() {
			t.Errorf("not sorted: %s >= %s", rules[i-1].ID(), rules[i].ID())
		}
	}
}
