package media_test

import (
	"bytes"
	"context"
	"database/sql"
	"errors"
	"io"
	"os"
	"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/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

// segmentReader returns an error if provided after reading errBytes bytes.
type segmentReader struct {
	r           io.Reader
	n, errBytes int64
	err         error
}

func (r *segmentReader) Read(p []byte) (int, error) {
	n, err := r.r.Read(p)
	r.n += int64(n)
	if r.n >= r.errBytes && r.err != nil {
		return n, r.err
	}
	return n, err
}

func (r *segmentReader) Close() error { return nil }

func TestPeaksForSegment(t *testing.T) {
	testCases := []struct {
		name                 string
		fixturePath          string
		fixtureReadErrBytes  int64
		fixtureReadErr       error
		fixtureMaxRead       int64
		startFrame, endFrame int64
		channels             int32
		numBins              int
		wantPeaks            []int16
		wantErr              string
	}{
		{
			name:        "OK, entire fixture, stereo, 1 bin",
			fixturePath: "testdata/tone-44100-stereo-int16.raw",
			startFrame:  0,
			endFrame:    44100,
			channels:    2,
			numBins:     1,
			wantPeaks:   []int16{32747, 32747},
		},
		{
			name:        "OK, entire fixture, stereo, 4 bins",
			fixturePath: "testdata/tone-44100-stereo-int16.raw",
			startFrame:  0,
			endFrame:    44100,
			channels:    2,
			numBins:     4,
			wantPeaks:   []int16{8173, 8177, 16366, 16370, 24557, 24555, 32747, 32747},
		},
		{
			name:        "OK, entire fixture, stereo, 16 bins",
			fixturePath: "testdata/tone-44100-stereo-int16.raw",
			startFrame:  0,
			endFrame:    44100,
			channels:    2,
			numBins:     16,
			wantPeaks:   []int16{2029, 2029, 4075, 4076, 6124, 6125, 8173, 8177, 10222, 10221, 12267, 12265, 14314, 14313, 16366, 16370, 18413, 18411, 20453, 20454, 22505, 22508, 24557, 24555, 26604, 26605, 28644, 28643, 30698, 30694, 32747, 32747},
		},
		{
			name:        "OK, entire fixture, mono, 1 bin",
			fixturePath: "testdata/tone-44100-mono-int16.raw",
			startFrame:  0,
			endFrame:    44100,
			channels:    1,
			numBins:     1,
			wantPeaks:   []int16{32748},
		},
		{
			name:        "OK, entire fixture, mono, 32 bins",
			fixturePath: "testdata/tone-44100-mono-int16.raw",
			startFrame:  0,
			endFrame:    44100,
			channels:    1,
			numBins:     32,
			wantPeaks:   []int16{1018, 2030, 3060, 4075, 5092, 6126, 7129, 8172, 9174, 10217, 11227, 12264, 13272, 14315, 15319, 16364, 17370, 18412, 19417, 20453, 21457, 22504, 23513, 24554, 25564, 26607, 27607, 28642, 29647, 30700, 31699, 32748},
		},
		{
			name:           "NOK, entire fixture, mono, 32 bins, read returns io.EOF after 50% complete",
			fixturePath:    "testdata/tone-44100-mono-int16.raw",
			fixtureMaxRead: 44100,
			startFrame:     0,
			endFrame:       44100,
			channels:       1,
			numBins:        32,
			wantPeaks:      []int16{1018, 2030, 3060, 4075, 5092, 6126, 7129, 8172, 9174, 10217, 11227, 12264, 13272, 14315, 15319, 16364, 2053, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		},
		{
			name:                "NOK, entire fixture, mono, 32 bins, read error after 50% complete",
			fixturePath:         "testdata/tone-44100-mono-int16.raw",
			fixtureReadErrBytes: 44100,
			fixtureReadErr:      errors.New("foo"),
			startFrame:          0,
			endFrame:            44100,
			channels:            1,
			numBins:             32,
			wantErr:             "read error: foo",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			startByte := tc.startFrame * int64(tc.channels) * media.SizeOfInt16
			endByte := tc.endFrame * int64(tc.channels) * media.SizeOfInt16
			expectedBytes := endByte - startByte
			if tc.fixtureMaxRead != 0 {
				expectedBytes = tc.fixtureMaxRead
			}

			fixture, err := os.Open(tc.fixturePath)
			require.NoError(t, err)
			defer fixture.Close()
			sr := segmentReader{
				r:        io.LimitReader(fixture, int64(expectedBytes)),
				err:      tc.fixtureReadErr,
				errBytes: tc.fixtureReadErrBytes,
			}

			mediaSet := store.MediaSet{
				ID:            uuid.New(),
				AudioChannels: tc.channels,
				AudioRawS3Key: sql.NullString{String: "foo", Valid: true},
			}

			// store is passed the mediaSetID and returns a mediaSet
			store := &mocks.Store{}
			store.On("GetMediaSet", mock.Anything, mediaSet.ID).Return(mediaSet, nil)
			defer store.AssertExpectations(t)

			// fileStore is passed the expected byte range, and returns an io.Reader
			fileStore := &mocks.FileStore{}
			fileStore.
				On("GetObjectWithRange", mock.Anything, "foo", startByte, endByte).
				Return(&sr, nil)

			service := media.NewMediaSetService(store, nil, fileStore, nil, media.NewTestWorkerPool(), config.Config{}, zap.NewNop().Sugar())
			peaks, err := service.GetPeaksForSegment(context.Background(), mediaSet.ID, tc.startFrame, tc.endFrame, tc.numBins)

			if tc.wantErr == "" {
				require.NoError(t, err)
				assert.Equal(t, tc.wantPeaks, peaks)
			} else {
				assert.EqualError(t, err, tc.wantErr)
			}
		})
	}
}

func BenchmarkGetPeaksForSegment(b *testing.B) {
	const (
		startFrame  = 0
		endFrame    = 1323000
		channels    = 2
		fixturePath = "testdata/tone-44100-stereo-int16-30000ms.raw"
		numBins     = 2000
	)

	audioFile, err := os.Open(fixturePath)
	require.NoError(b, err)
	audioData, err := io.ReadAll(audioFile)
	require.NoError(b, err)

	mediaSetID := uuid.New()
	mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: channels}

	store := &mocks.Store{}
	store.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil)

	for n := 0; n < b.N; n++ {
		// recreate the reader on each iteration
		b.StopTimer()
		readCloser := io.NopCloser(bytes.NewReader(audioData))
		fileStore := &mocks.FileStore{}
		fileStore.
			On("GetObjectWithRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
			Return(readCloser, nil)

		service := media.NewMediaSetService(store, nil, fileStore, nil, media.NewTestWorkerPool(), config.Config{}, zap.NewNop().Sugar())
		b.StartTimer()

		_, err = service.GetPeaksForSegment(context.Background(), mediaSetID, startFrame, endFrame, numBins)
		require.NoError(b, err)
	}
}
