🏑 index : ironmagma/quintodrome.git

package artwork

import (
	"context"
	"errors"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"time"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/core"
	"github.com/navidrome/navidrome/model"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("artistArtworkReader", func() {
	var _ = Describe("loadArtistFolder", func() {
		var (
			ctx             context.Context
			fds             *fakeDataStore
			repo            *fakeFolderRepo
			albums          model.Albums
			paths           []string
			now             time.Time
			expectedUpdTime time.Time
		)

		BeforeEach(func() {
			ctx = context.Background()
			DeferCleanup(stubCoreAbsolutePath())

			now = time.Now().Truncate(time.Second)
			expectedUpdTime = now.Add(5 * time.Minute)
			repo = &fakeFolderRepo{
				result: []model.Folder{
					{
						ImagesUpdatedAt: expectedUpdTime,
					},
				},
				err: nil,
			}
			fds = &fakeDataStore{
				folderRepo: repo,
			}
			albums = model.Albums{
				{LibraryID: 1, ID: "album1", Name: "Album 1"},
			}
		})

		When("no albums provided", func() {
			It("returns empty and zero time", func() {
				folder, upd, err := loadArtistFolder(ctx, fds, model.Albums{}, []string{"/dummy/path"})
				Expect(err).ToNot(HaveOccurred())
				Expect(folder).To(BeEmpty())
				Expect(upd).To(BeZero())
			})
		})

		When("artist has only one album", func() {
			It("returns the parent folder", func() {
				paths = []string{
					filepath.FromSlash("/music/artist/album1"),
				}
				folder, upd, err := loadArtistFolder(ctx, fds, albums, paths)
				Expect(err).ToNot(HaveOccurred())
				Expect(folder).To(Equal(filepath.FromSlash("/music/artist")))
				Expect(upd).To(Equal(expectedUpdTime))
			})
		})

		When("the artist have multiple albums", func() {
			It("returns the common prefix for the albums paths", func() {
				paths = []string{
					filepath.FromSlash("/music/library/artist/one"),
					filepath.FromSlash("/music/library/artist/two"),
				}
				folder, upd, err := loadArtistFolder(ctx, fds, albums, paths)
				Expect(err).ToNot(HaveOccurred())
				Expect(folder).To(Equal(filepath.FromSlash("/music/library/artist")))
				Expect(upd).To(Equal(expectedUpdTime))
			})
		})

		When("the album paths contain same prefix", func() {
			It("returns the common prefix", func() {
				paths = []string{
					filepath.FromSlash("/music/artist/album1"),
					filepath.FromSlash("/music/artist/album2"),
				}
				folder, upd, err := loadArtistFolder(ctx, fds, albums, paths)
				Expect(err).ToNot(HaveOccurred())
				Expect(folder).To(Equal(filepath.FromSlash("/music/artist")))
				Expect(upd).To(Equal(expectedUpdTime))
			})
		})

		When("ds.Folder().GetAll returns an error", func() {
			It("returns an error", func() {
				paths = []string{
					filepath.FromSlash("/music/artist/album1"),
					filepath.FromSlash("/music/artist/album2"),
				}
				repo.err = errors.New("fake error")
				folder, upd, err := loadArtistFolder(ctx, fds, albums, paths)
				Expect(err).To(MatchError(ContainSubstring("fake error")))
				// Folder and time are empty on error.
				Expect(folder).To(BeEmpty())
				Expect(upd).To(BeZero())
			})
		})
	})

	var _ = Describe("fromArtistFolder", func() {
		var (
			ctx      context.Context
			tempDir  string
			libFS    fs.FS
			testFunc sourceFunc
		)

		BeforeEach(func() {
			ctx = context.Background()
			tempDir = GinkgoT().TempDir()
			libFS = os.DirFS(tempDir)
		})

		When("artist folder contains matching image", func() {
			BeforeEach(func() {
				// Create test structure: /temp/artist/artist.jpg
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				artistImagePath := filepath.Join(artistDir, "artist.jpg")
				Expect(os.WriteFile(artistImagePath, []byte("fake image data"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("finds and returns the image", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("artist.jpg"))

				// Verify we can read the content
				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("fake image data"))
				reader.Close()
			})
		})

		When("artist folder name contains glob metacharacters", func() {
			BeforeEach(func() {
				artistDir := filepath.Join(tempDir, "Artist [Live]")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				artistImagePath := filepath.Join(artistDir, "artist.jpg")
				Expect(os.WriteFile(artistImagePath, []byte("bracketed artist image"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("treats the folder path literally when globbing through the library fs", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("Artist [Live]" + string(filepath.Separator) + "artist.jpg"))

				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("bracketed artist image"))
				reader.Close()
			})
		})

		When("artist folder is empty but parent contains image", func() {
			BeforeEach(func() {
				// Create test structure: /temp/parent/artist.jpg and /temp/parent/artist/album/
				parentDir := filepath.Join(tempDir, "parent")
				artistDir := filepath.Join(parentDir, "artist")
				albumDir := filepath.Join(artistDir, "album")
				Expect(os.MkdirAll(albumDir, 0755)).To(Succeed())

				// Put artist image in parent directory
				artistImagePath := filepath.Join(parentDir, "artist.jpg")
				Expect(os.WriteFile(artistImagePath, []byte("parent image"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("finds image in parent directory", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("parent" + string(filepath.Separator) + "artist.jpg"))

				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("parent image"))
				reader.Close()
			})
		})

		When("image is two levels up", func() {
			BeforeEach(func() {
				// Create test structure: /temp/grandparent/artist.jpg and /temp/grandparent/parent/artist/
				grandparentDir := filepath.Join(tempDir, "grandparent")
				parentDir := filepath.Join(grandparentDir, "parent")
				artistDir := filepath.Join(parentDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Put artist image in grandparent directory
				artistImagePath := filepath.Join(grandparentDir, "artist.jpg")
				Expect(os.WriteFile(artistImagePath, []byte("grandparent image"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("finds image in grandparent directory", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("grandparent" + string(filepath.Separator) + "artist.jpg"))

				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("grandparent image"))
				reader.Close()
			})
		})

		When("images exist at multiple levels", func() {
			BeforeEach(func() {
				// Create test structure with images at multiple levels
				grandparentDir := filepath.Join(tempDir, "grandparent")
				parentDir := filepath.Join(grandparentDir, "parent")
				artistDir := filepath.Join(parentDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Put artist images at all levels
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.jpg"), []byte("artist level"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(parentDir, "artist.jpg"), []byte("parent level"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(grandparentDir, "artist.jpg"), []byte("grandparent level"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("prioritizes the closest (artist folder) image", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("artist" + string(filepath.Separator) + "artist.jpg"))

				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("artist level"))
				reader.Close()
			})
		})

		When("pattern matches multiple files", func() {
			BeforeEach(func() {
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Create multiple matching files
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.abc"), []byte("text file"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.png"), []byte("png image"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.jpg"), []byte("jpg image"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("returns the first valid image file in sorted order", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())

				// Should return an image file,
				// Files are sorted: jpg comes before png alphabetically.
				// .abc comes first, but it's not an image.
				Expect(path).To(ContainSubstring("artist.jpg"))
				reader.Close()
			})
		})

		When("prioritizing files without numeric suffixes", func() {
			BeforeEach(func() {
				// Test case for issue #4683: artist.jpg should come before artist.1.jpg
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Create multiple matches with and without numeric suffixes
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.1.jpg"), []byte("artist 1"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.jpg"), []byte("artist main"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.2.jpg"), []byte("artist 2"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("returns artist.jpg before artist.1.jpg and artist.2.jpg", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("artist.jpg"))

				// Verify it's the main file, not a numbered variant
				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("artist main"))
				reader.Close()
			})
		})

		When("handling case-insensitive sorting", func() {
			BeforeEach(func() {
				// Test case to ensure case-insensitive natural sorting
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Create files with mixed case names
				Expect(os.WriteFile(filepath.Join(artistDir, "Folder.jpg"), []byte("folder"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "artist.jpg"), []byte("artist"), 0600)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(artistDir, "BACK.jpg"), []byte("back"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "*.*")
			})

			It("sorts case-insensitively", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())

				// Should return artist.jpg first (case-insensitive: "artist" < "back" < "folder")
				Expect(path).To(ContainSubstring("artist.jpg"))

				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("artist"))
				reader.Close()
			})
		})

		When("no matching files exist anywhere", func() {
			BeforeEach(func() {
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Create non-matching files
				Expect(os.WriteFile(filepath.Join(artistDir, "cover.jpg"), []byte("cover image"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("returns an error", func() {
				reader, path, err := testFunc()
				Expect(err).To(HaveOccurred())
				Expect(reader).To(BeNil())
				Expect(path).To(BeEmpty())
				Expect(err.Error()).To(ContainSubstring("no matches for 'artist.*'"))
				Expect(err.Error()).To(ContainSubstring("parent directories"))
			})
		})

		When("directory traversal reaches filesystem root", func() {
			BeforeEach(func() {
				// Start from a shallow directory to test root boundary
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("handles root boundary gracefully", func() {
				reader, path, err := testFunc()
				Expect(err).To(HaveOccurred())
				Expect(reader).To(BeNil())
				Expect(path).To(BeEmpty())
				// Should not panic or cause infinite loop
			})
		})

		When("file exists but cannot be opened", func() {
			BeforeEach(func() {
				artistDir := filepath.Join(tempDir, "artist")
				Expect(os.MkdirAll(artistDir, 0755)).To(Succeed())

				// Create a file that cannot be opened (permission denied)
				restrictedFile := filepath.Join(artistDir, "artist.jpg")
				Expect(os.WriteFile(restrictedFile, []byte("restricted"), 0600)).To(Succeed())

				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("logs warning and continues searching", func() {
				// This test depends on the ability to restrict file permissions
				// For now, we'll just ensure it doesn't panic and returns appropriate error
				reader, _, err := testFunc()
				// The file should be readable in test environment, so this will succeed
				// In a real scenario with permission issues, it would continue searching
				if err == nil {
					Expect(reader).ToNot(BeNil())
					reader.Close()
				}
			})
		})

		When("single album artist scenario (original issue)", func() {
			BeforeEach(func() {
				// Simulate the exact folder structure from the issue:
				// /music/artist/album1/ (single album)
				// /music/artist/artist.jpg (artist image that should be found)
				artistDir := filepath.Join(tempDir, "music", "artist")
				albumDir := filepath.Join(artistDir, "album1")
				Expect(os.MkdirAll(albumDir, 0755)).To(Succeed())

				// Create artist.jpg in the artist folder (this was not being found before)
				artistImagePath := filepath.Join(artistDir, "artist.jpg")
				Expect(os.WriteFile(artistImagePath, []byte("single album artist image"), 0600)).To(Succeed())

				// The fromArtistFolder is called with the artist folder path
				testFunc = fromArtistFolder(ctx, libFS, tempDir, artistDir, "artist.*")
			})

			It("finds artist.jpg in artist folder for single album artist", func() {
				reader, path, err := testFunc()
				Expect(err).ToNot(HaveOccurred())
				Expect(reader).ToNot(BeNil())
				Expect(path).To(ContainSubstring("artist.jpg"))
				Expect(path).To(ContainSubstring("artist"))

				// Verify the content
				data, err := io.ReadAll(reader)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("single album artist image"))
				reader.Close()
			})
		})
	})

	Describe("fromArtistUploadedImage", func() {
		var (
			tempDir string
			reader  *artistReader
		)

		BeforeEach(func() {
			DeferCleanup(configtest.SetupConfig())
			tempDir = GinkgoT().TempDir()
			conf.Server.DataFolder = conf.NewDir(tempDir)

			// Create the artwork/artist directory
			Expect(os.MkdirAll(filepath.Join(tempDir, "artwork", "artist"), 0755)).To(Succeed())

			reader = &artistReader{}
		})

		When("artist has an uploaded image", func() {
			It("returns the uploaded image", func() {
				imgPath := filepath.Join(tempDir, "artwork", "artist", "ar-1_test.jpg")
				Expect(os.WriteFile(imgPath, []byte("uploaded artist image"), 0600)).To(Succeed())

				reader.artist = model.Artist{ID: "ar-1", UploadedImage: "ar-1_test.jpg"}
				sf := reader.fromArtistUploadedImage()
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))

				data, err := io.ReadAll(r)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("uploaded artist image"))
				r.Close()
			})
		})

		When("artist has no uploaded image", func() {
			It("returns nil reader (falls through)", func() {
				reader.artist = model.Artist{ID: "ar-1"}
				sf := reader.fromArtistUploadedImage()
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).To(BeNil())
				Expect(path).To(BeEmpty())
			})
		})
	})

	Describe("fromArtistImageFolder", func() {
		var (
			ctx     context.Context
			tempDir string
			ar      *artistReader
		)

		BeforeEach(func() {
			ctx = context.Background()
			DeferCleanup(configtest.SetupConfig())
			tempDir = GinkgoT().TempDir()
			ar = &artistReader{}
		})

		When("ArtistImageFolder is not configured", func() {
			It("returns nil (skips)", func() {
				conf.Server.ArtistImageFolder = ""
				ar.artist = model.Artist{Name: "Test Artist"}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).To(BeNil())
				Expect(path).To(BeEmpty())
			})
		})

		When("image exists matching MBID", func() {
			It("finds the image by MBID", func() {
				conf.Server.ArtistImageFolder = tempDir
				mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e"
				imgPath := filepath.Join(tempDir, mbid+".jpg")
				Expect(os.WriteFile(imgPath, []byte("mbid image"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))

				data, err := io.ReadAll(r)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("mbid image"))
				r.Close()
			})
		})

		When("MBID match is case-insensitive", func() {
			It("finds the image regardless of case", func() {
				conf.Server.ArtistImageFolder = tempDir
				mbid := "F27EC8DB-AF05-4F36-916E-3D57F91ECF5E"
				imgPath := filepath.Join(tempDir, "f27ec8db-af05-4f36-916e-3d57f91ecf5e.png")
				Expect(os.WriteFile(imgPath, []byte("mbid case image"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))
				r.Close()
			})
		})

		When("no MBID file exists but artist name file does", func() {
			It("falls back to artist name match", func() {
				conf.Server.ArtistImageFolder = tempDir
				imgPath := filepath.Join(tempDir, "Test Artist.jpg")
				Expect(os.WriteFile(imgPath, []byte("name image"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: "nonexistent-mbid"}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))

				data, err := io.ReadAll(r)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("name image"))
				r.Close()
			})
		})

		When("artist name match is case-insensitive", func() {
			It("matches regardless of case", func() {
				conf.Server.ArtistImageFolder = tempDir
				imgPath := filepath.Join(tempDir, "test artist.jpg")
				Expect(os.WriteFile(imgPath, []byte("case insensitive"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist"}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))
				r.Close()
			})
		})

		When("both MBID and name files exist", func() {
			It("prefers MBID over name match", func() {
				conf.Server.ArtistImageFolder = tempDir
				mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e"
				mbidPath := filepath.Join(tempDir, mbid+".jpg")
				namePath := filepath.Join(tempDir, "Test Artist.jpg")
				Expect(os.WriteFile(mbidPath, []byte("mbid image"), 0600)).To(Succeed())
				Expect(os.WriteFile(namePath, []byte("name image"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid}
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(mbidPath))

				data, err := io.ReadAll(r)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("mbid image"))
				r.Close()
			})
		})

		When("no matching image found", func() {
			It("returns an error", func() {
				conf.Server.ArtistImageFolder = tempDir
				// Create an unrelated file
				Expect(os.WriteFile(filepath.Join(tempDir, "other.jpg"), []byte("other"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist"}
				sf := ar.fromArtistImageFolder(ctx)
				r, _, err := sf()
				Expect(err).To(HaveOccurred())
				Expect(r).To(BeNil())
				Expect(err.Error()).To(ContainSubstring("no image found"))
			})
		})

		When("cached imgFolderImgPath is set", func() {
			It("uses cached path instead of scanning", func() {
				conf.Server.ArtistImageFolder = tempDir
				imgPath := filepath.Join(tempDir, "cached.jpg")
				Expect(os.WriteFile(imgPath, []byte("cached image"), 0600)).To(Succeed())

				ar.artist = model.Artist{Name: "Test Artist"}
				ar.imgFolderImgPath = imgPath
				sf := ar.fromArtistImageFolder(ctx)
				r, path, err := sf()
				Expect(err).ToNot(HaveOccurred())
				Expect(r).ToNot(BeNil())
				Expect(path).To(Equal(imgPath))

				data, err := io.ReadAll(r)
				Expect(err).ToNot(HaveOccurred())
				Expect(string(data)).To(Equal("cached image"))
				r.Close()
			})
		})
	})

	Describe("findImageInArtistFolder", func() {
		var tempDir string

		BeforeEach(func() {
			tempDir = GinkgoT().TempDir()
		})

		When("matching file exists by MBID", func() {
			It("returns the file path", func() {
				mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e"
				imgPath := filepath.Join(tempDir, mbid+".jpg")
				Expect(os.WriteFile(imgPath, []byte("image"), 0600)).To(Succeed())

				path := findImageInArtistFolder(tempDir, mbid, "Test")
				Expect(path).To(Equal(imgPath))
			})
		})

		When("matching file exists by name", func() {
			It("returns the file path", func() {
				imgPath := filepath.Join(tempDir, "Test Artist.png")
				Expect(os.WriteFile(imgPath, []byte("image"), 0600)).To(Succeed())

				path := findImageInArtistFolder(tempDir, "", "Test Artist")
				Expect(path).To(Equal(imgPath))
			})
		})

		When("no matching file exists", func() {
			It("returns empty string", func() {
				path := findImageInArtistFolder(tempDir, "", "Unknown Artist")
				Expect(path).To(BeEmpty())
			})
		})

		When("folder does not exist", func() {
			It("returns empty string", func() {
				path := findImageInArtistFolder("/nonexistent/path", "", "Test")
				Expect(path).To(BeEmpty())
			})
		})
	})
})

type fakeFolderRepo struct {
	model.FolderRepository
	result       []model.Folder
	parentResult *model.Folder
	getErr       error
	getCallCount int
	err          error
}

func (f *fakeFolderRepo) GetAll(...model.QueryOptions) ([]model.Folder, error) {
	return f.result, f.err
}

func (f *fakeFolderRepo) Get(id string) (*model.Folder, error) {
	f.getCallCount++
	if f.getErr != nil {
		return nil, f.getErr
	}
	if f.parentResult != nil {
		return f.parentResult, nil
	}
	return nil, model.ErrNotFound
}

type fakeDataStore struct {
	model.DataStore
	folderRepo *fakeFolderRepo
}

func (fds *fakeDataStore) Folder(_ context.Context) model.FolderRepository {
	return fds.folderRepo
}

func stubCoreAbsolutePath() func() {
	// Override core.AbsolutePath to return a fixed string during tests.
	original := core.AbsolutePath
	core.AbsolutePath = func(_ context.Context, ds model.DataStore, libID int, p string) string {
		return filepath.FromSlash("/music")
	}
	return func() {
		core.AbsolutePath = original
	}
}