package loop

import (
	"testing"
	"time"

	"github.com/kami/maven/internal/store"
)

// helper: rule with a known envelope so tested values stay stable even if
// the canonical WaterRule's numbers move.
func tuneRule(name string, base, min, max time.Duration) Rule {
	return Rule{
		Name:     name,
		Severity: Sev1,
		Cooldown: Cooldown{Base: base, Min: min, Max: max},
	}
}

func TestTuneCooldownEmptyReturnsBase(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	if got := TuneCooldown(r, nil); got != r.Cooldown.Base {
		t.Fatalf("empty outcomes: want base %v, got %v", r.Cooldown.Base, got)
	}
	if got := TuneCooldown(r, []string{}); got != r.Cooldown.Base {
		t.Fatalf("no outcomes: want base %v, got %v", r.Cooldown.Base, got)
	}
}

func TestTuneCooldownThreeIgnoredOneActedLengthens(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	os := []string{store.NudgeIgnored, store.NudgeIgnored, store.NudgeIgnored, store.NudgeActed}
	got := TuneCooldown(r, os)
	// factor = 1 + 0.5*(.75 - .25) = 1.25  →  37.5m
	want := time.Duration(float64(r.Cooldown.Base) * 1.25)
	if got != want {
		t.Fatalf("3 ignored / 1 acted: want %v, got %v", want, got)
	}
}

func TestTuneCooldownMostlyIgnoredLengthens(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	os := repeat(store.NudgeIgnored, 7)
	os = append(os, store.NudgeActed)
	got := TuneCooldown(r, os)
	// factor = 1 + 0.5*(7/8 - 1/8) = 1 + 0.5*0.75 = 1.375 → 41.25m
	want := time.Duration(float64(r.Cooldown.Base) * 1.375)
	if got != want {
		t.Fatalf("mostly ignored: want %v, got %v", want, got)
	}
}

func TestTuneCooldownMostlyActedShortens(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	os := repeat(store.NudgeActed, 7)
	os = append(os, store.NudgeIgnored)
	got := TuneCooldown(r, os)
	// factor = 1 + 0.5*(1/8 - 7/8) = 1 - 0.5*0.75 = 0.625 → 18.75m
	want := time.Duration(float64(r.Cooldown.Base) * 0.625)
	if got != want {
		t.Fatalf("mostly acted: want %v, got %v", want, got)
	}
}

func TestTuneCooldownClampsAtMax(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 35*time.Minute)
	os := repeat(store.NudgeIgnored, 8) // all ignored → factor 1.5 → 45m, clamped to Max 35m
	if got := TuneCooldown(r, os); got != 35*time.Minute {
		t.Fatalf("all ignored should clamp to Max: want 35m, got %v", got)
	}
}

func TestTuneCooldownClampsAtMin(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 20*time.Minute, 6*time.Hour)
	os := repeat(store.NudgeActed, 8) // all acted → factor 0.5 → 15m, clamped to Min 20m
	if got := TuneCooldown(r, os); got != 20*time.Minute {
		t.Fatalf("all acted should clamp to Min: want 20m, got %v", got)
	}
}

func TestTuneCooldownSnoozedIsNeutral(t *testing.T) {
	// all snoozed → both rates 0 → factor 1 → base unchanged.
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	os := repeat(store.NudgeSnoozed, 8)
	if got := TuneCooldown(r, os); got != r.Cooldown.Base {
		t.Fatalf("all snoozed should be neutral: want base %v, got %v", r.Cooldown.Base, got)
	}
}

func TestFeedbackKeyMatchesRuleName(t *testing.T) {
	r := tuneRule("water", 30*time.Minute, 15*time.Minute, 6*time.Hour)
	if got := FeedbackKey(r); got != "cooldown:water" {
		t.Fatalf("FeedbackKey: want cooldown:water, got %q", got)
	}
}

func TestParseCooldownFactRoundTrip(t *testing.T) {
	d := 45 * time.Minute
	f := store.Fact{
		Key:    "cooldown:water",
		Source: FeedbackSource,
		Value:  MarshalCooldown(d),
		Ts:     refTime(),
	}
	got, ok := ParseCooldownFact(f)
	if !ok {
		t.Fatalf("ParseCooldownFact: want ok, got false (value %q)", f.Value)
	}
	if got != d {
		t.Fatalf("round trip: want %v, got %v", d, got)
	}
}

func TestParseCooldownFactRejectsBadRows(t *testing.T) {
	now := refTime()
	cases := []struct {
		name string
		f    store.Fact
	}{
		{"missing value", store.Fact{Key: "cooldown:water", Source: FeedbackSource, Ts: now}},
		{"wrong source", store.Fact{Key: "cooldown:water", Source: "tap:water", Value: MarshalCooldown(30 * time.Minute), Ts: now}},
		{"non-numeric", store.Fact{Key: "cooldown:water", Source: FeedbackSource, Value: `"45m"`, Ts: now}},
		{"negative ns", store.Fact{Key: "cooldown:water", Source: FeedbackSource, Value: "-1000", Ts: now}},
		{"zero ts", store.Fact{Key: "cooldown:water", Source: FeedbackSource, Value: MarshalCooldown(30 * time.Minute)}},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if _, ok := ParseCooldownFact(c.f); ok {
				t.Fatalf("ParseCooldownFact %q: want not ok", c.name)
			}
		})
	}
}

func TestCooldownForTakesBaseDuration(t *testing.T) {
	last := refTime()
	got := CooldownFor(45*time.Minute, last)
	if got != last.Add(45*time.Minute) {
		t.Fatalf("CooldownFor(base, last): want %v, got %v", last.Add(45*time.Minute), got)
	}
	if got := CooldownFor(45*time.Minute, time.Time{}); !got.IsZero() {
		t.Fatalf("CooldownFor zero lastSend: want zero, got %v", got)
	}
}

func repeat(v string, n int) []string {
	out := make([]string, n)
	for i := range out {
		out[i] = v
	}
	return out
}
