// Copyright 2015 Google LLC
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package gensupport

import (
	"bytes"
	"hash/crc32"
	"io"
	"reflect"
	"testing"
	"testing/iotest"

	"google.golang.org/api/googleapi"
)

// getChunkAsString reads a chunk from mb, but does not call Next.
func getChunkAsString(t *testing.T, mb *MediaBuffer) (string, error) {
	chunk, _, size, err := mb.Chunk()

	buf, e := io.ReadAll(chunk)
	if e != nil {
		t.Fatalf("Failed reading chunk: %v", e)
	}
	if size != len(buf) {
		t.Fatalf("reported chunk size doesn't match actual chunk size: got: %v; want: %v", size, len(buf))
	}
	return string(buf), err
}

func TestChunking(t *testing.T) {
	type testCase struct {
		data       string // the data to read from the Reader
		finalErr   error  // error to return after data has been read
		chunkSize  int
		wantChunks []string
	}

	for _, singleByteReads := range []bool{true, false} {
		for _, tc := range []testCase{
			{
				data:       "abcdefg",
				finalErr:   nil,
				chunkSize:  3,
				wantChunks: []string{"abc", "def", "g"},
			},
			{
				data:       "abcdefg",
				finalErr:   nil,
				chunkSize:  1,
				wantChunks: []string{"a", "b", "c", "d", "e", "f", "g"},
			},
			{
				data:       "abcdefg",
				finalErr:   nil,
				chunkSize:  7,
				wantChunks: []string{"abcdefg"},
			},
			{
				data:       "abcdefg",
				finalErr:   nil,
				chunkSize:  8,
				wantChunks: []string{"abcdefg"},
			},
			{
				data:       "abcdefg",
				finalErr:   io.ErrUnexpectedEOF,
				chunkSize:  3,
				wantChunks: []string{"abc", "def", "g"},
			},
			{
				data:       "abcdefg",
				finalErr:   io.ErrUnexpectedEOF,
				chunkSize:  8,
				wantChunks: []string{"abcdefg"},
			},
		} {
			var r io.Reader = &errReader{buf: []byte(tc.data), err: tc.finalErr}

			if singleByteReads {
				r = iotest.OneByteReader(r)
			}

			mb := NewMediaBuffer(r, tc.chunkSize)
			var gotErr error
			got := []string{}
			for {
				chunk, err := getChunkAsString(t, mb)
				if len(chunk) != 0 {
					got = append(got, string(chunk))
				}
				if err != nil {
					gotErr = err
					break
				}
				mb.Next()
			}

			if !reflect.DeepEqual(got, tc.wantChunks) {
				t.Errorf("Failed reading buffer: got: %v; want:%v", got, tc.wantChunks)
			}

			expectedErr := tc.finalErr
			if expectedErr == nil {
				expectedErr = io.EOF
			}
			if gotErr != expectedErr {
				t.Errorf("Reading buffer error: got: %v; want: %v", gotErr, expectedErr)
			}
		}
	}
}

func TestChunkCanBeReused(t *testing.T) {
	er := &errReader{buf: []byte("abcdefg")}
	mb := NewMediaBuffer(er, 3)

	// expectChunk reads a chunk and checks that it got what was wanted.
	expectChunk := func(want string, wantErr error) {
		got, err := getChunkAsString(t, mb)
		if err != wantErr {
			t.Errorf("error reading buffer: got: %v; want: %v", err, wantErr)
		}
		if !reflect.DeepEqual(got, want) {
			t.Errorf("Failed reading buffer: got: %q; want:%q", got, want)
		}
	}
	expectChunk("abc", nil)
	// On second call, should get same chunk again.
	expectChunk("abc", nil)
	mb.Next()
	expectChunk("def", nil)
	expectChunk("def", nil)
	mb.Next()
	expectChunk("g", io.EOF)
	expectChunk("g", io.EOF)
	mb.Next()
	expectChunk("", io.EOF)
}

func TestPos(t *testing.T) {
	er := &errReader{buf: []byte("abcdefg")}
	mb := NewMediaBuffer(er, 3)

	expectChunkAtOffset := func(want int64, wantErr error) {
		_, off, _, err := mb.Chunk()
		if err != wantErr {
			t.Errorf("error reading buffer: got: %v; want: %v", err, wantErr)
		}
		if got := off; got != want {
			t.Errorf("resumable buffer Pos: got: %v; want: %v", got, want)
		}
	}

	// We expect the first chunk to be at offset 0.
	expectChunkAtOffset(0, nil)
	// Fetching the same chunk should return the same offset.
	expectChunkAtOffset(0, nil)

	// Calling Next multiple times should only cause off to advance by 3, since off is not advanced until
	// the chunk is actually read.
	mb.Next()
	mb.Next()
	expectChunkAtOffset(3, nil)

	mb.Next()

	// Load the final 1-byte chunk.
	expectChunkAtOffset(6, io.EOF)

	// Next will advance 1 byte.  But there are no more chunks, so off will not increase beyond 7.
	mb.Next()
	expectChunkAtOffset(7, io.EOF)
	mb.Next()
	expectChunkAtOffset(7, io.EOF)
}

// bytes.Reader implements both Reader and ReaderAt.  The following types
// implement various combinations of Reader, ReaderAt and ContentTyper, by
// wrapping bytes.Reader.  All implement at least ReaderAt, so they can be
// passed to ReaderAtToReader.  The following table summarizes which types
// implement which interfaces:
//
//                 ReaderAt	Reader	ContentTyper
// reader              x          x
// typerReader         x          x          x
// readerAt            x
// typerReaderAt       x                     x

// reader implements Reader, in addition to ReaderAt.
type reader struct {
	r *bytes.Reader
}

func (r *reader) ReadAt(b []byte, off int64) (n int, err error) {
	return r.r.ReadAt(b, off)
}

func (r *reader) Read(b []byte) (n int, err error) {
	return r.r.Read(b)
}

// typerReader implements Reader and ContentTyper, in addition to ReaderAt.
type typerReader struct {
	r *bytes.Reader
}

func (tr *typerReader) ReadAt(b []byte, off int64) (n int, err error) {
	return tr.r.ReadAt(b, off)
}

func (tr *typerReader) Read(b []byte) (n int, err error) {
	return tr.r.Read(b)
}

func (tr *typerReader) ContentType() string {
	return "ctype"
}

// readerAt implements only ReaderAt.
type readerAt struct {
	r *bytes.Reader
}

func (ra *readerAt) ReadAt(b []byte, off int64) (n int, err error) {
	return ra.r.ReadAt(b, off)
}

// typerReaderAt implements ContentTyper, in addition to ReaderAt.
type typerReaderAt struct {
	r *bytes.Reader
}

func (tra *typerReaderAt) ReadAt(b []byte, off int64) (n int, err error) {
	return tra.r.ReadAt(b, off)
}

func (tra *typerReaderAt) ContentType() string {
	return "ctype"
}

func TestAdapter(t *testing.T) {
	data := "abc"

	checkConversion := func(to io.Reader, wantTyper bool) {
		if _, ok := to.(googleapi.ContentTyper); ok != wantTyper {
			t.Errorf("reader implements typer? got: %v; want: %v", ok, wantTyper)
		}
		if typer, ok := to.(googleapi.ContentTyper); ok && typer.ContentType() != "ctype" {
			t.Errorf("content type: got: %s; want: ctype", typer.ContentType())
		}
		buf, err := io.ReadAll(to)
		if err != nil {
			t.Errorf("error reading data: %v", err)
			return
		}
		if !bytes.Equal(buf, []byte(data)) {
			t.Errorf("failed reading data: got: %s; want: %s", buf, data)
		}
	}

	type testCase struct {
		from      io.ReaderAt
		wantTyper bool
	}
	for _, tc := range []testCase{
		{
			from:      &reader{bytes.NewReader([]byte(data))},
			wantTyper: false,
		},
		{
			// Reader and ContentTyper
			from:      &typerReader{bytes.NewReader([]byte(data))},
			wantTyper: true,
		},
		{
			// ReaderAt
			from:      &readerAt{bytes.NewReader([]byte(data))},
			wantTyper: false,
		},
		{
			// ReaderAt and ContentTyper
			from:      &typerReaderAt{bytes.NewReader([]byte(data))},
			wantTyper: true,
		},
	} {
		to := ReaderAtToReader(tc.from, int64(len(data)))
		checkConversion(to, tc.wantTyper)
		// tc.from is a ReaderAt, and should be treated like one, even
		// if it also implements Reader.  Specifically, it can be
		// reused and read from the beginning.
		to = ReaderAtToReader(tc.from, int64(len(data)))
		checkConversion(to, tc.wantTyper)
	}
}

func TestChecksum(t *testing.T) {
	data := "abcdefg"
	mb := NewMediaBuffer(bytes.NewReader([]byte(data)), 3)
	mb.enableAutoChecksum = true
	for {
		_, err := getChunkAsString(t, mb)
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatalf("getChunkAsString() error: %v", err)
		}
		mb.Next()
	}

	want := crc32.Checksum([]byte(data), crc32cTable)
	if got := mb.fullObjectChecksum; got != want {
		t.Errorf("mb.fullObjectChecksum = %d; want %d", got, want)
	}
}
