package lidarrquarantine

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"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"
	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
)

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 seedUser(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: false,
	})
	if err != nil {
		t.Fatalf("seed user %s: %v", name, err)
	}
	return u
}

func seedTrack(t *testing.T, pool *pgxpool.Pool, title, mbid string) (dbq.Track, dbq.Album, dbq.Artist) {
	t.Helper()
	q := dbq.New(pool)
	artist, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: "Test Artist", SortName: "Test Artist",
	})
	if err != nil {
		t.Fatalf("artist: %v", err)
	}
	albumMBID := mbid + "-album"
	album, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "Test Album", SortTitle: "Test Album",
		ArtistID: artist.ID, Mbid: &albumMBID,
	})
	if err != nil {
		t.Fatalf("album: %v", err)
	}
	track, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: title, AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: filepath.Join(t.TempDir(), title+".mp3"),
		FileSize: 100, FileFormat: "mp3",
	})
	if err != nil {
		t.Fatalf("track: %v", err)
	}
	return track, album, artist
}

func TestFlag_HappyPath(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "Bad Track", "abc")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	row, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "crackly")
	if err != nil {
		t.Fatalf("Flag: %v", err)
	}
	if string(row.Reason) != "bad_rip" {
		t.Errorf("reason = %v", row.Reason)
	}
	if row.Notes == nil || *row.Notes != "crackly" {
		t.Errorf("notes = %v", row.Notes)
	}
}

func TestFlag_UpsertOnSecondFlag(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "first"); err != nil {
		t.Fatalf("first flag: %v", err)
	}
	row, err := svc.Flag(context.Background(), user.ID, track.ID, "wrong_tags", "")
	if err != nil {
		t.Fatalf("second flag: %v", err)
	}
	if string(row.Reason) != "wrong_tags" {
		t.Errorf("reason = %v", row.Reason)
	}
	if row.Notes != nil {
		t.Errorf("notes = %v, want nil after empty notes upsert", row.Notes)
	}
}

func TestFlag_NonexistentTrackReturnsErrTrackNotFound(t *testing.T) {
	pool := newPool(t)
	user := seedUser(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, lidarrconfig.New(pool), nil)
	_, err := svc.Flag(context.Background(), user.ID, bogus, "bad_rip", "")
	if !errors.Is(err, ErrTrackNotFound) {
		t.Errorf("err = %v, want ErrTrackNotFound", err)
	}
}

func TestFlag_BadReasonRejected(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	_, err := svc.Flag(context.Background(), user.ID, track.ID, "garbage", "")
	if !errors.Is(err, ErrBadReason) {
		t.Errorf("err = %v, want ErrBadReason", err)
	}
}

func TestUnflag_DeletesRow(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", ""); err != nil {
		t.Fatalf("Flag: %v", err)
	}
	if err := svc.Unflag(context.Background(), user.ID, track.ID); err != nil {
		t.Fatalf("Unflag: %v", err)
	}
	if err := svc.Unflag(context.Background(), user.ID, track.ID); !errors.Is(err, ErrQuarantineNotFound) {
		t.Errorf("second Unflag err = %v, want ErrQuarantineNotFound", err)
	}
}

func TestListMine_OrderedNewestFirst(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	t1, _, _ := seedTrack(t, pool, "T1", "x")
	t2, _, _ := seedTrack(t, pool, "T2", "y")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), user.ID, t1.ID, "bad_rip", ""); err != nil {
		t.Fatalf("Flag t1: %v", err)
	}
	if _, err := svc.Flag(context.Background(), user.ID, t2.ID, "duplicate", ""); err != nil {
		t.Fatalf("Flag t2: %v", err)
	}

	rows, err := svc.ListMine(context.Background(), user.ID)
	if err != nil {
		t.Fatalf("ListMine: %v", err)
	}
	if len(rows) != 2 {
		t.Fatalf("len = %d, want 2", len(rows))
	}
	// T2 was flagged second — newest first.
	if rows[0].LidarrQuarantine.TrackID != t2.ID {
		t.Errorf("first row track = %v, want T2 (%v)", rows[0].LidarrQuarantine.TrackID, t2.ID)
	}
}

func TestListAdminQueue_AggregatesByTrackWithReasonCounts(t *testing.T) {
	pool := newPool(t)
	alice := seedUser(t, pool, "alice")
	bob := seedUser(t, pool, "bob")
	carol := seedUser(t, pool, "carol")
	track, _, _ := seedTrack(t, pool, "Hot Mess", "abc")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), alice.ID, track.ID, "bad_rip", ""); err != nil {
		t.Fatalf("alice flag: %v", err)
	}
	if _, err := svc.Flag(context.Background(), bob.ID, track.ID, "bad_rip", ""); err != nil {
		t.Fatalf("bob flag: %v", err)
	}
	if _, err := svc.Flag(context.Background(), carol.ID, track.ID, "wrong_tags", ""); err != nil {
		t.Fatalf("carol flag: %v", err)
	}

	rows, err := svc.ListAdminQueue(context.Background())
	if err != nil {
		t.Fatalf("ListAdminQueue: %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("len = %d, want 1 aggregated row", len(rows))
	}
	r := rows[0]
	if r.ReportCount != 3 {
		t.Errorf("report_count = %d, want 3", r.ReportCount)
	}
	if r.ReasonCounts["bad_rip"] != 2 || r.ReasonCounts["wrong_tags"] != 1 {
		t.Errorf("reason_counts = %+v, want bad_rip=2 wrong_tags=1", r.ReasonCounts)
	}
	if len(r.Reports) != 3 {
		t.Errorf("reports len = %d, want 3", len(r.Reports))
	}
}

func TestResolve_ClearsRowsAndWritesAudit(t *testing.T) {
	pool := newPool(t)
	alice := seedUser(t, pool, "alice")
	bob := seedUser(t, pool, "bob")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), alice.ID, track.ID, "bad_rip", ""); err != nil {
		t.Fatalf("alice flag: %v", err)
	}
	if _, err := svc.Flag(context.Background(), bob.ID, track.ID, "wrong_tags", ""); err != nil {
		t.Fatalf("bob flag: %v", err)
	}

	audit, err := svc.Resolve(context.Background(), track.ID, alice.ID)
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if audit.AffectedUsers != 2 {
		t.Errorf("affected_users = %d, want 2", audit.AffectedUsers)
	}
	if audit.Action != dbq.LidarrQuarantineActionKindResolved {
		t.Errorf("action = %v, want resolved", audit.Action)
	}
	if audit.LidarrAlbumMbid != nil {
		t.Errorf("lidarr_album_mbid = %v, want nil for resolve", audit.LidarrAlbumMbid)
	}
	n, _ := dbq.New(pool).CountQuarantineForTrack(context.Background(), track.ID)
	if n != 0 {
		t.Errorf("rows after resolve = %d, want 0", n)
	}
}

func TestResolve_NoExistingRowsStillWritesAudit(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	// No flags applied — resolve a track with zero quarantine rows.
	audit, err := svc.Resolve(context.Background(), track.ID, user.ID)
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if audit.AffectedUsers != 0 {
		t.Errorf("affected_users = %d, want 0", audit.AffectedUsers)
	}
	if audit.Action != dbq.LidarrQuarantineActionKindResolved {
		t.Errorf("action = %v, want resolved", audit.Action)
	}
	if audit.LidarrAlbumMbid != nil {
		t.Errorf("lidarr_album_mbid = %v, want nil for resolve", audit.LidarrAlbumMbid)
	}
}

func TestResolve_TrackNotFound(t *testing.T) {
	pool := newPool(t)
	user := seedUser(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, lidarrconfig.New(pool), nil)
	_, err := svc.Resolve(context.Background(), bogus, user.ID)
	if !errors.Is(err, ErrTrackNotFound) {
		t.Errorf("err = %v, want ErrTrackNotFound", err)
	}
}

func TestDeleteFile_RemovesFileAndAuditsAffected(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	q := dbq.New(pool)

	dir := t.TempDir()
	path := filepath.Join(dir, "track.mp3")
	if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "A", SortName: "A"})
	album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{Title: "Al", SortTitle: "Al", ArtistID: artist.ID})
	track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
	})

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	if _, err := svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", ""); err != nil {
		t.Fatalf("Flag: %v", err)
	}

	audit, err := svc.DeleteFile(context.Background(), track.ID, user.ID)
	if err != nil {
		t.Fatalf("DeleteFile: %v", err)
	}
	if audit.AffectedUsers != 1 {
		t.Errorf("affected_users = %d, want 1", audit.AffectedUsers)
	}
	if audit.Action != dbq.LidarrQuarantineActionKindDeletedFile {
		t.Errorf("action = %v, want deleted_file", audit.Action)
	}
	if audit.LidarrAlbumMbid != nil {
		t.Errorf("lidarr_album_mbid = %v, want nil for delete_file", audit.LidarrAlbumMbid)
	}
	if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
		t.Errorf("file still exists: %v", err)
	}
	// Track row gone; CASCADE cleared the quarantine row.
	if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
		t.Errorf("track row still exists")
	}
}

func TestDeleteViaLidarr_FullCascade(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")

	var captured []string
	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		captured = append(captured, r.Method+" "+r.URL.Path+"?"+r.URL.RawQuery)
		w.Header().Set("Content-Type", "application/json")
		if r.URL.Path == "/api/v1/album" && r.Method == http.MethodGet {
			_, _ = w.Write([]byte(`[{"id":42,"foreignAlbumId":"al-mbid","title":"Al","artistId":7}]`))
			return
		}
		// DELETE /api/v1/album/42 -> 200.
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(stub.Close)

	cfg := lidarrconfig.New(pool)
	if err := cfg.Save(context.Background(), lidarrconfig.Config{
		Enabled: true, BaseURL: stub.URL, APIKey: "k",
	}); err != nil {
		t.Fatalf("save config: %v", err)
	}
	clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
	svc := NewService(pool, cfg, clientFn)

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

	audit, deleted, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
	if err != nil {
		t.Fatalf("DeleteViaLidarr: %v", err)
	}
	if deleted != 1 {
		t.Errorf("deleted = %d, want 1 track removed", deleted)
	}
	if audit.Action != dbq.LidarrQuarantineActionKindDeletedViaLidarr {
		t.Errorf("action = %v", audit.Action)
	}
	if audit.AffectedUsers != 1 {
		t.Errorf("affected_users = %d, want 1", audit.AffectedUsers)
	}
	if audit.LidarrAlbumMbid == nil || *audit.LidarrAlbumMbid != "al-mbid" {
		t.Errorf("lidarr_album_mbid = %v", audit.LidarrAlbumMbid)
	}
	if _, err := q.GetTrackByID(context.Background(), track.ID); err == nil {
		t.Errorf("track row still exists")
	}
	// Verify Lidarr received DELETE with both flags true (query order may vary).
	foundDelete := false
	for _, c := range captured {
		if c == "DELETE /api/v1/album/42?addImportListExclusion=true&deleteFiles=true" ||
			c == "DELETE /api/v1/album/42?deleteFiles=true&addImportListExclusion=true" {
			foundDelete = true
		}
	}
	if !foundDelete {
		t.Errorf("Lidarr DELETE not called with both flags true; captured = %v", captured)
	}
}

func TestDeleteViaLidarr_LidarrDisabled(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")
	track, _, _ := seedTrack(t, pool, "T", "x")

	svc := NewService(pool, lidarrconfig.New(pool), nil)
	_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
	if !errors.Is(err, ErrLidarrDisabled) {
		t.Errorf("err = %v, want ErrLidarrDisabled", err)
	}
}

func TestDeleteViaLidarr_AlbumMBIDMissing(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")

	q := dbq.New(pool)
	artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "A", SortName: "A"})
	// Album with NO mbid set.
	album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: "Al", SortTitle: "Al", ArtistID: artist.ID, // Mbid: nil
	})
	dir := t.TempDir()
	path := filepath.Join(dir, "T.mp3")
	_ = os.WriteFile(path, []byte("x"), 0o644)
	track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: "T", AlbumID: album.ID, ArtistID: artist.ID,
		DurationMs: 1000, FilePath: path, FileSize: 1, FileFormat: "mp3",
	})

	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(stub.Close)
	cfg := lidarrconfig.New(pool)
	_ = cfg.Save(context.Background(), lidarrconfig.Config{Enabled: true, BaseURL: stub.URL, APIKey: "k"})
	clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
	svc := NewService(pool, cfg, clientFn)
	_, _ = svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "")

	_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
	if !errors.Is(err, ErrAlbumMBIDMissing) {
		t.Errorf("err = %v, want ErrAlbumMBIDMissing", err)
	}
}

func TestDeleteViaLidarr_LidarrAlbumNotFound(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")

	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		if r.URL.Path == "/api/v1/album" && r.Method == http.MethodGet {
			_, _ = w.Write([]byte("[]")) // Lidarr returns empty -> ErrNotFound
			return
		}
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(stub.Close)
	cfg := lidarrconfig.New(pool)
	_ = cfg.Save(context.Background(), lidarrconfig.Config{Enabled: true, BaseURL: stub.URL, APIKey: "k"})
	clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
	svc := NewService(pool, cfg, clientFn)

	track, _, _ := seedTrack(t, pool, "T", "x")
	_, _ = svc.Flag(context.Background(), user.ID, track.ID, "bad_rip", "")

	_, _, err := svc.DeleteViaLidarr(context.Background(), track.ID, user.ID)
	if !errors.Is(err, ErrLidarrAlbumNotFound) {
		t.Errorf("err = %v, want ErrLidarrAlbumNotFound", err)
	}
}

func TestDeleteFile_TrackNotFound(t *testing.T) {
	pool := newPool(t)
	user := seedUser(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, lidarrconfig.New(pool), nil)
	_, err := svc.DeleteFile(context.Background(), bogus, user.ID)
	if !errors.Is(err, ErrTrackNotFound) {
		t.Errorf("err = %v, want ErrTrackNotFound", err)
	}
}

func TestDeleteViaLidarr_TrackNotFound(t *testing.T) {
	pool := newPool(t)
	user := seedUser(t, pool, "alice")

	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(stub.Close)
	cfg := lidarrconfig.New(pool)
	if err := cfg.Save(context.Background(), lidarrconfig.Config{
		Enabled: true, BaseURL: stub.URL, APIKey: "k",
	}); err != nil {
		t.Fatalf("save config: %v", err)
	}
	clientFn := func() *lidarr.Client { return lidarr.NewClient(stub.URL, "k") }
	svc := NewService(pool, cfg, clientFn)

	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

	_, _, err := svc.DeleteViaLidarr(context.Background(), bogus, user.ID)
	if !errors.Is(err, ErrTrackNotFound) {
		t.Errorf("err = %v, want ErrTrackNotFound", err)
	}
}
