package lidarrrequests

import (
	"context"
	"io"
	"log/slog"
	"testing"
	"time"

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

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

// seedApprovedRequestDirect inserts a pending request and immediately
// approves it via ApproveLidarrRequest, bypassing the Lidarr API call.
func seedApprovedRequestDirect(t *testing.T, q *dbq.Queries, userID pgtype.UUID, p CreateParams) dbq.LidarrRequest {
	t.Helper()
	ctx := context.Background()
	row, err := q.CreateLidarrRequest(ctx, dbq.CreateLidarrRequestParams{
		UserID:           userID,
		Kind:             dbq.LidarrRequestKind(p.Kind),
		LidarrArtistMbid: p.LidarrArtistMBID,
		LidarrAlbumMbid:  nilableStr(p.LidarrAlbumMBID),
		LidarrTrackMbid:  nilableStr(p.LidarrTrackMBID),
		ArtistName:       p.ArtistName,
		AlbumTitle:       nilableStr(p.AlbumTitle),
		TrackTitle:       nilableStr(p.TrackTitle),
	})
	if err != nil {
		t.Fatalf("seedApprovedRequestDirect create: %v", err)
	}
	approved, err := q.ApproveLidarrRequest(ctx, dbq.ApproveLidarrRequestParams{
		ID:               row.ID,
		QualityProfileID: nil,
		RootFolderPath:   nil,
		DecidedBy:        userID,
	})
	if err != nil {
		t.Fatalf("seedApprovedRequestDirect approve: %v", err)
	}
	return approved
}

func nilableStr(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

func seedArtist(t *testing.T, q *dbq.Queries, name, mbid string) dbq.Artist {
	t.Helper()
	a, err := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name: name, SortName: name, Mbid: &mbid,
	})
	if err != nil {
		t.Fatalf("seedArtist: %v", err)
	}
	return a
}

func seedAlbum(t *testing.T, q *dbq.Queries, artistID pgtype.UUID, title, mbid string) dbq.Album {
	t.Helper()
	a, err := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
		Title: title, SortTitle: title,
		ArtistID: artistID,
		Mbid:     &mbid,
	})
	if err != nil {
		t.Fatalf("seedAlbum: %v", err)
	}
	return a
}

func seedTrack(t *testing.T, q *dbq.Queries, albumID, artistID pgtype.UUID, title, filePath string) dbq.Track {
	t.Helper()
	tr, err := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
		Title: title, AlbumID: albumID, ArtistID: artistID,
		DurationMs: 180000, FilePath: filePath,
		FileSize: 1024, FileFormat: "flac",
	})
	if err != nil {
		t.Fatalf("seedTrack: %v", err)
	}
	return tr
}

func enableLidarrForPool(t *testing.T, pool *pgxpool.Pool) {
	t.Helper()
	if err := lidarrconfig.New(pool).Save(context.Background(), lidarrconfig.Config{
		Enabled: true, BaseURL: "http://lidarr:8686", APIKey: "test",
	}); err != nil {
		t.Fatalf("enableLidarrForPool: %v", err)
	}
}

func newTestLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

// TestReconciler_MatchesArtistByMBID: an approved artist-kind request whose
// MBID is in the artists table gets completed with matched_artist_id set.
func TestReconciler_MatchesArtistByMBID(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)
	artistMBID := "069b64b6-7884-4f6a-94cc-e4c1d6c87a01"
	artist := seedArtist(t, q, "Boards of Canada", artistMBID)

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "artist", LidarrArtistMBID: artistMBID, ArtistName: "Boards of Canada",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusCompleted {
		t.Errorf("status = %v, want completed", got.Status)
	}
	if got.MatchedArtistID != artist.ID {
		t.Errorf("matched_artist_id = %v, want %v", got.MatchedArtistID, artist.ID)
	}
}

// TestReconciler_MatchesAlbumByMBID: an approved album-kind request whose
// album MBID is in the albums table gets completed with matched_album_id set.
func TestReconciler_MatchesAlbumByMBID(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)
	artistMBID := "a-artist-mbid-album-test"
	albumMBID := "b-album-mbid-album-test"

	artist := seedArtist(t, q, "Test Artist", artistMBID)
	album := seedAlbum(t, q, artist.ID, "Test Album", albumMBID)

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "album", LidarrArtistMBID: artistMBID, ArtistName: "Test Artist",
		LidarrAlbumMBID: albumMBID, AlbumTitle: "Test Album",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusCompleted {
		t.Errorf("status = %v, want completed", got.Status)
	}
	if got.MatchedAlbumID != album.ID {
		t.Errorf("matched_album_id = %v, want %v", got.MatchedAlbumID, album.ID)
	}
}

// TestReconciler_MatchesTrackViaAlbumMBID: an approved track-kind request
// matches when the parent album appears in the library. matched_track_id is
// also set to a track from that album. The track MBID on the request does
// not need to match any tracks.mbid — matching is via the parent album.
func TestReconciler_MatchesTrackViaAlbumMBID(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)
	artistMBID := "a-artist-mbid-track-test"
	albumMBID := "b-album-mbid-track-test"
	trackMBID := "c-track-mbid-does-not-exist-in-tracks-table"

	artist := seedArtist(t, q, "Track Test Artist", artistMBID)
	album := seedAlbum(t, q, artist.ID, "Track Test Album", albumMBID)
	track := seedTrack(t, q, album.ID, artist.ID, "Track One", "/music/track-test/01.flac")

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "track", LidarrArtistMBID: artistMBID, ArtistName: "Track Test Artist",
		LidarrAlbumMBID: albumMBID, AlbumTitle: "Track Test Album",
		LidarrTrackMBID: trackMBID, TrackTitle: "Track One",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusCompleted {
		t.Errorf("status = %v, want completed", got.Status)
	}
	if got.MatchedAlbumID != album.ID {
		t.Errorf("matched_album_id = %v, want %v", got.MatchedAlbumID, album.ID)
	}
	if got.MatchedTrackID != track.ID {
		t.Errorf("matched_track_id = %v, want %v", got.MatchedTrackID, track.ID)
	}
}

// TestReconciler_NoMatchLeavesPending: an approved request whose MBID is not
// in any library table is left unchanged after tickOnce.
func TestReconciler_NoMatchLeavesPending(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "artist", LidarrArtistMBID: "nonexistent-mbid-xyz", ArtistName: "Ghost Artist",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusApproved {
		t.Errorf("status = %v, want approved (unchanged)", got.Status)
	}
	if got.MatchedArtistID.Valid {
		t.Errorf("matched_artist_id should be NULL, got %v", got.MatchedArtistID)
	}
}

// TestReconciler_AlreadyCompletedRowNotReprocessed: a completed row is not
// touched by the reconciler because ListApprovedLidarrRequestsForReconcile
// filters on status='approved'.
func TestReconciler_AlreadyCompletedRowNotReprocessed(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)
	artistMBID := "already-done-artist-mbid"
	artist := seedArtist(t, q, "Already Done Artist", artistMBID)

	// Seed approved, then complete it before running tickOnce.
	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "artist", LidarrArtistMBID: artistMBID, ArtistName: "Already Done Artist",
	})
	completed, err := q.CompleteLidarrRequest(ctx, dbq.CompleteLidarrRequestParams{
		ID:              req.ID,
		MatchedArtistID: artist.ID,
		MatchedAlbumID:  pgtype.UUID{},
		MatchedTrackID:  pgtype.UUID{},
	})
	if err != nil {
		t.Fatalf("CompleteLidarrRequest setup: %v", err)
	}

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusCompleted {
		t.Errorf("status = %v, want completed (unchanged)", got.Status)
	}
	if !got.UpdatedAt.Time.Equal(completed.UpdatedAt.Time) {
		t.Errorf("updated_at moved: was %v, now %v — reconciler should not have touched completed row",
			completed.UpdatedAt.Time, got.UpdatedAt.Time)
	}
}

// TestReconciler_DisabledIsNoOp: when lidarr_config.enabled=false the
// reconciler skips the entire tick, leaving approved requests untouched.
func TestReconciler_DisabledIsNoOp(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	// lidarr_config is left disabled (newPool sets it to false).
	user := seedUser(t, pool)
	artistMBID := "disabled-noop-mbid"
	// Seed the matching artist so it WOULD be found if lidarr were enabled.
	seedArtist(t, q, "No-Op Artist", artistMBID)

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "artist", LidarrArtistMBID: artistMBID, ArtistName: "No-Op Artist",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusApproved {
		t.Errorf("status = %v, want approved — reconciler should no-op when disabled", got.Status)
	}
}

// TestReconciler_RunRunsAtLeastOneTick exercises Run's goroutine loop with a
// short tick, verifies it picks up an approved row and transitions it to
// completed before the test cancels the context.
func TestReconciler_RunRunsAtLeastOneTick(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	pool := newPool(t)
	enableLidarrForPool(t, pool)
	q := dbq.New(pool)
	user := seedUser(t, pool)

	const mbid = "run-mbid"
	seedArtist(t, q, "Run Artist", mbid)
	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "artist", LidarrArtistMBID: mbid, ArtistName: "Run Artist",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	rec.tick = 25 * time.Millisecond

	done := make(chan struct{})
	go func() {
		rec.Run(ctx)
		close(done)
	}()

	// Poll the row up to ~2s for the reconciler to flip it. Beats sleeping
	// a fixed amount on slow CI.
	deadline := time.Now().Add(2 * time.Second)
	for {
		got, err := q.GetLidarrRequestByID(ctx, req.ID)
		if err == nil && got.Status == dbq.LidarrRequestStatusCompleted {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("status not completed within 2s; last err=%v", err)
		}
		time.Sleep(10 * time.Millisecond)
	}

	cancel()
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("Run did not exit within 1s of cancel")
	}
}

// TestReconciler_TrackKindNoTracksInAlbumYet: the parent album exists in the
// library but no track rows have been ingested yet. Reconciler should leave
// the row approved so a subsequent tick can pick it up once tracks land.
func TestReconciler_TrackKindNoTracksInAlbumYet(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)
	artistMBID := "ar-mbid-tracks-pending"
	albumMBID := "al-mbid-tracks-pending"

	artist := seedArtist(t, q, "Pending Tracks Artist", artistMBID)
	_ = seedAlbum(t, q, artist.ID, "Album Without Tracks", albumMBID)
	// No tracks seeded.

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "track", LidarrArtistMBID: artistMBID, ArtistName: "Pending Tracks Artist",
		LidarrAlbumMBID: albumMBID, AlbumTitle: "Album Without Tracks",
		LidarrTrackMBID: "t-mbid-not-yet", TrackTitle: "Some Track",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusApproved {
		t.Errorf("status = %v, want approved (album present, no tracks yet)", got.Status)
	}
}

// TestReconciler_AlbumKindNoMatchInAlbumsTable exercises the album branch's
// no-rows return path when the request's album_mbid isn't in the library yet.
func TestReconciler_AlbumKindNoMatchInAlbumsTable(t *testing.T) {
	pool := newPool(t)
	q := dbq.New(pool)
	ctx := context.Background()

	enableLidarrForPool(t, pool)
	user := seedUser(t, pool)

	req := seedApprovedRequestDirect(t, q, user, CreateParams{
		Kind: "album", LidarrArtistMBID: "ar-no-match", ArtistName: "Nope Artist",
		LidarrAlbumMBID: "al-not-in-library", AlbumTitle: "Nope Album",
	})

	rec := NewReconciler(pool, lidarrconfig.New(pool), nil, newTestLogger(), nil)
	if err := rec.tickOnce(ctx); err != nil {
		t.Fatalf("tickOnce: %v", err)
	}

	got, err := q.GetLidarrRequestByID(ctx, req.ID)
	if err != nil {
		t.Fatalf("GetLidarrRequestByID: %v", err)
	}
	if got.Status != dbq.LidarrRequestStatusApproved {
		t.Errorf("status = %v, want approved (album not yet in library)", got.Status)
	}
}
