package api

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

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

// truncateLibrary clears the music tables between tests. Sessions/users are
// cleared by testHandlers. CASCADE covers tracks→albums→artists FK chain.
func truncateLibrary(t *testing.T, pool *pgxpool.Pool) {
	t.Helper()
	if _, err := pool.Exec(context.Background(),
		"TRUNCATE tracks, albums, artists, scrobble_queue RESTART IDENTITY CASCADE"); err != nil {
		t.Fatalf("truncate: %v", err)
	}
}

// seedArtist inserts an artist with deterministic sort_name = name.
func seedArtist(t *testing.T, pool *pgxpool.Pool, name string) dbq.Artist {
	t.Helper()
	a, err := dbq.New(pool).UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name:     name,
		SortName: name,
		Mbid:     nil,
	})
	if err != nil {
		t.Fatalf("UpsertArtist(%s): %v", name, err)
	}
	return a
}

// seedAlbum inserts an album belonging to artistID. releaseYear=0 leaves
// release_date unset.
func seedAlbum(t *testing.T, pool *pgxpool.Pool, artistID pgtype.UUID, title string, releaseYear int) dbq.Album {
	t.Helper()
	var rel pgtype.Date
	if releaseYear > 0 {
		rel = pgtype.Date{Time: time.Date(releaseYear, 1, 1, 0, 0, 0, 0, time.UTC), Valid: true}
	}
	a, err := dbq.New(pool).UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title:        title,
		SortTitle:    title,
		ArtistID:     artistID,
		ReleaseDate:  rel,
		Mbid:         nil,
		CoverArtPath: nil,
	})
	if err != nil {
		t.Fatalf("UpsertAlbum(%s): %v", title, err)
	}
	return a
}

// seedTrack inserts a track. trackNumber<=0 leaves it NULL. filePath is
// synthesized from title to stay unique across seeds.
func seedTrack(t *testing.T, pool *pgxpool.Pool, albumID, artistID pgtype.UUID, title string, trackNumber int, durationMs int32) dbq.Track {
	t.Helper()
	var tn *int32
	if trackNumber > 0 {
		v := int32(trackNumber)
		tn = &v
	}
	tr, err := dbq.New(pool).UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title:       title,
		AlbumID:     albumID,
		ArtistID:    artistID,
		TrackNumber: tn,
		DiscNumber:  nil,
		DurationMs:  durationMs,
		FilePath:    "/seed/" + title + ".flac",
		FileSize:    1024,
		FileFormat:  "flac",
		Bitrate:     nil,
		Mbid:        nil,
		Genre:       nil,
	})
	if err != nil {
		t.Fatalf("UpsertTrack(%s): %v", title, err)
	}
	return tr
}

// seedTrackWithGenre is seedTrack but lets the caller set Genre. Used by
// tests that exercise the contextual recommendation path, since session
// vectors collect tags from tracks.genre.
func seedTrackWithGenre(t *testing.T, pool *pgxpool.Pool, albumID, artistID pgtype.UUID, title string, trackNumber int, durationMs int32, genre string) dbq.Track {
	t.Helper()
	var tn *int32
	if trackNumber > 0 {
		v := int32(trackNumber)
		tn = &v
	}
	tr, err := dbq.New(pool).UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title:       title,
		AlbumID:     albumID,
		ArtistID:    artistID,
		TrackNumber: tn,
		DiscNumber:  nil,
		DurationMs:  durationMs,
		FilePath:    "/seed/" + title + ".flac",
		FileSize:    1024,
		FileFormat:  "flac",
		Bitrate:     nil,
		Mbid:        nil,
		Genre:       &genre,
	})
	if err != nil {
		t.Fatalf("UpsertTrack(%s): %v", title, err)
	}
	return tr
}

// insertOpenSessionWithVector creates a play_session with ended_at NULL and
// inserts a play_event in it whose session_vector_at_play is the given JSON.
// Used to simulate "user is mid-listen" for the radio handler's current-vector
// lookup. The play_event references a placeholder track inserted on the fly
// so the FK is valid; the placeholder is not used for radio scoring.
func insertOpenSessionWithVector(t *testing.T, pool *pgxpool.Pool, userID, anyArtistID pgtype.UUID, vectorJSON []byte) {
	t.Helper()
	q := dbq.New(pool)
	al, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "PlaceholderAlbum", SortTitle: "PlaceholderAlbum", ArtistID: anyArtistID,
	})
	if err != nil {
		t.Fatalf("placeholder album: %v", err)
	}
	ph, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "Placeholder", AlbumID: al.ID, ArtistID: anyArtistID,
		FilePath: "/seed/placeholder.flac", DurationMs: 100_000, FileSize: 1024, FileFormat: "flac",
	})
	if err != nil {
		t.Fatalf("placeholder track: %v", err)
	}
	var sessionID pgtype.UUID
	if err := pool.QueryRow(context.Background(),
		`INSERT INTO play_sessions (user_id, started_at, last_event_at, client_id)
		 VALUES ($1, now() - interval '5 minutes', now(), 'test') RETURNING id`,
		userID).Scan(&sessionID); err != nil {
		t.Fatalf("insert session: %v", err)
	}
	if _, err := pool.Exec(context.Background(),
		`INSERT INTO play_events (user_id, track_id, session_id, started_at, session_vector_at_play)
		 VALUES ($1, $2, $3, now() - interval '1 minute', $4)`,
		userID, ph.ID, sessionID, vectorJSON); err != nil {
		t.Fatalf("insert play_event: %v", err)
	}
}

// seedTrackWithFile creates a fresh temp directory, writes fileBody to
// <dir>/<title>.<ext>, and inserts a Track row whose file_path points at it.
// Returns the inserted Track and the directory (callers drop sidecar covers
// into this dir when a test needs them). FileFormat is ext lowercased.
func seedTrackWithFile(t *testing.T, pool *pgxpool.Pool, albumID, artistID pgtype.UUID, title string, fileBody []byte, ext string) (dbq.Track, string) {
	t.Helper()
	dir := t.TempDir()
	path := filepath.Join(dir, title+"."+ext)
	if err := os.WriteFile(path, fileBody, 0o644); err != nil {
		t.Fatalf("WriteFile(%s): %v", path, err)
	}
	one := int32(1)
	tr, err := dbq.New(pool).UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title:       title,
		AlbumID:     albumID,
		ArtistID:    artistID,
		TrackNumber: &one,
		DiscNumber:  nil,
		// DurationMs: arbitrary non-zero; tests don't assert on it.
		DurationMs: int32(len(fileBody)),
		FilePath:   path,
		FileSize:   int64(len(fileBody)),
		FileFormat: strings.ToLower(ext),
		Bitrate:    nil,
		Mbid:       nil,
		Genre:      nil,
	})
	if err != nil {
		t.Fatalf("UpsertTrack(%s): %v", title, err)
	}
	return tr, dir
}
