package protoparserutil

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"reflect"
	"testing"
)

func TestReadLinesBlockFailure(t *testing.T) {
	f := func(s string) {
		t.Helper()
		r := bytes.NewBufferString(s)
		if _, _, err := ReadLinesBlock(r, nil, nil); err == nil {
			t.Fatalf("expecting non-nil error")
		}
		sbr := &singleByteReader{
			b: []byte(s),
		}
		if _, _, err := ReadLinesBlock(sbr, nil, nil); err == nil {
			t.Fatalf("expecting non-nil error")
		}
		fr := &failureReader{}
		if _, _, err := ReadLinesBlock(fr, nil, nil); err == nil {
			t.Fatalf("expecting non-nil error")
		}

		un := &unexpectedEOF{}
		if _, _, err := ReadLinesBlock(un, nil, nil); err != nil {
			if !errors.Is(err, io.EOF) {
				t.Fatalf("get unexpected error, expecting io.EOF")
			}
		}

		ef := eofErr{}
		if _, _, err := ReadLinesBlock(ef, nil, nil); err != nil {
			if !errors.Is(err, io.EOF) {
				t.Fatalf("get unexpected error, expecting io.EOF")
			}
		}
	}

	// empty string
	f("")

	// too long string
	b := make([]byte, maxLineSize+1)
	f(string(b))
}

type failureReader struct{}

func (fr *failureReader) Read(_ []byte) (int, error) {
	return 0, fmt.Errorf("some error")
}

type unexpectedEOF struct{}

func (un unexpectedEOF) Read(_ []byte) (int, error) {
	return 0, io.ErrUnexpectedEOF
}

type eofErr struct{}

func (eo eofErr) Read(_ []byte) (int, error) {
	return 0, io.EOF
}

func TestReadLinesBlockMultiLinesSingleByteReader(t *testing.T) {
	f := func(s string, linesExpected []string) {
		t.Helper()

		r := &singleByteReader{
			b: []byte(s),
		}
		var err error
		var dstBuf, tailBuf []byte
		var lines []string
		for {
			dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf)
			if err != nil {
				if err == io.EOF {
					break
				}
				t.Fatalf("unexpected error in ReadLinesBlock(%q): %s", s, err)
			}
			lines = append(lines, string(dstBuf))
		}
		if !reflect.DeepEqual(lines, linesExpected) {
			t.Fatalf("unexpected lines after reading %q: got %q; want %q", s, lines, linesExpected)
		}
	}

	f("", nil)
	f("foo", []string{"foo"})
	f("foo\n", []string{"foo"})
	f("foo\nbar", []string{"foo", "bar"})
	f("\nfoo\nbar", []string{"", "foo", "bar"})
	f("\nfoo\nbar\n", []string{"", "foo", "bar"})
	f("\nfoo\nbar\n\n", []string{"", "foo", "bar", ""})
}

func TestReadLinesBlockMultiLinesBytesBuffer(t *testing.T) {
	f := func(s string, linesExpected []string) {
		t.Helper()

		r := bytes.NewBufferString(s)
		var err error
		var dstBuf, tailBuf []byte
		var lines []string
		for {
			dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf)
			if err != nil {
				if err == io.EOF {
					break
				}
				t.Fatalf("unexpected error in ReadLinesBlock(%q): %s", s, err)
			}
			lines = append(lines, string(dstBuf))
		}
		if !reflect.DeepEqual(lines, linesExpected) {
			t.Fatalf("unexpected lines after reading %q: got %q; want %q", s, lines, linesExpected)
		}
	}

	f("", nil)
	f("foo", []string{"foo"})
	f("foo\n", []string{"foo"})
	f("foo\nbar", []string{"foo", "bar"})
	f("\nfoo\nbar", []string{"\nfoo", "bar"})
	f("\nfoo\nbar\n", []string{"\nfoo\nbar"})
	f("\nfoo\nbar\n\n", []string{"\nfoo\nbar\n"})
}

func TestReadLinesBlockSuccessSingleByteReader(t *testing.T) {
	f := func(s, dstBufExpected, tailBufExpected string) {
		t.Helper()

		r := &singleByteReader{
			b: []byte(s),
		}
		dstBuf, tailBuf, err := ReadLinesBlock(r, nil, nil)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if string(dstBuf) != dstBufExpected {
			t.Fatalf("unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
		}
		if string(tailBuf) != tailBufExpected {
			t.Fatalf("unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
		}

		// Verify the same with non-empty dstBuf and tailBuf
		r = &singleByteReader{
			b: []byte(s),
		}
		dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf[:0])
		if err != nil {
			t.Fatalf("non-empty bufs: unexpected error: %s", err)
		}
		if string(dstBuf) != dstBufExpected {
			t.Fatalf("non-empty bufs: unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
		}
		if string(tailBuf) != tailBufExpected {
			t.Fatalf("non-empty bufs: unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
		}
	}

	f("\n", "", "")
	f("foo\n", "foo", "")
	f("\nfoo", "", "")
	f("foo\nbar", "foo", "")
	f("foo\nbar\nbaz", "foo", "")
	f("foo", "foo", "")

	// The maximum line size
	b := make([]byte, maxLineSize+10)
	b[maxLineSize] = '\n'
	f(string(b), string(b[:maxLineSize]), "")
}

func TestReadLinesBlockSuccessBytesBuffer(t *testing.T) {
	f := func(s, dstBufExpected, tailBufExpected string) {
		t.Helper()

		r := bytes.NewBufferString(s)
		dstBuf, tailBuf, err := ReadLinesBlock(r, nil, nil)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if string(dstBuf) != dstBufExpected {
			t.Fatalf("unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
		}
		if string(tailBuf) != tailBufExpected {
			t.Fatalf("unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
		}

		// Verify the same with non-empty dstBuf and tailBuf
		r = bytes.NewBufferString(s)
		dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf[:0])
		if err != nil {
			t.Fatalf("non-empty bufs: unexpected error: %s", err)
		}
		if string(dstBuf) != dstBufExpected {
			t.Fatalf("non-empty bufs: unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
		}
		if string(tailBuf) != tailBufExpected {
			t.Fatalf("non-empty bufs: unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
		}
	}

	f("\n", "", "")
	f("foo\n", "foo", "")
	f("\nfoo", "", "foo")
	f("foo\nbar", "foo", "bar")
	f("foo\nbar\nbaz", "foo\nbar", "baz")

	// The maximum line size
	b := make([]byte, maxLineSize+10)
	b[maxLineSize] = '\n'
	f(string(b), string(b[:maxLineSize]), string(b[maxLineSize+1:]))
}

type singleByteReader struct {
	b []byte
}

func (sbr *singleByteReader) Read(p []byte) (int, error) {
	if len(sbr.b) == 0 {
		return 0, io.EOF
	}
	n := copy(p, sbr.b[:1])
	sbr.b = sbr.b[n:]
	if len(sbr.b) == 0 {
		return n, io.EOF
	}
	return n, nil
}
