package recommendation

import (
	"testing"
	"time"
)

func TestContextAffinity(t *testing.T) {
	const baseline = 0.4 // 40% of the user's plays fall in the current cell
	const k = contextAffinityShrinkage

	// Heavy history, over-represented in the current cell → positive.
	if a := contextAffinity(80, 100, baseline, k); a <= 0 {
		t.Errorf("over-represented artist affinity = %.3f, want positive", a)
	}
	// Heavy history, under-represented → negative.
	if a := contextAffinity(10, 100, baseline, k); a >= 0 {
		t.Errorf("under-represented artist affinity = %.3f, want negative", a)
	}
	// A sparse artist (1/1) shrinks toward the baseline, so its affinity is
	// smaller than a heavily-played artist with the same raw cell-share.
	sparse := contextAffinity(1, 1, baseline, k)
	heavy := contextAffinity(100, 100, baseline, k)
	if sparse >= heavy {
		t.Errorf("sparse (%.3f) should shrink below heavy (%.3f)", sparse, heavy)
	}
	// No plays → neutral.
	if a := contextAffinity(0, 0, baseline, k); a != 0 {
		t.Errorf("no plays affinity = %.3f, want 0", a)
	}
	// Result stays within [-1, 1].
	for _, tc := range [][2]float64{{100, 100}, {0, 100}, {50, 50}} {
		a := contextAffinity(tc[0], tc[1], baseline, k)
		if a < -1 || a > 1 {
			t.Errorf("affinity out of [-1,1]: %.3f", a)
		}
	}
}

func TestScore_ContextTermAddsAndSubtracts(t *testing.T) {
	now := time.Now()
	zeroJitter := func() float64 { return 0.5 } // (0.5*2-1)=0 with any magnitude
	w := ScoringWeights{ContextTimeWeight: 2.0} // all other weights 0

	pos := Score(ScoringInputs{ContextAffinityScore: 1.0}, w, now, zeroJitter)
	if !almostEq(pos, 2.0) {
		t.Errorf("positive context affinity: Score = %.3f, want 2.0", pos)
	}
	neg := Score(ScoringInputs{ContextAffinityScore: -1.0}, w, now, zeroJitter)
	if !almostEq(neg, -2.0) {
		t.Errorf("negative context affinity: Score = %.3f, want -2.0 (demotes)", neg)
	}
	off := Score(ScoringInputs{ContextAffinityScore: 1.0}, ScoringWeights{}, now, zeroJitter)
	if !almostEq(off, 0.0) {
		t.Errorf("ContextTimeWeight 0: Score = %.3f, want 0 (no effect)", off)
	}
}
