package streamio

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"strings"
	"testing"
	"testing/iotest"

	"github.com/stretchr/testify/require"
)

func TestReceiveSources(t *testing.T) {
	testData := "Hello this is the test data that will be received"
	testCases := []struct {
		desc string
		r    io.Reader
	}{
		{desc: "base", r: strings.NewReader(testData)},
		{desc: "dataerr", r: iotest.DataErrReader(strings.NewReader(testData))},
		{desc: "onebyte", r: iotest.OneByteReader(strings.NewReader(testData))},
		{desc: "dataerr(onebyte)", r: iotest.DataErrReader(iotest.OneByteReader(strings.NewReader(testData)))},
	}

	for _, tc := range testCases {
		data, err := ioutil.ReadAll(&opaqueReader{NewReader(receiverFromReader(tc.r))})
		require.NoError(t, err, tc.desc)
		require.Equal(t, testData, string(data), tc.desc)
	}
}

func TestReadSizes(t *testing.T) {
	testData := "Hello this is the test data that will be received. It goes on for a while bla bla bla."
	for n := 1; n < 100; n *= 3 {
		desc := fmt.Sprintf("reads of size %d", n)
		result := &bytes.Buffer{}
		reader := &opaqueReader{NewReader(receiverFromReader(strings.NewReader(testData)))}
		_, err := io.CopyBuffer(&opaqueWriter{result}, reader, make([]byte, n))

		require.NoError(t, err, desc)
		require.Equal(t, testData, result.String())
	}
}

func TestWriterTo(t *testing.T) {
	testData := "Hello this is the test data that will be received. It goes on for a while bla bla bla."
	testCases := []struct {
		desc string
		r    io.Reader
	}{
		{desc: "base", r: strings.NewReader(testData)},
		{desc: "dataerr", r: iotest.DataErrReader(strings.NewReader(testData))},
		{desc: "onebyte", r: iotest.OneByteReader(strings.NewReader(testData))},
		{desc: "dataerr(onebyte)", r: iotest.DataErrReader(iotest.OneByteReader(strings.NewReader(testData)))},
	}

	for _, tc := range testCases {
		result := &bytes.Buffer{}
		reader := NewReader(receiverFromReader(tc.r))
		n, err := reader.(io.WriterTo).WriteTo(result)

		require.NoError(t, err, tc.desc)
		require.Equal(t, int64(len(testData)), n, tc.desc)
		require.Equal(t, testData, result.String(), tc.desc)
	}
}

func receiverFromReader(r io.Reader) func() ([]byte, error) {
	return func() ([]byte, error) {
		data := make([]byte, 10)
		n, err := r.Read(data)
		return data[:n], err
	}
}

// Hide io.WriteTo if it exists
type opaqueReader struct {
	io.Reader
}

// Hide io.ReadFrom if it exists
type opaqueWriter struct {
	io.Writer
}

func TestWriterChunking(t *testing.T) {
	defer func(oldBufferSize int) {
		WriteBufferSize = oldBufferSize
	}(WriteBufferSize)
	WriteBufferSize = 5

	testData := "Hello this is some test data"
	ts := &testSender{}
	w := NewWriter(ts.send)
	_, err := io.CopyBuffer(&opaqueWriter{w}, strings.NewReader(testData), make([]byte, 10))

	require.NoError(t, err)
	require.Equal(t, testData, string(bytes.Join(ts.sends, nil)))
	for _, send := range ts.sends {
		require.True(t, len(send) <= WriteBufferSize, "send calls may not exceed WriteBufferSize")
	}
}

type testSender struct {
	sends [][]byte
}

func (ts *testSender) send(p []byte) error {
	buf := make([]byte, len(p))
	copy(buf, p)
	ts.sends = append(ts.sends, buf)
	return nil
}

func TestReadFrom(t *testing.T) {
	defer func(oldBufferSize int) {
		WriteBufferSize = oldBufferSize
	}(WriteBufferSize)
	WriteBufferSize = 5

	testData := "Hello this is the test data that will be received. It goes on for a while bla bla bla."
	testCases := []struct {
		desc string
		r    io.Reader
	}{
		{desc: "base", r: strings.NewReader(testData)},
		{desc: "dataerr", r: iotest.DataErrReader(strings.NewReader(testData))},
		{desc: "onebyte", r: iotest.OneByteReader(strings.NewReader(testData))},
		{desc: "dataerr(onebyte)", r: iotest.DataErrReader(iotest.OneByteReader(strings.NewReader(testData)))},
	}

	for _, tc := range testCases {
		ts := &testSender{}
		n, err := NewWriter(ts.send).(io.ReaderFrom).ReadFrom(tc.r)

		require.NoError(t, err, tc.desc)
		require.Equal(t, int64(len(testData)), n, tc.desc)
		require.Equal(t, testData, string(bytes.Join(ts.sends, nil)), tc.desc)
		for _, send := range ts.sends {
			require.True(t, len(send) <= WriteBufferSize, "send calls may not exceed WriteBufferSize")
		}
	}
}
