package playlists

import (
	"testing"

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

// pickSeedArtistsFromRows is the pure helper that turns sqlc rows into
// the seed list. The DB-level query is exercised in system_test.go's
// integration test; this unit test pins the post-fetch logic.

func TestPickSeedArtistsFromRows_PreservesOrder(t *testing.T) {
	mk := func(b byte, score int64) seedArtistRow {
		return seedArtistRow{
			ArtistID: pgtype.UUID{Bytes: [16]byte{b}, Valid: true},
			Score:    score,
		}
	}
	// Caller (SQL) already orders + limits; helper preserves order.
	rows := []seedArtistRow{mk(1, 100), mk(2, 50), mk(3, 25)}
	got := pickSeedArtistsFromRows(rows)
	if len(got) != 3 {
		t.Fatalf("len: got %d, want 3", len(got))
	}
	if got[0].Bytes[0] != 1 || got[1].Bytes[0] != 2 || got[2].Bytes[0] != 3 {
		t.Errorf("expected [1,2,3] preserving input order; got %v", got)
	}
}

func TestPickSeedArtistsFromRows_FewerThanThree(t *testing.T) {
	mk := func(b byte, score int64) seedArtistRow {
		return seedArtistRow{ArtistID: pgtype.UUID{Bytes: [16]byte{b}, Valid: true}, Score: score}
	}
	rows := []seedArtistRow{mk(1, 100)}
	got := pickSeedArtistsFromRows(rows)
	if len(got) != 1 {
		t.Errorf("len: got %d, want 1", len(got))
	}
}

func TestPickSeedArtistsFromRows_Empty(t *testing.T) {
	got := pickSeedArtistsFromRows(nil)
	if len(got) != 0 {
		t.Errorf("empty input should yield empty output; got %v", got)
	}
}

// userIDHash is the per-user, per-day hash that drives the daily-
// determinism RNGs. Same family as tieBreakHash, just keyed on user
// ID instead of track ID.

func TestUserIDHash_Deterministic(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Valid: true}
	a := userIDHash(u, "2026-05-04")
	b := userIDHash(u, "2026-05-04")
	if a != b {
		t.Fatalf("same inputs gave different hashes: %d vs %d", a, b)
	}
}

func TestUserIDHash_DifferentDateChangesHash(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Valid: true}
	a := userIDHash(u, "2026-05-04")
	b := userIDHash(u, "2026-05-05")
	if a == b {
		t.Errorf("different dates should change hash; both = %d", a)
	}
}

func TestUserIDHash_DifferentUserChangesHash(t *testing.T) {
	a := pgtype.UUID{Bytes: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Valid: true}
	b := pgtype.UUID{Bytes: [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, Valid: true}
	if userIDHash(a, "2026-05-04") == userIDHash(b, "2026-05-04") {
		t.Errorf("different users should change hash")
	}
}

// pickForYouSeedForDay rotates the chosen For-You seed across the
// user's top-played candidates using userIDHash. Verifies the picker
// is deterministic within a day, varies across days, and degrades
// gracefully when fewer than 5 candidates exist.

func TestPickForYouSeedForDay_DeterministicWithinDay(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	seeds := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
		{Bytes: [16]byte{30}, Valid: true},
		{Bytes: [16]byte{40}, Valid: true},
		{Bytes: [16]byte{50}, Valid: true},
	}
	a := pickForYouSeedForDay(seeds, u, "2026-05-04")
	b := pickForYouSeedForDay(seeds, u, "2026-05-04")
	if a != b {
		t.Fatalf("same day should pick same seed; got %v then %v", a, b)
	}
}

func TestPickForYouSeedForDay_VariesAcrossDays(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	seeds := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
		{Bytes: [16]byte{30}, Valid: true},
		{Bytes: [16]byte{40}, Valid: true},
		{Bytes: [16]byte{50}, Valid: true},
	}
	picks := map[[16]byte]bool{}
	for i := 1; i <= 30; i++ {
		date := "2026-05-" + twoDigits(i)
		picks[pickForYouSeedForDay(seeds, u, date).Bytes] = true
	}
	if len(picks) < 2 {
		t.Errorf("expected >=2 distinct seeds across 30 days; got %d", len(picks))
	}
}

func TestPickForYouSeedForDay_SingleCandidate(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	only := pgtype.UUID{Bytes: [16]byte{99}, Valid: true}
	got := pickForYouSeedForDay([]pgtype.UUID{only}, u, "2026-05-04")
	if got != only {
		t.Errorf("single-seed pool should return that seed; got %v", got)
	}
}

func TestPickForYouSeedForDay_EmptyPool(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	got := pickForYouSeedForDay(nil, u, "2026-05-04")
	if got.Valid {
		t.Errorf("empty pool should return zero UUID; got %v", got)
	}
}

// pickSeedArtistsForDay takes the user's top-5 candidate artists and
// returns 3 of them via daily-deterministic shuffle. Verifies the
// picker is deterministic within a day, varies across days, and
// degrades gracefully when fewer than 3 or 5 candidates exist.

func TestPickSeedArtistsForDay_DeterministicWithinDay(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	pool := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
		{Bytes: [16]byte{30}, Valid: true},
		{Bytes: [16]byte{40}, Valid: true},
		{Bytes: [16]byte{50}, Valid: true},
	}
	a := pickSeedArtistsForDay(pool, u, "2026-05-04")
	b := pickSeedArtistsForDay(pool, u, "2026-05-04")
	if len(a) != 3 || len(b) != 3 {
		t.Fatalf("expected 3 seeds; got %d / %d", len(a), len(b))
	}
	for i := range a {
		if a[i] != b[i] {
			t.Fatalf("same day should produce same order; differs at i=%d", i)
		}
	}
}

func TestPickSeedArtistsForDay_VariesAcrossDays(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	pool := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
		{Bytes: [16]byte{30}, Valid: true},
		{Bytes: [16]byte{40}, Valid: true},
		{Bytes: [16]byte{50}, Valid: true},
	}
	// Collect the trio (as a sorted byte tuple) across many dates.
	// With C(5,3) = 10 possible trios, 30 dates should yield >=2 distinct sets.
	seen := map[[3]byte]bool{}
	for i := 1; i <= 30; i++ {
		got := pickSeedArtistsForDay(pool, u, "2026-05-"+twoDigits(i))
		if len(got) != 3 {
			t.Fatalf("expected 3 seeds; got %d", len(got))
		}
		// Sort the three byte values for set-equivalence comparison.
		v := [3]byte{got[0].Bytes[0], got[1].Bytes[0], got[2].Bytes[0]}
		if v[0] > v[1] {
			v[0], v[1] = v[1], v[0]
		}
		if v[1] > v[2] {
			v[1], v[2] = v[2], v[1]
		}
		if v[0] > v[1] {
			v[0], v[1] = v[1], v[0]
		}
		seen[v] = true
	}
	if len(seen) < 2 {
		t.Errorf("expected >=2 distinct seed trios across 30 days; got %d", len(seen))
	}
}

func TestPickSeedArtistsForDay_FewerThanFive(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	pool := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
		{Bytes: [16]byte{30}, Valid: true},
	}
	got := pickSeedArtistsForDay(pool, u, "2026-05-04")
	if len(got) != 3 {
		t.Errorf("with 3 candidates, expected all 3 returned; got %d", len(got))
	}
}

func TestPickSeedArtistsForDay_FewerThanThree(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	pool := []pgtype.UUID{
		{Bytes: [16]byte{10}, Valid: true},
		{Bytes: [16]byte{20}, Valid: true},
	}
	got := pickSeedArtistsForDay(pool, u, "2026-05-04")
	if len(got) != 2 {
		t.Errorf("with 2 candidates, expected 2 returned; got %d", len(got))
	}
}

func TestPickSeedArtistsForDay_Empty(t *testing.T) {
	u := pgtype.UUID{Bytes: [16]byte{1}, Valid: true}
	got := pickSeedArtistsForDay(nil, u, "2026-05-04")
	if len(got) != 0 {
		t.Errorf("empty pool should return empty; got %d", len(got))
	}
}

func twoDigits(n int) string {
	if n < 10 {
		return "0" + string(rune('0'+n))
	}
	return string(rune('0'+n/10)) + string(rune('0'+n%10))
}
