package playlists

import (
	"testing"
	"time"

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

	"git.fabledsword.com/bvandeusen/minstrel/internal/recommendation"
)

// fixedNow keeps recencyDecay stable across the pure roll-up tests.
var fixedNow = time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC)

// byteOf returns the per-test entity discriminator (makeCand sets it in
// Bytes[15]) so assertions can name albums/artists by their small int.
func byteOf(u pgtype.UUID) byte { return u.Bytes[15] }

// countWithByte reports how many of ids carry the given Bytes[15] marker.
func countWithByte(ids []pgtype.UUID, want byte) int {
	n := 0
	for _, id := range ids {
		if byteOf(id) == want {
			n++
		}
	}
	return n
}

func TestSumTopK(t *testing.T) {
	if got := sumTopK([]float64{1, 2, 3, 4}, 2); got != 7 {
		t.Errorf("sumTopK top-2 of 1..4 = %v, want 7", got)
	}
	if got := sumTopK([]float64{5}, 3); got != 5 {
		t.Errorf("sumTopK fewer-than-k = %v, want 5", got)
	}
	if got := sumTopK(nil, 3); got != 0 {
		t.Errorf("sumTopK empty = %v, want 0", got)
	}
}

func TestRollUpCandidates_MultiMatchAlbumRanksFirst(t *testing.T) {
	// Album 10 (artist 100): three solid matches. Album 20 (artist 200):
	// one slightly-stronger single match. Sum-of-top-3 should rank the
	// multi-match album ahead of the single-match one. Similarity gaps are
	// wide enough that the ±0.1 jitter can't reorder the result.
	cands := []recommendation.Candidate{
		makeCand(1, 10, 100, 0.8),
		makeCand(2, 10, 100, 0.8),
		makeCand(3, 10, 100, 0.8),
		makeCand(4, 20, 200, 0.95),
	}
	albums, _ := rollUpCandidates(cands, testUserID, "2026-06-11", fixedNow)
	if len(albums) < 2 {
		t.Fatalf("want >=2 albums, got %d", len(albums))
	}
	if byteOf(albums[0]) != 10 {
		t.Errorf("top album = %d, want 10 (multi-match beats single)", byteOf(albums[0]))
	}
}

func TestRollUpCandidates_PerArtistAlbumCap(t *testing.T) {
	// Artist 100 spans albums 10/11/12; the album row caps any one artist
	// at youMightLikeMaxAlbumsPerArtist (2). Artist 200's album 20 should
	// still appear.
	cands := []recommendation.Candidate{
		makeCand(1, 10, 100, 0.9),
		makeCand(2, 11, 100, 0.85),
		makeCand(3, 12, 100, 0.8),
		makeCand(4, 20, 200, 0.7),
	}
	albums, _ := rollUpCandidates(cands, testUserID, "2026-06-11", fixedNow)
	from100 := countWithByte(albums, 10) + countWithByte(albums, 11) + countWithByte(albums, 12)
	if from100 > youMightLikeMaxAlbumsPerArtist {
		t.Errorf("artist 100 contributed %d albums, want <= %d",
			from100, youMightLikeMaxAlbumsPerArtist)
	}
	if countWithByte(albums, 20) != 1 {
		t.Errorf("album 20 (artist 200) should survive the cap; got %d",
			countWithByte(albums, 20))
	}
}

func TestRollUpCandidates_ArtistRollupDistinct(t *testing.T) {
	// Three artists; the artist roll-up should surface all three distinct
	// artist IDs (no per-artist cap on the artist row).
	cands := []recommendation.Candidate{
		makeCand(1, 10, 100, 0.9),
		makeCand(2, 11, 101, 0.6),
		makeCand(3, 12, 102, 0.3),
	}
	_, artists := rollUpCandidates(cands, testUserID, "2026-06-11", fixedNow)
	seen := map[byte]bool{}
	for _, a := range artists {
		seen[byteOf(a)] = true
	}
	for _, want := range []byte{100, 101, 102} {
		if !seen[want] {
			t.Errorf("artist %d missing from roll-up", want)
		}
	}
}

func TestCapAlbumsPerArtist_DropsBeyondCap(t *testing.T) {
	mk := func(albumN, artistN int) (pgtype.UUID, pgtype.UUID) {
		var al, ar pgtype.UUID
		al.Valid, ar.Valid = true, true
		al.Bytes[15], ar.Bytes[15] = byte(albumN), byte(artistN)
		return al, ar
	}
	a10, ar1 := mk(10, 1)
	a11, _ := mk(11, 1)
	a12, _ := mk(12, 1)
	a20, ar2 := mk(20, 2)
	albumArtist := map[pgtype.UUID]pgtype.UUID{a10: ar1, a11: ar1, a12: ar1, a20: ar2}
	in := []scoredEntity{{a10, 3}, {a11, 2}, {a12, 1}, {a20, 0.5}}
	got := capAlbumsPerArtist(in, albumArtist, 2)
	if len(got) != 3 {
		t.Fatalf("len = %d, want 3 (artist 1 capped at 2 + artist 2's one)", len(got))
	}
	// Highest-scored two of artist 1 (albums 10, 11) kept; 12 dropped.
	if byteOf(got[2].id) != 20 {
		t.Errorf("third survivor = %d, want 20", byteOf(got[2].id))
	}
}
