package media_test

import (
	"bytes"
	"context"
	"errors"
	"io"
	"testing"

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

func TestGetSegment(t *testing.T) {
	const inFixturePath = "testdata/tone-44100-stereo-int16-30000ms.raw"
	mediaSetID := uuid.MustParse("4c440241-cca9-436f-adb0-be074588cf2b")
	wp := media.NewTestWorkerPool()

	t.Run("invalid range", func(t *testing.T) {
		var mockStore mocks.Store
		var fileStore mocks.FileStore
		service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, wp, config.Config{}, zap.NewNop().Sugar())

		stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 1, 0, media.AudioFormatMP3)
		require.Nil(t, stream)
		require.EqualError(t, err, "invalid range")
	})

	t.Run("error fetching media set", func(t *testing.T) {
		var mockStore mocks.Store
		mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(store.MediaSet{}, pgx.ErrNoRows)
		var fileStore mocks.FileStore
		service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, wp, config.Config{}, zap.NewNop().Sugar())

		stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3)
		require.Nil(t, stream)
		require.EqualError(t, err, "error getting media set: no rows in result set")
	})

	t.Run("error fetching audio data", func(t *testing.T) {
		mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: 2}

		var mockStore mocks.Store
		mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil)

		var fileStore mocks.FileStore
		fileStore.On("GetObjectWithRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
			Return(nil, errors.New("network error"))

		service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, wp, config.Config{}, zap.NewNop().Sugar())

		stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3)
		require.Nil(t, stream)
		require.EqualError(t, err, "error getting object from store: network error")
	})

	t.Run("ffmpeg returns non-zero error code", func(t *testing.T) {
		mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: 2}

		var mockStore mocks.Store
		mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil)

		var fileStore mocks.FileStore
		fileStore.On("GetObjectWithRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
			Return(fixtureReader(t, inFixturePath, 1), nil)

		cmd := helperCommand(t, "", "", "something bad happened", 2)
		service := media.NewMediaSetService(&mockStore, nil, &fileStore, cmd, wp, config.Config{}, zap.NewNop().Sugar())

		stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3)
		require.NoError(t, err)

		_, err = stream.Next(context.Background())
		require.EqualError(t, err, "error waiting for ffmpeg: exit status 2, output: something bad happened")

	})

	testCases := []struct {
		name                       string
		audioFormat                media.AudioFormat
		audioChannels              int32
		inStartFrame, inEndFrame   int64
		wantStartByte, wantEndByte int64
		outFixturePath             string
		wantCommand                string
		wantOutput                 string
	}{
		{
			name:           "mono to mp3",
			audioFormat:    media.AudioFormatMP3,
			audioChannels:  1,
			inStartFrame:   500,
			inEndFrame:     2_000,
			wantStartByte:  1_000,
			wantEndByte:    4_000,
			outFixturePath: "testdata/fake.mp3",
			wantCommand:    "ffmpeg -hide_banner -loglevel error -f s16le -ac 1 -ar 48000 -i - -f mp3 -",
			wantOutput:     "this is a fake mp3",
		},
		{
			name:           "stereo to mp3",
			audioFormat:    media.AudioFormatMP3,
			audioChannels:  2,
			inStartFrame:   0,
			inEndFrame:     1_323_000,
			wantStartByte:  0,
			wantEndByte:    5_292_000,
			outFixturePath: "testdata/fake.mp3",
			wantCommand:    "ffmpeg -hide_banner -loglevel error -f s16le -ac 2 -ar 48000 -i - -f mp3 -",
			wantOutput:     "this is a fake mp3",
		},
		{
			name:           "mono to wav",
			audioFormat:    media.AudioFormatWAV,
			audioChannels:  1,
			inStartFrame:   16_384,
			inEndFrame:     32_768,
			wantStartByte:  32_768,
			wantEndByte:    65_536,
			outFixturePath: "testdata/fake.wav",
			wantCommand:    "ffmpeg -hide_banner -loglevel error -f s16le -ac 1 -ar 48000 -i - -f wav -",
			wantOutput:     "this is a fake wav",
		},
		{
			name:           "stereo to wav",
			audioFormat:    media.AudioFormatWAV,
			audioChannels:  2,
			inStartFrame:   2_048,
			inEndFrame:     4_096,
			wantStartByte:  8_192,
			wantEndByte:    16_384,
			outFixturePath: "testdata/fake.wav",
			wantCommand:    "ffmpeg -hide_banner -loglevel error -f s16le -ac 2 -ar 48000 -i - -f wav -",
			wantOutput:     "this is a fake wav",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := context.Background()

			mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: tc.audioChannels}
			var mockStore mocks.Store
			mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil)
			defer mockStore.AssertExpectations(t)

			var fileStore mocks.FileStore
			fileStore.
				On("GetObjectWithRange", mock.Anything, "media_sets/4c440241-cca9-436f-adb0-be074588cf2b/audio.raw", tc.wantStartByte, tc.wantEndByte).
				Return(fixtureReader(t, inFixturePath, tc.wantEndByte-tc.wantStartByte), nil)
			defer fileStore.AssertExpectations(t)

			cmd := helperCommand(t, tc.wantCommand, tc.outFixturePath, "", 0)
			service := media.NewMediaSetService(&mockStore, nil, &fileStore, cmd, wp, config.Config{}, zap.NewNop().Sugar())

			stream, err := service.GetAudioSegment(ctx, mediaSetID, tc.inStartFrame, tc.inEndFrame, tc.audioFormat)
			require.NoError(t, err)

			var data bytes.Buffer
			var lastPercentComplete float32
			var progress media.AudioSegmentProgress
			for {
				progress, err = stream.Next(ctx)
				if err == io.EOF {
					break
				}

				require.NoError(t, err)
				assert.GreaterOrEqual(t, progress.PercentComplete, lastPercentComplete)
				lastPercentComplete = progress.PercentComplete
				data.Write(progress.Data)
			}

			assert.Equal(t, tc.wantOutput, data.String())
			assert.Equal(t, float32(100), lastPercentComplete)
		})
	}
}
