🏡 index : ironmagma/quintodrome.git

package persistence

import (
	"context"
	"slices"
	"strings"

	"github.com/Masterminds/squirrel"
	"github.com/deluan/rest"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/id"
	"github.com/navidrome/navidrome/model/request"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("GenreRepository", func() {
	var repo model.GenreRepository
	var restRepo model.ResourceRepository
	var tagRepo model.TagRepository
	var ctx context.Context

	BeforeEach(func() {
		ctx = request.WithUser(GinkgoT().Context(), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true})
		genreRepo := NewGenreRepository(ctx, GetDBXBuilder())
		repo = genreRepo
		restRepo = genreRepo.(model.ResourceRepository)
		tagRepo = NewTagRepository(ctx, GetDBXBuilder())

		// Clear any existing tags to ensure test isolation
		db := GetDBXBuilder()
		_, err := db.NewQuery("DELETE FROM tag").Execute()
		Expect(err).ToNot(HaveOccurred())

		// Ensure library 1 exists and user has access to it
		_, err = db.NewQuery("INSERT OR IGNORE INTO library (id, name, path, default_new_users) VALUES (1, 'Test Library', '/test', true)").Execute()
		Expect(err).ToNot(HaveOccurred())
		_, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 1)").Execute()
		Expect(err).ToNot(HaveOccurred())

		// Add comprehensive test data that covers all test scenarios
		newTag := func(name, value string) model.Tag {
			return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value}
		}

		err = tagRepo.Add(1,
			newTag("genre", "rock"),
			newTag("genre", "pop"),
			newTag("genre", "jazz"),
			newTag("genre", "electronic"),
			newTag("genre", "classical"),
			newTag("genre", "ambient"),
			newTag("genre", "techno"),
			newTag("genre", "house"),
			newTag("genre", "trance"),
			newTag("genre", "Alternative Rock"),
			newTag("genre", "Blues"),
			newTag("genre", "Country"),
			// These should not be counted as genres
			newTag("mood", "happy"),
			newTag("mood", "ambient"),
		)
		Expect(err).ToNot(HaveOccurred())
	})

	Describe("GetAll", func() {
		It("should return all genres", func() {
			genres, err := repo.GetAll()
			Expect(err).ToNot(HaveOccurred())
			Expect(genres).To(HaveLen(12))

			// Verify that all returned items are genres (TagName = "genre")
			genreNames := make([]string, len(genres))
			for i, genre := range genres {
				genreNames[i] = genre.Name
			}
			Expect(genreNames).To(ContainElement("rock"))
			Expect(genreNames).To(ContainElement("pop"))
			Expect(genreNames).To(ContainElement("jazz"))
			// Should not contain mood tags
			Expect(genreNames).ToNot(ContainElement("happy"))
		})

		It("should support query options", func() {
			// Test with limiting results
			genres, err := repo.GetAll(model.QueryOptions{Max: 1})
			Expect(err).ToNot(HaveOccurred())
			Expect(genres).To(HaveLen(1))
		})

		It("should handle empty results gracefully", func() {
			// Clear all genre tags
			_, err := GetDBXBuilder().NewQuery("DELETE FROM tag WHERE tag_name = 'genre'").Execute()
			Expect(err).ToNot(HaveOccurred())

			genres, err := repo.GetAll()
			Expect(err).ToNot(HaveOccurred())
			Expect(genres).To(BeEmpty())
		})
		Describe("filtering and sorting", func() {
			It("should filter by name using like match", func() {
				// Test filtering by partial name match using the "name" filter which maps to containsFilter("tag_value")
				options := model.QueryOptions{
					Filters: squirrel.Like{"tag_value": "%rock%"}, // Direct field access
				}
				genres, err := repo.GetAll(options)
				Expect(err).ToNot(HaveOccurred())
				Expect(genres).To(HaveLen(2)) // Should match "rock" and "Alternative Rock"

				// Verify all returned genres contain "rock" in their name
				for _, genre := range genres {
					Expect(strings.ToLower(genre.Name)).To(ContainSubstring("rock"))
				}
			})

			It("should sort by name in ascending order", func() {
				// Test sorting by name with the fixed mapping
				options := model.QueryOptions{
					Filters: squirrel.Like{"tag_value": "%e%"}, // Should match genres containing "e"
					Sort:    "name",
				}
				genres, err := repo.GetAll(options)
				Expect(err).ToNot(HaveOccurred())
				Expect(genres).To(HaveLen(7))

				Expect(slices.IsSortedFunc(genres, func(a, b model.Genre) int {
					return strings.Compare(b.Name, a.Name) // Inverted to check descending order
				}))
			})

			It("should sort by name in descending order", func() {
				// Test sorting by name in descending order
				options := model.QueryOptions{
					Filters: squirrel.Like{"tag_value": "%e%"}, // Should match genres containing "e"
					Sort:    "name",
					Order:   "desc",
				}
				genres, err := repo.GetAll(options)
				Expect(err).ToNot(HaveOccurred())
				Expect(genres).To(HaveLen(7))

				Expect(slices.IsSortedFunc(genres, func(a, b model.Genre) int {
					return strings.Compare(a.Name, b.Name)
				}))
			})
		})
	})

	Describe("Count", func() {
		It("should return correct count of genres", func() {
			count, err := restRepo.Count()
			Expect(err).ToNot(HaveOccurred())
			Expect(count).To(Equal(int64(12))) // We have 12 genre tags
		})

		It("should handle zero count", func() {
			// Clear all genre tags
			_, err := GetDBXBuilder().NewQuery("DELETE FROM tag WHERE tag_name = 'genre'").Execute()
			Expect(err).ToNot(HaveOccurred())

			count, err := restRepo.Count()
			Expect(err).ToNot(HaveOccurred())
			Expect(count).To(BeZero())
		})

		It("should only count genre tags", func() {
			// Add a non-genre tag
			nonGenreTag := model.Tag{
				ID:       id.NewTagID("mood", "energetic"),
				TagName:  "mood",
				TagValue: "energetic",
			}
			err := tagRepo.Add(1, nonGenreTag)
			Expect(err).ToNot(HaveOccurred())

			count, err := restRepo.Count()
			Expect(err).ToNot(HaveOccurred())
			// Count should not include the mood tag
			Expect(count).To(Equal(int64(12))) // Should still be 12 genre tags
		})

		It("should filter by name using like match", func() {
			// Test filtering by partial name match using the "name" filter which maps to containsFilter("tag_value")
			options := rest.QueryOptions{
				Filters: map[string]any{"name": "%rock%"},
			}
			count, err := restRepo.Count(options)
			Expect(err).ToNot(HaveOccurred())
			Expect(count).To(BeNumerically("==", 2))
		})
	})

	Describe("Read", func() {
		It("should return existing genre", func() {
			// Use one of the existing genres from our consolidated dataset
			genreID := id.NewTagID("genre", "rock")
			result, err := restRepo.Read(genreID)
			Expect(err).ToNot(HaveOccurred())
			genre := result.(*model.Genre)
			Expect(genre.ID).To(Equal(genreID))
			Expect(genre.Name).To(Equal("rock"))
		})

		It("should return error for non-existent genre", func() {
			_, err := restRepo.Read("non-existent-id")
			Expect(err).To(HaveOccurred())
		})

		It("should not return non-genre tags", func() {
			moodID := id.NewTagID("mood", "happy") // This exists as a mood tag, not genre
			_, err := restRepo.Read(moodID)
			Expect(err).To(HaveOccurred()) // Should not find it as a genre
		})
	})

	Describe("ReadAll", func() {
		It("should return all genres through ReadAll", func() {
			result, err := restRepo.ReadAll()
			Expect(err).ToNot(HaveOccurred())
			genres := result.(model.Genres)
			Expect(genres).To(HaveLen(12)) // We have 12 genre tags

			genreNames := make([]string, len(genres))
			for i, genre := range genres {
				genreNames[i] = genre.Name
			}
			// Check for some of our consolidated dataset genres
			Expect(genreNames).To(ContainElement("rock"))
			Expect(genreNames).To(ContainElement("pop"))
			Expect(genreNames).To(ContainElement("jazz"))
		})

		It("should support rest query options", func() {
			result, err := restRepo.ReadAll()
			Expect(err).ToNot(HaveOccurred())
			Expect(result).ToNot(BeNil())
		})
	})

	Describe("Library Filtering", func() {
		Context("Headless Processes (No User Context)", func() {
			var headlessRepo model.GenreRepository
			var headlessRestRepo model.ResourceRepository

			BeforeEach(func() {
				// Create a repository with no user context (headless)
				headlessGenreRepo := NewGenreRepository(context.Background(), GetDBXBuilder())
				headlessRepo = headlessGenreRepo
				headlessRestRepo = headlessGenreRepo.(model.ResourceRepository)

				// Add genres to different libraries
				db := GetDBXBuilder()
				_, err := db.NewQuery("INSERT OR IGNORE INTO library (id, name, path) VALUES (2, 'Test Library 2', '/test2')").Execute()
				Expect(err).ToNot(HaveOccurred())

				// Add tags to different libraries
				newTag := func(name, value string) model.Tag {
					return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value}
				}

				err = tagRepo.Add(2, newTag("genre", "jazz"))
				Expect(err).ToNot(HaveOccurred())
			})

			It("should see all genres from all libraries when no user is in context", func() {
				// Headless processes should see all genres regardless of library
				genres, err := headlessRepo.GetAll()
				Expect(err).ToNot(HaveOccurred())

				// Should see genres from all libraries
				var genreNames []string
				for _, genre := range genres {
					genreNames = append(genreNames, genre.Name)
				}

				// Should include both rock (library 1) and jazz (library 2)
				Expect(genreNames).To(ContainElement("rock"))
				Expect(genreNames).To(ContainElement("jazz"))
			})

			It("should count all genres from all libraries when no user is in context", func() {
				count, err := headlessRestRepo.Count()
				Expect(err).ToNot(HaveOccurred())

				// Should count all genres from all libraries
				Expect(count).To(BeNumerically(">=", 2))
			})

			It("should allow headless processes to apply explicit library_id filters", func() {
				// Filter by specific library
				genres, err := headlessRestRepo.ReadAll(rest.QueryOptions{
					Filters: map[string]any{"library_id": 2},
				})
				Expect(err).ToNot(HaveOccurred())

				genreList := genres.(model.Genres)
				// Should see only genres from library 2
				Expect(genreList).To(HaveLen(1))
				Expect(genreList[0].Name).To(Equal("jazz"))
			})

			It("should get individual genres when no user is in context", func() {
				// Get all genres first to find an ID
				genres, err := headlessRepo.GetAll()
				Expect(err).ToNot(HaveOccurred())
				Expect(genres).ToNot(BeEmpty())

				// Headless process should be able to get the genre
				genre, err := headlessRestRepo.Read(genres[0].ID)
				Expect(err).ToNot(HaveOccurred())
				Expect(genre).ToNot(BeNil())
			})
		})
	})

	Describe("EntityName", func() {
		It("should return correct entity name", func() {
			name := restRepo.EntityName()
			Expect(name).To(Equal("tag")) // Genre repository uses tag table
		})
	})

	Describe("NewInstance", func() {
		It("should return new genre instance", func() {
			instance := restRepo.NewInstance()
			Expect(instance).To(BeAssignableToTypeOf(&model.Genre{}))
		})
	})
})