package tracks

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"testing"

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

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

// --- test seed helpers (inlined; mirrors lidarrquarantine patterns) -----

func newPool(t *testing.T) *pgxpool.Pool {
	t.Helper()
	if testing.Short() {
		t.Skip("skipping integration test in -short mode")
	}
	dsn := os.Getenv("MINSTREL_TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	if err := db.Migrate(dsn, slog.New(slog.NewTextHandler(io.Discard, nil))); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	pool, err := pgxpool.New(context.Background(), dsn)
	if err != nil {
		t.Fatalf("pool: %v", err)
	}
	t.Cleanup(pool.Close)
	dbtest.ResetDB(t, pool)
	return pool
}

func seedAdmin(t *testing.T, pool *pgxpool.Pool, name string) dbq.User {
	t.Helper()
	u, err := dbq.New(pool).CreateUser(context.Background(), dbq.CreateUserParams{
		Username: dbtest.TestUserPrefix + name, PasswordHash: "x",
		ApiToken: name + "-token", IsAdmin: true,
	})
	if err != nil {
		t.Fatalf("seed admin %s: %v", name, err)
	}
	return u
}

// seedArtistAlbumTrack creates an artist + album + track triple. mbid is
// set on both the album and the track when non-empty (the album mbid is
// derived from the track mbid suffix); empty leaves both nil so the
// track exercises the local-only path. filePath is the on-disk path
// stored on the track row. Names are unique per call so multiple seeds
// in one test don't collide on UNIQUE constraints.
func seedArtistAlbumTrack(t *testing.T, pool *pgxpool.Pool, prefix, mbid, filePath string) (dbq.Artist, dbq.Album, dbq.Track) {
	t.Helper()
	q := dbq.New(pool)
	artist, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: prefix + " Artist", SortName: prefix + " Artist",
	})
	if err != nil {
		t.Fatalf("artist: %v", err)
	}
	var albumMBIDPtr *string
	if mbid != "" {
		s := mbid + "-album"
		albumMBIDPtr = &s
	}
	album, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: prefix + " Album", SortTitle: prefix + " Album",
		ArtistID: artist.ID, Mbid: albumMBIDPtr,
	})
	if err != nil {
		t.Fatalf("album: %v", err)
	}
	var trackMBIDPtr *string
	if mbid != "" {
		s := mbid
		trackMBIDPtr = &s
	}
	track, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: prefix + " Track", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: filePath, FileSize: 1, FileFormat: "mp3",
		Mbid: trackMBIDPtr,
	})
	if err != nil {
		t.Fatalf("track: %v", err)
	}
	return artist, album, track
}

// --- fake LidarrUnmonitorer --------------------------------------------

// fakeLidarrUnmonitorer captures call args + a configurable error so
// tests can assert "called with mbid X" and "RemoveTrack tolerated this
// Lidarr error."
type fakeLidarrUnmonitorer struct {
	calls     []string // captured trackMbid per call
	albumMbid string   // last albumMbid received
	err       error
}

func (f *fakeLidarrUnmonitorer) UnmonitorTrack(_ context.Context, trackMbid, albumMbid string) error {
	f.calls = append(f.calls, trackMbid)
	f.albumMbid = albumMbid
	return f.err
}

// --- tests --------------------------------------------------------------

func TestRemoveTrack_NotFound(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	var bogus pgtype.UUID
	bogus.Bytes = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
	bogus.Valid = true

	svc := NewService(pool, nil, nil, "")
	_, _, _, err := svc.RemoveTrack(context.Background(), bogus, admin.ID, false)
	if !errors.Is(err, ErrNotFound) {
		t.Errorf("err = %v, want ErrNotFound", err)
	}
}

func TestRemoveTrack_LocalFile_HappyPath(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	dir := t.TempDir()
	path := filepath.Join(dir, "track.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	// mbid empty → no Lidarr branch even with unmonitor=true.
	_, _, track := seedArtistAlbumTrack(t, pool, "Solo", "", path)

	fake := &fakeLidarrUnmonitorer{}
	svc := NewService(pool, nil, fake, "")
	_, _, lidarrFailed, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, false)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if lidarrFailed {
		t.Error("lidarrUnmonitorFailed = true; want false on local-file happy path")
	}
	if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
		t.Errorf("file still present: %v", err)
	}
	q := dbq.New(pool)
	if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
		t.Errorf("track row still exists")
	}
	if len(fake.calls) != 0 {
		t.Errorf("Lidarr called %d times; want 0 (no mbid, unmonitor=false)", len(fake.calls))
	}
}

func TestRemoveTrack_FileAlreadyMissing(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	// Path doesn't exist on disk; service must tolerate and clean up DB.
	missing := filepath.Join(t.TempDir(), "does-not-exist.mp3")
	_, _, track := seedArtistAlbumTrack(t, pool, "Ghost", "", missing)

	svc := NewService(pool, nil, nil, "")
	_, _, _, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, false)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	q := dbq.New(pool)
	if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
		t.Errorf("track row still exists")
	}
}

func TestRemoveTrack_Cascade_AlbumEmpty(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	q := dbq.New(pool)
	// Two-album artist: removing the sole track of album A leaves A
	// orphaned, but the artist still has album B (so the artist cascade
	// must NOT fire).
	artist, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: "Cascade Artist", SortName: "Cascade Artist",
	})
	if err != nil {
		t.Fatalf("artist: %v", err)
	}
	albumA, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "Album A", SortTitle: "Album A", ArtistID: artist.ID,
	})
	if err != nil {
		t.Fatalf("albumA: %v", err)
	}
	albumB, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "Album B", SortTitle: "Album B", ArtistID: artist.ID,
	})
	if err != nil {
		t.Fatalf("albumB: %v", err)
	}
	dir := t.TempDir()
	pathA := filepath.Join(dir, "a.mp3")
	pathB := filepath.Join(dir, "b.mp3")
	_ = os.WriteFile(pathA, []byte("x"), 0o644)
	_ = os.WriteFile(pathB, []byte("x"), 0o644)
	trackA, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "TA", AlbumID: albumA.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: pathA, FileSize: 1, FileFormat: "mp3",
	})
	if err != nil {
		t.Fatalf("trackA: %v", err)
	}
	if _, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "TB", AlbumID: albumB.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: pathB, FileSize: 1, FileFormat: "mp3",
	}); err != nil {
		t.Fatalf("trackB: %v", err)
	}

	svc := NewService(pool, nil, nil, "")
	delAlbum, delArtist, _, err := svc.RemoveTrack(context.Background(), trackA.ID, admin.ID, false)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if delAlbum == nil || *delAlbum != albumA.ID {
		t.Errorf("deletedAlbumID = %v, want %v", delAlbum, albumA.ID)
	}
	if delArtist != nil {
		t.Errorf("deletedArtistID = %v, want nil (artist still has album B)", delArtist)
	}
	if _, err := q.GetAlbumByID(context.Background(), albumA.ID); err == nil {
		t.Errorf("album A still exists after cascade")
	}
	if _, err := q.GetAlbumByID(context.Background(), albumB.ID); err != nil {
		t.Errorf("album B unexpectedly removed: %v", err)
	}
	if _, err := q.GetArtistByID(context.Background(), artist.ID); err != nil {
		t.Errorf("artist unexpectedly removed: %v", err)
	}
}

func TestRemoveTrack_Cascade_ArtistEmpty(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	dir := t.TempDir()
	path := filepath.Join(dir, "lone.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	artist, album, track := seedArtistAlbumTrack(t, pool, "Lone", "", path)

	svc := NewService(pool, nil, nil, "")
	delAlbum, delArtist, _, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, false)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if delAlbum == nil || *delAlbum != album.ID {
		t.Errorf("deletedAlbumID = %v, want %v", delAlbum, album.ID)
	}
	if delArtist == nil || *delArtist != artist.ID {
		t.Errorf("deletedArtistID = %v, want %v", delArtist, artist.ID)
	}
	q := dbq.New(pool)
	if _, err := q.GetAlbumByID(context.Background(), album.ID); err == nil {
		t.Errorf("album still exists after cascade")
	}
	if _, err := q.GetArtistByID(context.Background(), artist.ID); err == nil {
		t.Errorf("artist still exists after cascade")
	}
}

// unmonitor=true AND mbid set → Lidarr called once; lidarrUnmonitorFailed=false.
// Uses two albums on the same artist so the album's row survives the
// cascade — that lets us assert the album mbid (captured pre-tx) is
// passed through correctly.
func TestRemoveTrack_Unmonitor_HappyPath(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	q := dbq.New(pool)
	artist, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: "Lidarr Artist", SortName: "Lidarr Artist",
	})
	if err != nil {
		t.Fatalf("artist: %v", err)
	}
	albumMbid := "album-mbid-xyz"
	album, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "Lidarr Album", SortTitle: "Lidarr Album",
		ArtistID: artist.ID, Mbid: &albumMbid,
	})
	if err != nil {
		t.Fatalf("album: %v", err)
	}
	dir := t.TempDir()
	pathA := filepath.Join(dir, "a.mp3")
	pathB := filepath.Join(dir, "b.mp3")
	_ = os.WriteFile(pathA, []byte("x"), 0o644)
	_ = os.WriteFile(pathB, []byte("x"), 0o644)
	trackMbid := "track-mbid-abc"
	track, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "T1", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: pathA, FileSize: 1, FileFormat: "mp3",
		Mbid: &trackMbid,
	})
	if err != nil {
		t.Fatalf("track1: %v", err)
	}
	// Sibling track keeps the album alive after the delete.
	if _, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "T2", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: pathB, FileSize: 1, FileFormat: "mp3",
	}); err != nil {
		t.Fatalf("track2: %v", err)
	}

	fake := &fakeLidarrUnmonitorer{}
	svc := NewService(pool, nil, fake, "")
	_, _, lidarrFailed, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, true)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if lidarrFailed {
		t.Error("lidarrUnmonitorFailed = true; want false on happy path")
	}
	if len(fake.calls) != 1 {
		t.Fatalf("Lidarr call count = %d, want 1", len(fake.calls))
	}
	if fake.calls[0] != trackMbid {
		t.Errorf("Lidarr called with mbid %q, want %q", fake.calls[0], trackMbid)
	}
	if fake.albumMbid != albumMbid {
		t.Errorf("Lidarr called with albumMbid %q, want %q", fake.albumMbid, albumMbid)
	}
}

// unmonitor=true AND Lidarr fails → file + DB still deleted, flag set,
// service returns nil error.
func TestRemoveTrack_Unmonitor_LidarrErrorIsNonFatal(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	q := dbq.New(pool)
	artist, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: "ErrFatal Artist", SortName: "ErrFatal Artist",
	})
	if err != nil {
		t.Fatalf("artist: %v", err)
	}
	albumMbid := "err-album-mbid"
	album, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "ErrFatal Album", SortTitle: "ErrFatal Album",
		ArtistID: artist.ID, Mbid: &albumMbid,
	})
	if err != nil {
		t.Fatalf("album: %v", err)
	}
	dir := t.TempDir()
	path := filepath.Join(dir, "f.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	trackMbid := "err-track-mbid"
	track, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
		Mbid: &trackMbid,
	})
	if err != nil {
		t.Fatalf("track: %v", err)
	}

	fake := &fakeLidarrUnmonitorer{err: errors.New("simulated lidarr 5xx")}
	svc := NewService(pool, nil, fake, "")
	_, _, lidarrFailed, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, true)
	if err != nil {
		t.Fatalf("RemoveTrack: %v (want nil — Lidarr failure is non-fatal)", err)
	}
	if !lidarrFailed {
		t.Error("lidarrUnmonitorFailed = false; want true after Lidarr error")
	}
	// File + DB still gone.
	if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
		t.Errorf("file still present after Lidarr error: %v", err)
	}
	if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
		t.Errorf("track row still present after Lidarr error")
	}
}

// unmonitor=true AND mbid empty (non-Lidarr track) → no Lidarr call.
func TestRemoveTrack_Unmonitor_NoMbidSkipsLidarr(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	dir := t.TempDir()
	path := filepath.Join(dir, "local.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	_, _, track := seedArtistAlbumTrack(t, pool, "Local", "", path)

	fake := &fakeLidarrUnmonitorer{}
	svc := NewService(pool, nil, fake, "")
	_, _, lidarrFailed, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, true)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if lidarrFailed {
		t.Error("lidarrUnmonitorFailed = true; want false (no mbid)")
	}
	if len(fake.calls) != 0 {
		t.Errorf("Lidarr call count = %d, want 0 (track has no mbid)", len(fake.calls))
	}
}

// unmonitor=false AND mbid set → no Lidarr call.
func TestRemoveTrack_NoUnmonitor_SkipsLidarr(t *testing.T) {
	pool := newPool(t)
	admin := seedAdmin(t, pool, "alice")

	dir := t.TempDir()
	path := filepath.Join(dir, "lidarr.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	_, _, track := seedArtistAlbumTrack(t, pool, "NoMon", "track-mbid", path)

	fake := &fakeLidarrUnmonitorer{}
	svc := NewService(pool, nil, fake, "")
	_, _, lidarrFailed, err := svc.RemoveTrack(context.Background(), track.ID, admin.ID, false)
	if err != nil {
		t.Fatalf("RemoveTrack: %v", err)
	}
	if lidarrFailed {
		t.Error("lidarrUnmonitorFailed = true; want false (unmonitor=false)")
	}
	if len(fake.calls) != 0 {
		t.Errorf("Lidarr call count = %d, want 0 (unmonitor=false)", len(fake.calls))
	}
}
