package nativeapi

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/deluan/rest"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/consts"
	"github.com/navidrome/navidrome/core/auth"
	"github.com/navidrome/navidrome/core/playlists"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/server"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Playlist Image Endpoints", func() {
	BeforeEach(func() {
		DeferCleanup(configtest.SetupConfig())
	})

	DescribeTable("uploadPlaylistImage guard",
		func(enableArtworkUpload, isAdmin bool, expectedStatus int) {
			conf.Server.EnableArtworkUpload = enableArtworkUpload
			handler := uploadPlaylistImage(&mockPlaylistsService{})

			req := httptest.NewRequest("POST", "/playlist/pls-1/image", nil)
			ctx := request.WithUser(GinkgoT().Context(), model.User{ID: "user-1", IsAdmin: isAdmin})
			req = req.WithContext(ctx)

			w := httptest.NewRecorder()
			handler.ServeHTTP(w, req)
			Expect(w.Code).To(Equal(expectedStatus))
		},
		Entry("enabled, regular user passes guard", true, false, http.StatusBadRequest),
		Entry("enabled, admin passes guard", true, true, http.StatusBadRequest),
		Entry("disabled, admin passes guard", false, true, http.StatusBadRequest),
		Entry("disabled, regular user is forbidden", false, false, http.StatusForbidden),
	)

	DescribeTable("deletePlaylistImage guard",
		func(enableArtworkUpload, isAdmin bool, expectedStatus int) {
			conf.Server.EnableArtworkUpload = enableArtworkUpload
			handler := deletePlaylistImage(&mockPlaylistsService{})

			req := httptest.NewRequest("DELETE", "/playlist/pls-1/image", nil)
			ctx := request.WithUser(GinkgoT().Context(), model.User{ID: "user-1", IsAdmin: isAdmin})
			req = req.WithContext(ctx)

			w := httptest.NewRecorder()
			handler.ServeHTTP(w, req)
			Expect(w.Code).To(Equal(expectedStatus))
		},
		Entry("enabled, regular user passes guard", true, false, http.StatusNotFound),
		Entry("enabled, admin passes guard", true, true, http.StatusNotFound),
		Entry("disabled, admin passes guard", false, true, http.StatusNotFound),
		Entry("disabled, regular user is forbidden", false, false, http.StatusForbidden),
	)
})

var _ = Describe("Playlist Tracks Endpoint", func() {
	var (
		router   http.Handler
		plsSvc   *mockPlaylistsService
		userRepo *tests.MockedUserRepo
		w        *httptest.ResponseRecorder
	)

	BeforeEach(func() {
		DeferCleanup(configtest.SetupConfig())
		conf.Server.SessionTimeout = time.Minute

		plsSvc = &mockPlaylistsService{}
		userRepo = tests.CreateMockUserRepo()

		ds := &tests.MockDataStore{
			MockedUser:     userRepo,
			MockedProperty: &tests.MockedPropertyRepo{},
		}

		auth.Init(ds)

		testUser := model.User{
			ID:          "user-1",
			UserName:    "testuser",
			Name:        "Test User",
			IsAdmin:     false,
			NewPassword: "testpass",
		}
		err := userRepo.Put(&testUser)
		Expect(err).ToNot(HaveOccurred())

		nativeRouter := New(ds, nil, plsSvc, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil)
		router = server.JWTVerifier(nativeRouter)
		w = httptest.NewRecorder()
	})

	createAuthenticatedRequest := func(method, path string) *http.Request {
		req := httptest.NewRequest(method, path, nil)
		testUser := model.User{ID: "user-1", UserName: "testuser"}
		token, err := auth.CreateToken(&testUser)
		Expect(err).ToNot(HaveOccurred())
		req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+token)
		return req
	}

	Describe("GET /playlist/{playlistId}/tracks", func() {
		It("returns 404 when playlist does not exist", func() {
			req := createAuthenticatedRequest("GET", "/playlist/non-existent/tracks")
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNotFound))
		})

		It("returns tracks when playlist exists", func() {
			plsSvc.tracksRepo = &mockPlaylistTrackRepo{
				tracks: model.PlaylistTracks{
					{ID: "1", MediaFileID: "mf-1", PlaylistID: "pls-1"},
					{ID: "2", MediaFileID: "mf-2", PlaylistID: "pls-1"},
				},
			}

			req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks")
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusOK))

			var response []model.PlaylistTrack
			err := json.Unmarshal(w.Body.Bytes(), &response)
			Expect(err).ToNot(HaveOccurred())
			Expect(response).To(HaveLen(2))
			Expect(response[0].ID).To(Equal("1"))
			Expect(response[1].ID).To(Equal("2"))
		})
	})

	Describe("GET /playlist/{playlistId}/tracks/{id}", func() {
		It("returns 404 when playlist does not exist", func() {
			req := createAuthenticatedRequest("GET", "/playlist/non-existent/tracks/1")
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNotFound))
		})

		It("returns the track when playlist exists", func() {
			plsSvc.tracksRepo = &mockPlaylistTrackRepo{
				tracks: model.PlaylistTracks{
					{ID: "1", MediaFileID: "mf-1", PlaylistID: "pls-1"},
				},
			}

			req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks/1")
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusOK))

			var response model.PlaylistTrack
			err := json.Unmarshal(w.Body.Bytes(), &response)
			Expect(err).ToNot(HaveOccurred())
			Expect(response.ID).To(Equal("1"))
			Expect(response.MediaFileID).To(Equal("mf-1"))
		})

		It("returns 404 when track does not exist in playlist", func() {
			plsSvc.tracksRepo = &mockPlaylistTrackRepo{
				tracks: model.PlaylistTracks{},
			}

			req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks/999")
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNotFound))
		})
	})
})

type mockPlaylistTrackRepo struct {
	model.PlaylistTrackRepository
	tracks model.PlaylistTracks
}

func (m *mockPlaylistTrackRepo) Count(...rest.QueryOptions) (int64, error) {
	return int64(len(m.tracks)), nil
}

func (m *mockPlaylistTrackRepo) ReadAll(...rest.QueryOptions) (any, error) {
	return m.tracks, nil
}

func (m *mockPlaylistTrackRepo) EntityName() string {
	return "playlist_track"
}

func (m *mockPlaylistTrackRepo) NewInstance() any {
	return &model.PlaylistTrack{}
}

func (m *mockPlaylistTrackRepo) Read(id string) (any, error) {
	for _, t := range m.tracks {
		if t.ID == id {
			return &t, nil
		}
	}
	return nil, rest.ErrNotFound
}

type mockPlaylistsService struct {
	playlists.Playlists
	tracksRepo    rest.Repository
	removeImageFn func(ctx context.Context, id string) error
	setImageFn    func(ctx context.Context, id string, reader io.Reader, ext string) error
}

func (m *mockPlaylistsService) RemoveImage(ctx context.Context, id string) error {
	if m.removeImageFn != nil {
		return m.removeImageFn(ctx, id)
	}
	return model.ErrNotFound
}

func (m *mockPlaylistsService) SetImage(ctx context.Context, id string, reader io.Reader, ext string) error {
	if m.setImageFn != nil {
		return m.setImageFn(ctx, id, reader, ext)
	}
	return model.ErrNotFound
}

func (m *mockPlaylistsService) TracksRepository(_ context.Context, _ string, _ bool) rest.Repository {
	return m.tracksRepo
}
