package artwork

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

	"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("/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("/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
			testFunc sourceFunc
		)

		BeforeEach(func() {
			ctx = context.Background()
			tempDir = GinkgoT().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, 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 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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()
			})
		})
	})
})

type fakeFolderRepo struct {
	model.FolderRepository
	result []model.Folder
	err    error
}

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

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
	}
}
