package taste

import (
	"testing"

	"github.com/jackc/pgx/v5/pgtype"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

func strLess(a, b string) bool { return a < b }

func TestFoldEnrichedTags(t *testing.T) {
	tagW := map[string]float64{"rock": 1.0} // pre-existing ID3 genre weight
	tags := []dbq.TrackTag{
		{Tag: "shoegaze", Weight: 1.0},
		{Tag: "melancholic", Weight: 0.5},
		{Tag: "rock", Weight: 0.4}, // overlaps genre → accumulates
	}
	// base=2 (engagement), scale=0.5 → contribution = 2 * weight * 0.5 = weight.
	foldEnrichedTags(tagW, tags, 2.0, 0.5)
	if got := tagW["shoegaze"]; got != 1.0 {
		t.Errorf("shoegaze = %v, want 1.0", got)
	}
	if got := tagW["melancholic"]; got != 0.5 {
		t.Errorf("melancholic = %v, want 0.5", got)
	}
	if got := tagW["rock"]; got != 1.4 { // 1.0 genre + 2*0.4*0.5
		t.Errorf("rock = %v, want 1.4", got)
	}

	// scale=0 disables the enriched contribution entirely.
	base := map[string]float64{"rock": 1.0}
	foldEnrichedTags(base, tags, 2.0, 0)
	if len(base) != 1 || base["rock"] != 1.0 {
		t.Errorf("scale=0 should leave tagW untouched; got %v", base)
	}
}

func TestRankWeighted_DropsEpsilonAndSortsDesc(t *testing.T) {
	m := map[string]float64{"a": 3.0, "b": 1.0, "tiny": 0.01, "d": -2.0}
	got := rankWeighted(m, 0.05, 10, 10, strLess)
	if len(got) != 3 {
		t.Fatalf("len = %d, want 3 (tiny dropped by epsilon)", len(got))
	}
	wantOrder := []string{"a", "b", "d"} // 3, 1, -2 descending
	for i, w := range wantOrder {
		if got[i].Key != w {
			t.Errorf("pos %d = %q, want %q", i, got[i].Key, w)
		}
	}
}

func TestRankWeighted_CapsTopAndBottom(t *testing.T) {
	m := map[string]float64{"e5": 5, "e4": 4, "e3": 3, "e2": 2, "e1": 1}
	got := rankWeighted(m, 0.0, 1, 1, strLess) // top 1 + bottom 1
	if len(got) != 2 {
		t.Fatalf("len = %d, want 2", len(got))
	}
	if got[0].Key != "e5" || got[1].Key != "e1" {
		t.Errorf("got [%s,%s], want [e5,e1]", got[0].Key, got[1].Key)
	}
}

func TestRankWeighted_DeterministicTieBreak(t *testing.T) {
	m := map[string]float64{"z": 1.0, "a": 1.0, "m": 1.0}
	got := rankWeighted(m, 0.0, 10, 10, strLess)
	// Equal weights → ordered by keyLess (string asc): a, m, z.
	want := []string{"a", "m", "z"}
	for i, w := range want {
		if got[i].Key != w {
			t.Errorf("tie-break pos %d = %q, want %q", i, got[i].Key, w)
		}
	}
}

func TestApplyFloor_ClampsNegatives(t *testing.T) {
	m := map[string]float64{"a": -5, "b": 2, "c": -1}
	applyFloor(m, -3)
	if m["a"] != -3 {
		t.Errorf("a = %.1f, want -3 (floored)", m["a"])
	}
	if m["b"] != 2 {
		t.Errorf("b = %.1f, want 2 (untouched)", m["b"])
	}
	if m["c"] != -1 {
		t.Errorf("c = %.1f, want -1 (above floor)", m["c"])
	}
}

func TestSplitGenres(t *testing.T) {
	mk := func(s string) *string { return &s }
	cases := []struct {
		in   *string
		want []string
	}{
		{nil, nil},
		{mk(""), nil},
		{mk("Rock"), []string{"Rock"}},
		{mk(" Rock "), []string{"Rock"}},
		{mk("Rock; Pop, Jazz"), []string{"Rock", "Pop", "Jazz"}},
	}
	for _, c := range cases {
		got := splitGenres(c.in)
		if len(got) != len(c.want) {
			t.Errorf("splitGenres(%v) len = %d, want %d", c.in, len(got), len(c.want))
			continue
		}
		for i := range got {
			if got[i] != c.want[i] {
				t.Errorf("splitGenres(%v)[%d] = %q, want %q", c.in, i, got[i], c.want[i])
			}
		}
	}
}

func TestUuidLess_Orders(t *testing.T) {
	a := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	b := pgtype.UUID{Bytes: [16]byte{2}, Valid: true}
	if !uuidLess(a, b) || uuidLess(b, a) {
		t.Error("uuidLess byte ordering wrong")
	}
}
