package coverart

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

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

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

// stubArtistProvider is a configurable ArtistArtProvider for artist enricher
// tests. Embeds fakeProvider for the base Provider methods; overrides
// FetchArtistArt with controllable byte/error responses.
type stubArtistProvider struct {
	fakeProvider
	thumb, fanart []byte
	err           error
}

func (f *stubArtistProvider) FetchArtistArt(_ context.Context, _ ArtistRef) ([]byte, []byte, error) {
	return f.thumb, f.fanart, f.err
}

// insertArtist seeds a fresh artist via UpsertArtist and returns its UUID.
// mbid may be "" to seed a MBID-less artist.
func insertArtist(t *testing.T, q *dbq.Queries, ctx context.Context, name, mbid string) pgtype.UUID {
	t.Helper()
	var mbidParam *string
	if mbid != "" {
		mbidParam = &mbid
	}
	a, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{
		Name:     name,
		SortName: name,
		Mbid:     mbidParam,
	})
	if err != nil {
		t.Fatalf("insertArtist %q: %v", name, err)
	}
	return a.ID
}

// --- Eligibility tests ---

func TestEnrichArtist_NullSource_Eligible(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
		thumb:        []byte("thumb_bytes"),
		fanart:       []byte("fanart_bytes"),
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Null Artist", "11111111-1111-1111-1111-111111111111")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "stub-artist" {
		t.Errorf("source = %v, want 'stub-artist'", row.ArtistArtSource)
	}
}

func TestEnrichArtist_FoundSource_Skip(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
		thumb:        []byte("should_not_write"),
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Found Artist", "22222222-2222-2222-2222-222222222222")

	// Pre-mark with a "found" source.
	src := "theaudiodb"
	if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
		ID:                      artistID,
		ArtistArtSource:         &src,
		ArtistArtSourcesVersion: e.settings.CurrentVersion(),
	}); err != nil {
		t.Fatalf("set: %v", err)
	}

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	// Source must remain 'theaudiodb'; no files should have been written.
	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "theaudiodb" {
		t.Errorf("source = %v, want 'theaudiodb' (preserved)", row.ArtistArtSource)
	}
	thumbPath := filepath.Join(dataDir, "artist-art", uuidString(artistID), "thumb.jpg")
	if _, err := os.Stat(thumbPath); !os.IsNotExist(err) {
		t.Errorf("expected no thumb.jpg (skip), got stat err = %v", err)
	}
}

func TestEnrichArtist_NoneCurrentVersion_Skip(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
		thumb:        []byte("should_not_write"),
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "None Current Artist", "33333333-3333-3333-3333-333333333333")

	src := "none"
	if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
		ID:                      artistID,
		ArtistArtSource:         &src,
		ArtistArtSourcesVersion: e.settings.CurrentVersion(),
	}); err != nil {
		t.Fatalf("set: %v", err)
	}

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	// Source must remain 'none' at current version.
	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
		t.Errorf("source = %v, want 'none' (skip — current version)", row.ArtistArtSource)
	}
}

func TestEnrichArtist_NoneStaleVersion_FlipAndRetry(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
		thumb:        []byte("retry_thumb"),
		fanart:       []byte("retry_fanart"),
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Stale None Artist", "44444444-4444-4444-4444-444444444444")

	// Set 'none' with a stale version (currentVersion - 1).
	staleVersion := e.settings.CurrentVersion() - 1
	src := "none"
	if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
		ID:                      artistID,
		ArtistArtSource:         &src,
		ArtistArtSourcesVersion: staleVersion,
	}); err != nil {
		t.Fatalf("set stale none: %v", err)
	}

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	// Stale 'none' should be flipped and re-enriched; stub succeeds so
	// source becomes the provider ID.
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "stub-artist" {
		t.Errorf("source = %v, want 'stub-artist' (stale none flipped and retried)", row.ArtistArtSource)
	}
}

// --- Atomicity: thumb-only / fanart-only ---

func TestEnrichArtist_ThumbOnly_ThumbPathSetFanartNull(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "thumb-only-provider", defaultOn: true},
		thumb:        []byte("thumb_only"),
		fanart:       nil,
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Thumb Only Artist", "55555555-5555-5555-5555-555555555555")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "thumb-only-provider" {
		t.Errorf("source = %v, want 'thumb-only-provider'", row.ArtistArtSource)
	}
	if row.ArtistThumbPath == nil {
		t.Errorf("artist_thumb_path is nil, want a path")
	}
	if row.ArtistFanartPath != nil {
		t.Errorf("artist_fanart_path = %v, want nil (fanart not provided)", row.ArtistFanartPath)
	}
	if row.ArtistThumbPath != nil {
		body, rerr := os.ReadFile(*row.ArtistThumbPath)
		if rerr != nil {
			t.Fatalf("read thumb: %v", rerr)
		}
		if string(body) != "thumb_only" {
			t.Errorf("thumb content = %q, want 'thumb_only'", string(body))
		}
	}
}

func TestEnrichArtist_FanartOnly_FanartPathSetThumbNull(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "fanart-only-provider", defaultOn: true},
		thumb:        nil,
		fanart:       []byte("fanart_only"),
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Fanart Only Artist", "66666666-6666-6666-6666-666666666666")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "fanart-only-provider" {
		t.Errorf("source = %v, want 'fanart-only-provider'", row.ArtistArtSource)
	}
	if row.ArtistThumbPath != nil {
		t.Errorf("artist_thumb_path = %v, want nil (thumb not provided)", row.ArtistThumbPath)
	}
	if row.ArtistFanartPath == nil {
		t.Errorf("artist_fanart_path is nil, want a path")
	}
}

// --- All-404 settles to 'none' ---

func TestEnrichArtist_All404_SettlesNone(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "404-provider", defaultOn: true},
		err:          ErrNotFound,
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "All404 Artist", "77777777-7777-7777-7777-777777777777")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
		t.Errorf("source = %v, want 'none' (all providers 404)", row.ArtistArtSource)
	}
	if row.ArtistArtSourcesVersion != e.settings.CurrentVersion() {
		t.Errorf("version = %d, want %d", row.ArtistArtSourcesVersion, e.settings.CurrentVersion())
	}
}

// --- Empty providers list settles vacuously to 'none' ---

func TestEnrichArtist_EmptyProviders_SettlesNone(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	// Register a plain provider that does NOT implement ArtistArtProvider —
	// EnabledArtistProviders will return an empty slice.
	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)
	Register(&fakeProvider{id: "album-only", defaultOn: true})

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "No Provider Artist", "88888888-8888-8888-8888-888888888888")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
		t.Errorf("source = %v, want 'none' (vacuous all-404 with empty provider list)", row.ArtistArtSource)
	}
}

// --- ErrTransient leaves row NULL ---

func TestEnrichArtist_ErrTransient_LeavesNull(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "transient-provider", defaultOn: true},
		err:          ErrTransient,
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Transient Artist", "99999999-9999-9999-9999-999999999999")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	// Transient: source must remain NULL (eligible for retry).
	if row.ArtistArtSource != nil {
		t.Errorf("source = %v, want nil (transient should not settle)", row.ArtistArtSource)
	}
}

// --- Filesystem layout ---

func TestEnrichArtist_FilesystemLayout(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	thumbBytes := []byte("thumb_content")
	fanartBytes := []byte("fanart_content")
	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "layout-provider", defaultOn: true},
		thumb:        thumbBytes,
		fanart:       fanartBytes,
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	artistID := insertArtist(t, q, ctx, "Layout Artist", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	artistIDStr := uuidString(artistID)
	thumbPath := filepath.Join(dataDir, "artist-art", artistIDStr, "thumb.jpg")
	fanartPath := filepath.Join(dataDir, "artist-art", artistIDStr, "fanart.jpg")

	thumbBody, rerr := os.ReadFile(thumbPath)
	if rerr != nil {
		t.Fatalf("read thumb.jpg: %v", rerr)
	}
	if string(thumbBody) != string(thumbBytes) {
		t.Errorf("thumb.jpg = %q, want %q", thumbBody, thumbBytes)
	}

	fanartBody, rerr := os.ReadFile(fanartPath)
	if rerr != nil {
		t.Fatalf("read fanart.jpg: %v", rerr)
	}
	if string(fanartBody) != string(fanartBytes) {
		t.Errorf("fanart.jpg = %q, want %q", fanartBody, fanartBytes)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistThumbPath == nil || *row.ArtistThumbPath != thumbPath {
		t.Errorf("DB thumb_path = %v, want %s", row.ArtistThumbPath, thumbPath)
	}
	if row.ArtistFanartPath == nil || *row.ArtistFanartPath != fanartPath {
		t.Errorf("DB fanart_path = %v, want %s", row.ArtistFanartPath, fanartPath)
	}
}

// --- CleanupArtistArt ---

func TestCleanupArtistArt_RemovesDirectory(t *testing.T) {
	dataDir := t.TempDir()

	artistID := pgtype.UUID{
		Bytes: [16]byte{0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb},
		Valid: true,
	}
	artistDir := filepath.Join(dataDir, "artist-art", uuidString(artistID))
	if err := os.MkdirAll(artistDir, 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(artistDir, "thumb.jpg"), []byte("x"), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}

	if err := CleanupArtistArt(dataDir, artistID); err != nil {
		t.Fatalf("CleanupArtistArt: %v", err)
	}

	if _, err := os.Stat(artistDir); !os.IsNotExist(err) {
		t.Errorf("directory still exists after cleanup: stat err = %v", err)
	}
}

func TestCleanupArtistArt_IdempotentMissingDir(t *testing.T) {
	dataDir := t.TempDir()
	artistID := pgtype.UUID{
		Bytes: [16]byte{0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc},
		Valid: true,
	}

	// Directory does not exist — should be a no-op.
	if err := CleanupArtistArt(dataDir, artistID); err != nil {
		t.Fatalf("CleanupArtistArt on missing dir: %v", err)
	}
}

// --- MBID guard ---

func TestEnrichArtist_NoMBID_SettlesNone(t *testing.T) {
	pool := newPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	resetRegistryForTests()
	t.Cleanup(resetRegistryForTests)

	// No MBID still runs the provider chain (name-based providers can
	// resolve without one). An MBID-only provider returns ErrNotFound
	// for an empty MBID; with the whole chain returning ErrNotFound the
	// row settles 'none' (version-stamped), NOT NULL.
	stub := &stubArtistProvider{
		fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
		err:          ErrNotFound,
	}
	Register(stub)

	e := newTestEnricher(t, pool)
	// Seed with no MBID.
	artistID := insertArtist(t, q, ctx, "No MBID Artist", "")

	dataDir := t.TempDir()
	if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
		t.Fatalf("EnrichArtist: %v", err)
	}

	row, err := q.GetArtistByID(ctx, artistID)
	if err != nil {
		t.Fatalf("GetArtistByID: %v", err)
	}
	if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
		t.Errorf("source = %v, want 'none' (settled)", row.ArtistArtSource)
	}
}
