package gbytes_test

import (
	"fmt"
	"io"
	"time"

	. "github.com/onsi/gomega/gbytes"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

type FakeCloser struct {
	err      error
	duration time.Duration
}

func (f FakeCloser) Close() error {
	time.Sleep(f.duration)
	return f.err
}

type FakeReader struct {
	err      error
	duration time.Duration
}

func (f FakeReader) Read(p []byte) (int, error) {
	time.Sleep(f.duration)
	if f.err != nil {
		return 0, f.err
	}

	for i := 0; i < len(p); i++ {
		p[i] = 'a'
	}

	return len(p), nil
}

type FakeWriter struct {
	err      error
	duration time.Duration
}

func (f FakeWriter) Write(p []byte) (int, error) {
	time.Sleep(f.duration)
	if f.err != nil {
		return 0, f.err
	}

	return len(p), nil
}

var _ = Describe("Io Wrappers", func() {
	Describe("TimeoutCloser", func() {
		var innerCloser io.Closer
		var timeoutCloser io.Closer

		JustBeforeEach(func() {
			timeoutCloser = TimeoutCloser(innerCloser, 20*time.Millisecond)
		})

		Context("when the underlying Closer closes with no error", func() {
			BeforeEach(func() {
				innerCloser = FakeCloser{}
			})

			It("returns with no error", func() {
				Ω(timeoutCloser.Close()).Should(Succeed())
			})
		})

		Context("when the underlying Closer closes with an error", func() {
			BeforeEach(func() {
				innerCloser = FakeCloser{err: fmt.Errorf("boom")}
			})

			It("returns the error", func() {
				Ω(timeoutCloser.Close()).Should(MatchError("boom"))
			})
		})

		Context("when the underlying Closer hangs", func() {
			BeforeEach(func() {
				innerCloser = FakeCloser{
					err:      fmt.Errorf("boom"),
					duration: time.Hour,
				}
			})

			It("returns ErrTimeout", func() {
				Ω(timeoutCloser.Close()).Should(MatchError(ErrTimeout))
			})
		})
	})

	Describe("TimeoutReader", func() {
		var innerReader io.Reader
		var timeoutReader io.Reader

		JustBeforeEach(func() {
			timeoutReader = TimeoutReader(innerReader, 20*time.Millisecond)
		})

		Context("when the underlying Reader returns no error", func() {
			BeforeEach(func() {
				innerReader = FakeReader{}
			})

			It("returns with no error", func() {
				p := make([]byte, 5)
				n, err := timeoutReader.Read(p)
				Ω(n).Should(Equal(5))
				Ω(err).ShouldNot(HaveOccurred())
				Ω(p).Should(Equal([]byte("aaaaa")))
			})
		})

		Context("when the underlying Reader returns an error", func() {
			BeforeEach(func() {
				innerReader = FakeReader{err: fmt.Errorf("boom")}
			})

			It("returns the error", func() {
				p := make([]byte, 5)
				_, err := timeoutReader.Read(p)
				Ω(err).Should(MatchError("boom"))
			})
		})

		Context("when the underlying Reader hangs", func() {
			BeforeEach(func() {
				innerReader = FakeReader{err: fmt.Errorf("boom"), duration: time.Hour}
			})

			It("returns ErrTimeout", func() {
				p := make([]byte, 5)
				_, err := timeoutReader.Read(p)
				Ω(err).Should(MatchError(ErrTimeout))
			})
		})
	})

	Describe("TimeoutWriter", func() {
		var innerWriter io.Writer
		var timeoutWriter io.Writer

		JustBeforeEach(func() {
			timeoutWriter = TimeoutWriter(innerWriter, 20*time.Millisecond)
		})

		Context("when the underlying Writer returns no error", func() {
			BeforeEach(func() {
				innerWriter = FakeWriter{}
			})

			It("returns with no error", func() {
				n, err := timeoutWriter.Write([]byte("aaaaa"))
				Ω(n).Should(Equal(5))
				Ω(err).ShouldNot(HaveOccurred())
			})
		})

		Context("when the underlying Writer returns an error", func() {
			BeforeEach(func() {
				innerWriter = FakeWriter{err: fmt.Errorf("boom")}
			})

			It("returns the error", func() {
				_, err := timeoutWriter.Write([]byte("aaaaa"))
				Ω(err).Should(MatchError("boom"))
			})
		})

		Context("when the underlying Writer hangs", func() {
			BeforeEach(func() {
				innerWriter = FakeWriter{err: fmt.Errorf("boom"), duration: time.Hour}
			})

			It("returns ErrTimeout", func() {
				_, err := timeoutWriter.Write([]byte("aaaaa"))
				Ω(err).Should(MatchError(ErrTimeout))
			})
		})
	})
})
