package recommendation

import (
	"encoding/json"
	"testing"

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

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

func track(artistID pgtype.UUID, genre string) dbq.Track {
	t := dbq.Track{
		ArtistID: artistID,
	}
	if genre != "" {
		g := genre
		t.Genre = &g
	}
	return t
}

func uuid(s string) pgtype.UUID {
	var u pgtype.UUID
	_ = u.Scan(s)
	return u
}

func TestBuildSessionVector_Empty_IsSeed(t *testing.T) {
	v := BuildSessionVector(nil)
	if !v.Seed {
		t.Errorf("Seed = false, want true for empty input")
	}
	if len(v.Artists) != 0 || len(v.Tags) != 0 || len(v.RecentTrackIDs) != 0 {
		t.Errorf("non-empty collections: %+v", v)
	}
}

func TestBuildSessionVector_OneTrack_IsSeed(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	tr := track(a1, "rock")
	tr.ID = uuid("22222222-2222-2222-2222-222222222222")
	v := BuildSessionVector([]dbq.Track{tr})
	if !v.Seed {
		t.Errorf("1 track: Seed=false, want true (< 3)")
	}
	if len(v.Artists) != 1 {
		t.Errorf("Artists: %+v", v.Artists)
	}
	if v.Tags["rock"] != 1 {
		t.Errorf("Tags: %+v", v.Tags)
	}
	if len(v.RecentTrackIDs) != 1 {
		t.Errorf("RecentTrackIDs: %+v", v.RecentTrackIDs)
	}
}

func TestBuildSessionVector_TwoTracks_IsSeed(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{track(a1, "rock"), track(a1, "jazz")})
	if !v.Seed {
		t.Errorf("2 tracks: Seed=false, want true (< 3)")
	}
}

func TestBuildSessionVector_ThreeTracks_NotSeed(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{
		track(a1, "rock"), track(a1, "rock"), track(a1, "rock"),
	})
	if v.Seed {
		t.Errorf("3 tracks: Seed=true, want false")
	}
	// All same artist → 1 entry.
	if len(v.Artists) != 1 {
		t.Errorf("Artists len = %d, want 1 (dedup)", len(v.Artists))
	}
	// All same genre → count 3.
	if v.Tags["rock"] != 3 {
		t.Errorf("Tags['rock'] = %d, want 3", v.Tags["rock"])
	}
}

func TestBuildSessionVector_DistinctArtistsAndGenres(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	a2 := uuid("22222222-2222-2222-2222-222222222222")
	a3 := uuid("33333333-3333-3333-3333-333333333333")
	v := BuildSessionVector([]dbq.Track{
		track(a1, "rock"),
		track(a2, "jazz"),
		track(a3, "rock"),
		track(a1, "experimental"),
	})
	// 4 tracks → not seed.
	if v.Seed {
		t.Errorf("Seed=true, want false")
	}
	// Distinct artists: 3 (a1 dedup'd).
	if len(v.Artists) != 3 {
		t.Errorf("Artists len = %d, want 3", len(v.Artists))
	}
	// Tag counts.
	if v.Tags["rock"] != 2 || v.Tags["jazz"] != 1 || v.Tags["experimental"] != 1 {
		t.Errorf("Tags = %+v", v.Tags)
	}
	if len(v.RecentTrackIDs) != 4 {
		t.Errorf("RecentTrackIDs len = %d", len(v.RecentTrackIDs))
	}
}

func TestBuildSessionVector_NilGenre_NotIndexed(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{
		track(a1, ""), // empty genre via the helper (nil pointer)
		track(a1, "rock"),
		track(a1, ""),
	})
	if v.Tags["rock"] != 1 {
		t.Errorf("Tags['rock'] = %d, want 1", v.Tags["rock"])
	}
	if _, exists := v.Tags[""]; exists {
		t.Errorf("Tags has empty-string entry: %+v", v.Tags)
	}
}

func TestBuildSessionVector_MultiGenreSplitsOnSemicolon(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{
		track(a1, "Indie Pop; Pop; Alternative Pop"),
		track(a1, "Pop"),
		track(a1, "Boom Bap; Hip Hop; Lo-Fi"),
	})
	// Pop appears once on track 1 and once on track 2 → count 2.
	if v.Tags["Pop"] != 2 {
		t.Errorf("Tags['Pop'] = %d, want 2 (one per matching track)", v.Tags["Pop"])
	}
	// Indie Pop, Alternative Pop only on track 1.
	if v.Tags["Indie Pop"] != 1 || v.Tags["Alternative Pop"] != 1 {
		t.Errorf("Indie/Alt Pop counts wrong: %+v", v.Tags)
	}
	// Track 3's genres.
	if v.Tags["Boom Bap"] != 1 || v.Tags["Hip Hop"] != 1 || v.Tags["Lo-Fi"] != 1 {
		t.Errorf("track-3 genre counts wrong: %+v", v.Tags)
	}
}

func TestBuildSessionVector_NoSeparatorStaysOpaque(t *testing.T) {
	// Concatenated strings without separators (broken tag-editor output)
	// can't be parsed; they stay as one tag and only match identical strings.
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{
		track(a1, "ElectronicComplextroGlitch Hop"),
		track(a1, "ElectronicComplextroGlitch Hop"),
	})
	if v.Tags["ElectronicComplextroGlitch Hop"] != 2 {
		t.Errorf("opaque tag count = %d, want 2", v.Tags["ElectronicComplextroGlitch Hop"])
	}
	if len(v.Tags) != 1 {
		t.Errorf("len(Tags) = %d, want 1", len(v.Tags))
	}
}

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

func TestBuildSessionVector_JSONRoundTrip(t *testing.T) {
	a1 := uuid("11111111-1111-1111-1111-111111111111")
	v := BuildSessionVector([]dbq.Track{
		track(a1, "rock"), track(a1, "jazz"), track(a1, "rock"),
	})
	data, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var got SessionVector
	if err := json.Unmarshal(data, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if got.Seed != v.Seed {
		t.Errorf("Seed: %v != %v", got.Seed, v.Seed)
	}
	if got.Tags["rock"] != v.Tags["rock"] {
		t.Errorf("Tags: %+v != %+v", got.Tags, v.Tags)
	}
	if len(got.Artists) != len(v.Artists) {
		t.Errorf("Artists: len %d != %d", len(got.Artists), len(v.Artists))
	}
	if len(got.RecentTrackIDs) != len(v.RecentTrackIDs) {
		t.Errorf("RecentTrackIDs: len %d != %d", len(got.RecentTrackIDs), len(v.RecentTrackIDs))
	}
}
