package internal

import (
	"bytes"
	"fmt"
	"io"
	"strings"
	"testing"

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

const maxDecompressionSize = 1024

func TestGzipEncodeDecode(t *testing.T) {
	enc, err := NewGzipEncoder()
	require.NoError(t, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(maxDecompressionSize))

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))
}

func TestGzipReuse(t *testing.T) {
	enc, err := NewGzipEncoder()
	require.NoError(t, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(maxDecompressionSize))

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))

	payload, err = enc.Encode([]byte("doody"))
	require.NoError(t, err)

	actual, err = dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "doody", string(actual))
}

func TestZlibEncodeDecode(t *testing.T) {
	enc, err := NewZlibEncoder()
	require.NoError(t, err)
	dec := NewZlibDecoder(WithMaxDecompressionSize(maxDecompressionSize))

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))
}

func TestZlibEncodeDecodeWithTooLargeMessage(t *testing.T) {
	enc, err := NewZlibEncoder()
	require.NoError(t, err)
	dec := NewZlibDecoder(WithMaxDecompressionSize(3))

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	_, err = dec.Decode(payload)
	require.ErrorContains(t, err, "size of decoded data exceeds allowed size 3")
}

func TestZstdEncodeDecode(t *testing.T) {
	enc, err := NewZstdEncoder()
	require.NoError(t, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(maxDecompressionSize))
	require.NoError(t, err)

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))
}

func TestZstdReuse(t *testing.T) {
	enc, err := NewZstdEncoder()
	require.NoError(t, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(maxDecompressionSize))
	require.NoError(t, err)

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))

	payload, err = enc.Encode([]byte("doody"))
	require.NoError(t, err)

	actual, err = dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "doody", string(actual))
}

func TestIdentityEncodeDecode(t *testing.T) {
	dec := NewIdentityDecoder(WithMaxDecompressionSize(maxDecompressionSize))
	enc, err := NewIdentityEncoder()
	require.NoError(t, err)

	payload, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	actual, err := dec.Decode(payload)
	require.NoError(t, err)

	require.Equal(t, "howdy", string(actual))
}

func TestStreamIdentityDecode(t *testing.T) {
	var r bytes.Buffer
	n, err := r.Write([]byte("howdy"))
	require.NoError(t, err)
	require.Equal(t, 5, n)

	dec, err := NewStreamContentDecoder("identity", &r)
	require.NoError(t, err)

	data, err := io.ReadAll(dec)
	require.NoError(t, err)

	require.Equal(t, []byte("howdy"), data)
}

func TestStreamGzipDecode(t *testing.T) {
	enc, err := NewGzipEncoder()
	require.NoError(t, err)
	written, err := enc.Encode([]byte("howdy"))
	require.NoError(t, err)

	w := bytes.NewBuffer(written)

	dec, err := NewStreamContentDecoder("gzip", w)
	require.NoError(t, err)

	b := make([]byte, 10)
	n, err := dec.Read(b)
	require.NoError(t, err)
	require.Equal(t, 5, n)

	require.Equal(t, []byte("howdy"), b[:n])
}

func TestCompressionLevel(t *testing.T) {
	tests := []struct {
		algorithm   string
		validLevels []int
		errormsg    string
	}{
		{
			algorithm:   "gzip",
			validLevels: []int{0, 1, 9},
			errormsg:    "invalid compression level",
		},
		{
			algorithm:   "zlib",
			validLevels: []int{0, 1, 9},
			errormsg:    "invalid compression level",
		},
		{
			algorithm:   "zstd",
			validLevels: []int{1, 3, 7, 11},
			errormsg:    "invalid compression level",
		},
		{
			algorithm: "identity",
			errormsg:  "does not support options",
		},
	}

	for _, tt := range tests {
		// Check default i.e. without specifying level
		t.Run(tt.algorithm+" default", func(t *testing.T) {
			enc, err := NewContentEncoder(tt.algorithm)
			require.NoError(t, err)
			require.NotNil(t, enc)
		})

		// Check invalid level
		t.Run(tt.algorithm+" invalid", func(t *testing.T) {
			_, err := NewContentEncoder(tt.algorithm, WithCompressionLevel(12))
			require.ErrorContains(t, err, tt.errormsg)
		})

		// Check known levels 0..9
		for level := 0; level < 10; level++ {
			name := fmt.Sprintf("%s level %d", tt.algorithm, level)
			t.Run(name, func(t *testing.T) {
				var valid bool
				for _, l := range tt.validLevels {
					if l == level {
						valid = true
						break
					}
				}

				enc, err := NewContentEncoder(tt.algorithm, WithCompressionLevel(level))
				if valid {
					require.NoError(t, err)
					require.NotNil(t, enc)
				} else {
					require.ErrorContains(t, err, tt.errormsg)
				}
			})
		}
	}
}

func BenchmarkGzipEncode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err := enc.Encode(data)
		require.NoError(b, err)
	}
}

func BenchmarkGzipDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkGzipEncodeDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkGzipEncodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err := enc.Encode(data)
		require.NoError(b, err)
	}
}

func BenchmarkGzipDecodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkGzipEncodeDecodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewGzipEncoder()
	require.NoError(b, err)
	dec := NewGzipDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZstdEncode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err := enc.Encode(data)
		require.NoError(b, err)
	}
}

func BenchmarkZstdDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZstdEncodeDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZstdEncodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err := enc.Encode(data)
		require.NoError(b, err)
	}
}

func BenchmarkZstdDecodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZstdEncodeDecodeBig(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 1024*1024))
	dataLen := int64(len(data)) + 1

	enc, err := NewZstdEncoder()
	require.NoError(b, err)
	dec, err := NewZstdDecoder(WithMaxDecompressionSize(dataLen))
	require.NoError(b, err)
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZlibEncode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZlibEncoder()
	require.NoError(b, err)
	dec := NewZlibDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err := enc.Encode(data)
		require.NoError(b, err)
	}
}

func BenchmarkZlibDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZlibEncoder()
	require.NoError(b, err)
	dec := NewZlibDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkZlibEncodeDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	enc, err := NewZlibEncoder()
	require.NoError(b, err)
	dec := NewZlibDecoder(WithMaxDecompressionSize(dataLen))
	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}

func BenchmarkIdentityEncodeDecode(b *testing.B) {
	data := []byte(strings.Repeat("-howdy stranger-", 64))
	dataLen := int64(len(data)) + 1

	dec := NewIdentityDecoder(WithMaxDecompressionSize(dataLen))
	enc, err := NewIdentityEncoder()
	require.NoError(b, err)

	payload, err := enc.Encode(data)
	require.NoError(b, err)
	actual, err := dec.Decode(payload)
	require.NoError(b, err)
	require.Equal(b, data, actual)

	for n := 0; n < b.N; n++ {
		payload, err := enc.Encode(data)
		require.NoError(b, err)

		_, err = dec.Decode(payload)
		require.NoError(b, err)
	}
}
