package server

import (
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"git.netflux.io/rob/clipper/config"
	"git.netflux.io/rob/clipper/generated/mocks"
	"git.netflux.io/rob/clipper/media"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

func TestHandler(t *testing.T) {
	testCases := []struct {
		name                                    string
		path, body, method, contentType, origin string
		config                                  config.Config
		wantStartFrame, wantEndFrame            int64
		wantAudioFormat                         media.AudioFormat
		wantStatus                              int
		wantHeaders                             map[string]string
		wantBody                                string
	}{
		{
			name:       "assets disabled, file system store disabled, GET /",
			path:       "/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store},
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "assets disabled, file system store disabled, GET /foo.js",
			path:       "/foo.js",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store},
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "assets enabled, file system store disabled, index.html exists, GET /",
			path:       "/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store, AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "index",
		},
		{
			name:       "assets enabled, file system store disabled, index.html does not exist, GET /css/",
			path:       "/css/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store, AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "assets enabled, file system store disabled, index.html does not exist, GET /css/style.css",
			path:       "/css/style.css",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store, AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "css",
		},
		{
			name:       "assets enabled, file system store disabled, GET /foo.js",
			path:       "/foo.js",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.S3Store, AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "foo",
		},
		{
			name:       "assets enabled, file system store enabled with path prefix /store/, GET /foo.js",
			path:       "/foo.js",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "foo",
		},
		{
			name:       "assets enabled, file system store enabled with path prefix /store/, GET /store/bar.mp4",
			path:       "/store/bar.mp4",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "bar",
		},
		{
			name:       "assets enabled, file system store enabled with path prefix /store/, GET /store/",
			path:       "/store/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "assets enabled, file system store enabled with path prefix /store/, GET /",
			path:       "/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusOK,
			wantBody:   "index",
		},
		{
			name:       "assets enabled, file system store enabled with path prefix /, GET / clobbers the assets routes",
			path:       "/",
			method:     http.MethodGet,
			config:     config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/"), FileStoreHTTPRoot: "testdata/http/filestore", AssetsHTTPRoot: "testdata/http/assets"},
			wantStatus: http.StatusNotFound,
		},
		{
			name:   "assets enabled, configured with custom Allowed-Origins header, origin does not match",
			path:   "/css/style.css",
			method: http.MethodGet,
			origin: "https://localhost:3000",
			config: config.Config{
				FileStore:          config.S3Store,
				AssetsHTTPRoot:     "testdata/http/assets",
				CORSAllowedOrigins: []string{"https://www.example.com"},
			},
			wantHeaders: map[string]string{"access-control-allow-origin": ""},
			wantStatus:  http.StatusOK,
		},
		{
			name:   "assets enabled, configured with custom Allowed-Origins header, origin does match",
			path:   "/css/style.css",
			method: http.MethodGet,
			origin: "https://www.example.com",
			config: config.Config{
				FileStore:          config.S3Store,
				AssetsHTTPRoot:     "testdata/http/assets",
				CORSAllowedOrigins: []string{"https://www.example.com"},
			},
			wantHeaders: map[string]string{"access-control-allow-origin": "https://www.example.com"},
			wantStatus:  http.StatusOK,
		},
		{
			name:        "POST /api/media_sets/:id/clip, NOK, no body",
			path:        "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip",
			contentType: "application/x-www-form-urlencoded",
			method:      http.MethodPost,
			config:      config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/")},
			wantStatus:  http.StatusBadRequest,
		},
		{
			name:        "POST /api/media_sets/:id/clip, NOK, missing params",
			path:        "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip",
			body:        "start_frame=0&end_frame=1024",
			contentType: "application/x-www-form-urlencoded",
			method:      http.MethodPost,
			config:      config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/")},
			wantStatus:  http.StatusBadRequest,
		},
		{
			name:        "POST /api/media_sets/:id/clip, NOK, invalid UUID",
			path:        "/api/media_sets/123/clip",
			body:        "start_frame=0&end_frame=1024&format=mp3",
			contentType: "application/x-www-form-urlencoded",
			method:      http.MethodPost,
			config:      config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/")},
			wantStatus:  http.StatusNotFound,
		},
		{
			name:            "POST /api/media_sets/:id/clip, MP3, OK",
			path:            "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip",
			body:            "start_frame=0&end_frame=1024&format=mp3",
			contentType:     "application/x-www-form-urlencoded",
			method:          http.MethodPost,
			config:          config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/")},
			wantStartFrame:  0,
			wantEndFrame:    1024,
			wantAudioFormat: media.AudioFormatMP3,
			wantHeaders: map[string]string{
				"content-type":        "audio/mp3",
				"content-disposition": "attachment; filename=clip.mp3",
			},
			wantStatus: http.StatusOK,
			wantBody:   "an audio file",
		},
		{
			name:            "POST /api/media_sets/:id/clip, WAV, OK",
			path:            "/api/media_sets/05951a4d-584e-4056-9ae7-08b9e4cd355d/clip",
			body:            "start_frame=4096&end_frame=8192&format=wav",
			contentType:     "application/x-www-form-urlencoded",
			method:          http.MethodPost,
			config:          config.Config{FileStore: config.FileSystemStore, FileStoreHTTPBaseURL: mustParseURL(t, "/store/")},
			wantStartFrame:  4096,
			wantEndFrame:    8192,
			wantAudioFormat: media.AudioFormatWAV,
			wantHeaders: map[string]string{
				"content-type":        "audio/wav",
				"content-disposition": "attachment; filename=clip.wav",
			},
			wantStatus: http.StatusOK,
			wantBody:   "an audio file",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var stream mocks.AudioSegmentStream
			stream.On("Next", mock.Anything).Return(media.AudioSegmentProgress{PercentComplete: 60, Data: []byte("an aud")}, nil).Once()
			stream.On("Next", mock.Anything).Return(media.AudioSegmentProgress{PercentComplete: 80, Data: []byte("io file")}, nil).Once()
			stream.On("Next", mock.Anything).Return(media.AudioSegmentProgress{PercentComplete: 100}, io.EOF).Once()

			var mediaSetService mocks.MediaSetService
			mediaSetService.
				On("GetAudioSegment", mock.Anything, uuid.MustParse("05951a4d-584e-4056-9ae7-08b9e4cd355d"), tc.wantStartFrame, tc.wantEndFrame, tc.wantAudioFormat).
				Return(&stream, nil)
			if tc.wantStartFrame != 0 {
				defer stream.AssertExpectations(t)
				defer mediaSetService.AssertExpectations(t)
			}

			handler := newHTTPHandler(nil, &mediaSetService, tc.config, zap.NewNop().Sugar())

			var body io.Reader
			if tc.body != "" {
				body = strings.NewReader(tc.body)
			}
			req := httptest.NewRequest(tc.method, tc.path, body)
			if tc.origin != "" {
				req.Header.Add("origin", tc.origin)
			}
			if tc.contentType != "" {
				req.Header.Add("content-type", tc.contentType)
			}

			w := httptest.NewRecorder()
			handler.ServeHTTP(w, req)
			resp := w.Result()

			assert.Equal(t, tc.wantStatus, resp.StatusCode)
			if tc.wantBody != "" {
				body, err := io.ReadAll(resp.Body)
				require.NoError(t, err)
				assert.Equal(t, tc.wantBody, string(body))
			}
			for k, v := range tc.wantHeaders {
				assert.Equal(t, v, resp.Header.Get(k))
			}
		})
	}
}

func mustParseURL(t *testing.T, u string) *url.URL {
	pu, err := url.Parse(u)
	require.NoError(t, err)
	return pu
}
