🏡 index : quine-global/quintodrome.git

package external_test

import (
	"context"
	"errors"

	_ "github.com/navidrome/navidrome/adapters/lastfm"
	_ "github.com/navidrome/navidrome/adapters/listenbrainz"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/core/agents"
	. "github.com/navidrome/navidrome/core/external"
	"github.com/navidrome/navidrome/core/matcher"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/stretchr/testify/mock"
)

var _ = Describe("Provider - TopSongs", func() {
	var (
		p             Provider
		artistRepo    *mockArtistRepo    // From provider_helper_test.go
		mediaFileRepo *mockMediaFileRepo // From provider_helper_test.go
		ag            *mockAgents        // Consolidated mock from export_test.go
		ctx           context.Context
	)

	BeforeEach(func() {
		DeferCleanup(configtest.SetupConfig())
		// Disable fuzzy matching for these tests to avoid unexpected GetAll calls
		conf.Server.Matcher.FuzzyThreshold = 100

		ctx = GinkgoT().Context()

		artistRepo = newMockArtistRepo()       // Use helper mock
		mediaFileRepo = newMockMediaFileRepo() // Use helper mock

		// Configure tests.MockDataStore to use the testify/mock-based repos
		ds := &tests.MockDataStore{
			MockedArtist:    artistRepo,
			MockedMediaFile: mediaFileRepo,
		}

		ag = new(mockAgents)

		p = NewProvider(ds, ag, matcher.New(ds), &fakeBroker{})
	})

	It("returns top songs for a known artist", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent response
		agentSongs := []agents.Song{
			{Name: "Song One", MBID: "mbid-song-1"},
			{Name: "Song Two", MBID: "mbid-song-2"},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

		// Mock finding matching tracks (both returned in a single query)
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
		song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 2)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(2))
		Expect(songs[0].ID).To(Equal("song-1"))
		Expect(songs[1].ID).To(Equal("song-2"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	Context("artist id and/or name", func() {
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}

		BeforeEach(func() {
			agentSongs := []agents.Song{{Name: "Song One", MBID: "mbid-song-1"}}
			ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

			song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
			mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
		})

		It("returns top songs for a known artist by id only", func() {
			artistRepo.On("Get", "artist-1").Return(&artist1, nil).Once()

			songs, err := p.TopSongs(ctx, "", "artist-1", 2)

			Expect(err).ToNot(HaveOccurred())
			Expect(songs).To(HaveLen(1))
			Expect(songs[0].ID).To(Equal("song-1"))
			artistRepo.AssertExpectations(GinkgoT())
			ag.AssertExpectations(GinkgoT())
			mediaFileRepo.AssertExpectations(GinkgoT())
		})

		It("returns top songs for a known artist by id, falling back to name", func() {
			artistRepo.On("Get", "fake-id").Return(nil, model.ErrNotFound).Once()
			artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

			songs, err := p.TopSongs(ctx, "Artist One", "fake-id", 2)

			Expect(err).ToNot(HaveOccurred())
			Expect(songs).To(HaveLen(1))
			Expect(songs[0].ID).To(Equal("song-1"))
			artistRepo.AssertExpectations(GinkgoT())
			ag.AssertExpectations(GinkgoT())
			mediaFileRepo.AssertExpectations(GinkgoT())
		})
	})

	It("bails if lookup by id returns a fatal error", func() {
		artistRepo.On("Get", "artist-1").Return(nil, model.ErrInvalidAuth).Once()
		songs, err := p.TopSongs(ctx, "Artist One", "artist-1", 2)
		Expect(songs).To(BeEmpty())
		Expect(err).To(HaveOccurred())
	})

	It("bails if lookup by name returns a fatal error", func() {
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(nil, model.ErrInvalidAuth).Once()
		songs, err := p.TopSongs(ctx, "Artist One", "", 2)
		Expect(songs).To(BeEmpty())
		Expect(err).To(HaveOccurred())
	})

	It("backfills name and MBID onto an unnamed primary credit (the queried artist) and matches", func() {
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil)

		// Agent leaves the first credit unnamed (e.g. an MBID-less collaborator slot). That blank
		// credit IS the queried artist, so enrichment fills both name and MBID; the song then matches
		// the queried artist's track via the backfilled identity.
		agentSongs := []agents.Song{
			{Name: "Song One", Artists: []agents.Artist{{}}},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()

		track := model.MediaFile{
			ID: "song-1", Title: "Song One", ArtistID: "artist-1",
			Participants: model.Participants{model.RoleArtist: model.ParticipantList{
				{Artist: model.Artist{ID: "artist-1", Name: "Artist One", OrderArtistName: "artist one", MbzArtistID: "mbid-artist-1"}},
			}},
		}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)

		songs, err := p.TopSongs(ctx, "Artist One", "", 1)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(1))
		Expect(songs[0].ID).To(Equal("song-1"))
	})

	It("does not stamp the queried MBID onto an already-named different first credit", func() {
		// The queried artist (One) appears only as a featured collaborator; the displayed first credit
		// is a DIFFERENT artist (Two) returned without an MBID. Enrichment must NOT assign One's MBID
		// to Two — only Two's name match (which fails here) or One's own credit may resolve the track.
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil)

		agentSongs := []agents.Song{
			{Name: "Collab Song", Artists: []agents.Artist{{Name: "Artist Two"}}},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()

		// Library track is credited to Artist One (the queried artist) under a same title. If the
		// queried MBID were wrongly stamped onto the "Artist Two" credit, that mismatched name+MBID			artistRepo.On("Get", "fake-id").Return(nil, model.ErrNotFound).Once()

		// could mis-resolve. With the guard, "Artist Two" stays MBID-less and does not match One's track.
		track := model.MediaFile{
			ID: "one-track", Title: "Collab Song", ArtistID: "artist-1",
			Participants: model.Participants{model.RoleArtist: model.ParticipantList{
				{Artist: model.Artist{ID: "artist-1", Name: "Artist One", OrderArtistName: "artist one", MbzArtistID: "mbid-artist-1"}},
			}},
		}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)

		songs, err := p.TopSongs(ctx, "Artist One", "", 1)

		Expect(err).ToNot(HaveOccurred())
		// "Artist Two" (named, MBID-less, not in the library) does not resolve to One's track.
		Expect(songs).To(BeEmpty())
	})

	It("returns nil for an unknown artist", func() {
		// Mock artist not found
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{}, nil).Once()

		songs, err := p.TopSongs(ctx, "Unknown Artist", "", 5)

		Expect(err).ToNot(HaveOccurred()) // TopSongs returns nil error if artist not found
		Expect(songs).To(BeNil())
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertNotCalled(GinkgoT(), "GetArtistTopSongs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
	})

	It("returns error when the agent returns an error", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent error
		agentErr := errors.New("agent error")
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agentErr).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 5)

		Expect(err).To(MatchError(agentErr))
		Expect(songs).To(BeNil())
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
	})

	It("returns ErrNotFound when the agent returns ErrNotFound", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent ErrNotFound
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agents.ErrNotFound).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 5)

		Expect(err).To(MatchError(model.ErrNotFound))
		Expect(songs).To(BeNil())
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
	})

	// This endpoint answered with an empty list before retry-later existed; it must keep doing so.
	It("returns an empty list, not a client error, when the agents are throttled", func() {
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).
			Return(nil, agents.ErrRetryLater).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 5)

		Expect(songs).To(BeEmpty())
		Expect(err).To(MatchError(model.ErrNotFound), "the handler renders this as an empty 200")
		Expect(err).ToNot(MatchError(agents.ErrRetryLater))
		ag.AssertExpectations(GinkgoT())
	})

	It("returns fewer songs if count is less than available top songs", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent response (only need 1 for the test)
		agentSongs := []agents.Song{{Name: "Song One", MBID: "mbid-song-1"}}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()

		// Mock finding matching track
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 1)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(1))
		Expect(songs[0].ID).To(Equal("song-1"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("returns fewer songs if fewer matching tracks are found", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()
		// Matcher artist resolution for the title-match path (song2 falls through).
		artistRepo.On("GetAll", mock.Anything).Return(model.Artists{artist1}, nil).Maybe()

		// Mock agent response
		agentSongs := []agents.Song{
			{Name: "Song One", MBID: "mbid-song-1"},
			{Name: "Song Two", MBID: "mbid-song-2"},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

		// Mock finding matching tracks (only find song 1 on bulk query)
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once() // bulk MBID query
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{}, nil).Once()      // title track-fetch for song2: no match

		songs, err := p.TopSongs(ctx, "Artist One", "", 2)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(1))
		Expect(songs[0].ID).To(Equal("song-1"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("returns error when context is canceled during agent call", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Setup context that will be canceled
		canceledCtx, cancel := context.WithCancel(ctx)

		// Mock agent call to return context canceled error
		ag.On("GetArtistTopSongs", canceledCtx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, context.Canceled).Once()

		cancel() // Cancel the context before calling
		songs, err := p.TopSongs(canceledCtx, "Artist One", "", 5)

		Expect(err).To(MatchError(context.Canceled))
		Expect(songs).To(BeNil())
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
	})

	It("falls back to title matching when MbzRecordingID is missing", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()
		// Matcher artist resolution for both title-fallback songs.
		artistRepo.On("GetAll", mock.Anything).Return(model.Artists{artist1}, nil).Maybe()

		// Mock agent response with songs that have NO MBID (empty string)
		agentSongs := []agents.Song{
			{Name: "Song One", MBID: ""}, // No MBID, should fall back to title matching
			{Name: "Song Two", MBID: ""}, // No MBID, should fall back to title matching
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

		// Title track-fetch: tracks must carry RoleArtist participants so back-mapping routes them.
		participant1 := model.Participant{Artist: model.Artist{ID: "artist-1", Name: "Artist One", OrderArtistName: "artist one"}}
		song1 := model.MediaFile{
			ID: "song-1", Title: "Song One", Artist: "Artist One", ArtistID: "artist-1",
			Participants: model.Participants{model.RoleArtist: model.ParticipantList{participant1}},
		}
		song2 := model.MediaFile{
			ID: "song-2", Title: "Song Two", Artist: "Artist One", ArtistID: "artist-1",
			Participants: model.Participants{model.RoleArtist: model.ParticipantList{participant1}},
		}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 2)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(2))
		Expect(songs[0].ID).To(Equal("song-1"))
		Expect(songs[1].ID).To(Equal("song-2"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("combines MBID and title matching when some songs have missing MbzRecordingID", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()
		// Matcher artist resolution for song2's title-fallback path.
		artistRepo.On("GetAll", mock.Anything).Return(model.Artists{artist1}, nil).Maybe()

		// Mock agent response with mixed MBID availability
		agentSongs := []agents.Song{
			{Name: "Song One", MBID: "mbid-song-1"}, // Has MBID, should match by MBID
			{Name: "Song Two", MBID: ""},            // No MBID, should fall back to title matching
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

		// Mock the MBID query (finds song1 by MBID)
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1", OrderTitle: "song one"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()

		// Title track-fetch: song2 must carry RoleArtist participants for back-mapping.
		participant1 := model.Participant{Artist: model.Artist{ID: "artist-1", Name: "Artist One", OrderArtistName: "artist one"}}
		song2 := model.MediaFile{
			ID: "song-2", Title: "Song Two", Artist: "Artist One", ArtistID: "artist-1",
			Participants: model.Participants{model.RoleArtist: model.ParticipantList{participant1}},
		}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song2}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 2)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(2))
		Expect(songs[0].ID).To(Equal("song-1")) // Found by MBID
		Expect(songs[1].ID).To(Equal("song-2")) // Found by title
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("only returns requested count when provider returns additional items", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent response
		agentSongs := []agents.Song{
			{Name: "Song One", MBID: "mbid-song-1"},
			{Name: "Song Two", MBID: "mbid-song-2"},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()

		// Mock finding matching tracks (both returned in a single query)
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
		song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 1)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(1))
		Expect(songs[0].ID).To(Equal("song-1"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("matches songs by ID first when agent provides IDs", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent response with IDs provided (highest priority matching)
		// Note: Songs have no MBID to ensure only ID matching is used
		agentSongs := []agents.Song{
			{ID: "song-1", Name: "Song One"},
			{ID: "song-2", Name: "Song Two"},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()

		// Mock ID lookup (first query - should match both songs directly)
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1"}
		song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 2)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(2))
		Expect(songs[0].ID).To(Equal("song-1"))
		Expect(songs[1].ID).To(Equal("song-2"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})

	It("falls back to MBID when ID is not found", func() {
		// Mock finding the artist
		artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
		artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()

		// Mock agent response with ID that won't be found, but MBID that will
		agentSongs := []agents.Song{
			{ID: "non-existent-id", Name: "Song One", MBID: "mbid-song-1"},
		}
		ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()

		// Mock ID lookup - returns empty (ID not found)
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{}, nil).Once()
		// Mock MBID lookup - finds the song
		song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
		mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()

		songs, err := p.TopSongs(ctx, "Artist One", "", 1)

		Expect(err).ToNot(HaveOccurred())
		Expect(songs).To(HaveLen(1))
		Expect(songs[0].ID).To(Equal("song-1"))
		artistRepo.AssertExpectations(GinkgoT())
		ag.AssertExpectations(GinkgoT())
		mediaFileRepo.AssertExpectations(GinkgoT())
	})
})