package backend

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	rtest "github.com/restic/restic/internal/test"
)

func TestRead(t *testing.T) {
	data := []byte("abcdef")
	var ctr int
	kick := func() {
		ctr++
	}
	var closed bool
	onClose := func() {
		closed = true
	}
	isTimeout := func(err error) bool {
		return false
	}

	wd := newWatchdogReadCloser(io.NopCloser(bytes.NewReader(data)), 1, kick, onClose, isTimeout)

	out, err := io.ReadAll(wd)
	rtest.OK(t, err)
	rtest.Equals(t, data, out, "data mismatch")
	// the EOF read also triggers the kick function
	rtest.Equals(t, len(data)*2+2, ctr, "unexpected number of kick calls")

	rtest.Equals(t, false, closed, "close function called too early")
	rtest.OK(t, wd.Close())
	rtest.Equals(t, true, closed, "close function not called")
}

func TestRoundtrip(t *testing.T) {
	t.Parallel()

	// at the higher delay values, it takes longer to transmit the request/response body
	// than the roundTripper timeout
	for _, delay := range []int{0, 1, 10, 20} {
		t.Run(fmt.Sprintf("%v", delay), func(t *testing.T) {
			msg := []byte("ping-pong-data")
			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				data, err := io.ReadAll(r.Body)
				if err != nil {
					w.WriteHeader(500)
					return
				}
				w.WriteHeader(200)

				// slowly send the reply
				for len(data) >= 2 {
					_, _ = w.Write(data[:2])
					w.(http.Flusher).Flush()
					data = data[2:]
					time.Sleep(time.Duration(delay) * time.Millisecond)
				}
				_, _ = w.Write(data)
			}))
			defer srv.Close()

			rt := newWatchdogRoundtripper(http.DefaultTransport, 100*time.Millisecond, 2)
			req, err := http.NewRequestWithContext(context.TODO(), "GET", srv.URL, io.NopCloser(newSlowReader(bytes.NewReader(msg), time.Duration(delay)*time.Millisecond)))
			rtest.OK(t, err)

			resp, err := rt.RoundTrip(req)
			rtest.OK(t, err)
			rtest.Equals(t, 200, resp.StatusCode, "unexpected status code")

			response, err := io.ReadAll(resp.Body)
			rtest.OK(t, err)
			rtest.Equals(t, msg, response, "unexpected response")

			rtest.OK(t, resp.Body.Close())
		})
	}
}

func TestCanceledRoundtrip(t *testing.T) {
	rt := newWatchdogRoundtripper(http.DefaultTransport, time.Second, 2)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req, err := http.NewRequestWithContext(ctx, "GET", "http://some.random.url.dfdgsfg", nil)
	rtest.OK(t, err)

	resp, err := rt.RoundTrip(req)
	rtest.Equals(t, context.Canceled, err)
	// make linter happy
	if resp != nil {
		rtest.OK(t, resp.Body.Close())
	}
}

type slowReader struct {
	data  io.Reader
	delay time.Duration
}

func newSlowReader(data io.Reader, delay time.Duration) *slowReader {
	return &slowReader{
		data:  data,
		delay: delay,
	}
}

func (s *slowReader) Read(p []byte) (n int, err error) {
	time.Sleep(s.delay)
	return s.data.Read(p)
}

func TestUploadTimeout(t *testing.T) {
	t.Parallel()

	msg := []byte("ping")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(500)
			return
		}
		t.Error("upload should have been canceled")
	}))
	defer srv.Close()

	rt := newWatchdogRoundtripper(http.DefaultTransport, 10*time.Millisecond, 1024)
	req, err := http.NewRequestWithContext(context.TODO(), "GET", srv.URL, io.NopCloser(newSlowReader(bytes.NewReader(msg), 100*time.Millisecond)))
	rtest.OK(t, err)

	resp, err := rt.RoundTrip(req)
	rtest.Equals(t, errRequestTimeout, err)
	// make linter happy
	if resp != nil {
		rtest.OK(t, resp.Body.Close())
	}
}

func TestProcessingTimeout(t *testing.T) {
	t.Parallel()

	msg := []byte("ping")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(500)
			return
		}
		time.Sleep(100 * time.Millisecond)
		w.WriteHeader(200)
	}))
	defer srv.Close()

	rt := newWatchdogRoundtripper(http.DefaultTransport, 10*time.Millisecond, 1024)
	req, err := http.NewRequestWithContext(context.TODO(), "GET", srv.URL, io.NopCloser(bytes.NewReader(msg)))
	rtest.OK(t, err)

	resp, err := rt.RoundTrip(req)
	rtest.Equals(t, errRequestTimeout, err)
	// make linter happy
	if resp != nil {
		rtest.OK(t, resp.Body.Close())
	}
}

func TestDownloadTimeout(t *testing.T) {
	t.Parallel()

	msg := []byte("ping")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		data, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(500)
			return
		}
		w.WriteHeader(200)
		_, _ = w.Write(data[:2])
		w.(http.Flusher).Flush()
		data = data[2:]

		time.Sleep(100 * time.Millisecond)
		_, _ = w.Write(data)

	}))
	defer srv.Close()

	rt := newWatchdogRoundtripper(http.DefaultTransport, 25*time.Millisecond, 1024)
	req, err := http.NewRequestWithContext(context.TODO(), "GET", srv.URL, io.NopCloser(bytes.NewReader(msg)))
	rtest.OK(t, err)

	resp, err := rt.RoundTrip(req)
	rtest.OK(t, err)
	rtest.Equals(t, 200, resp.StatusCode, "unexpected status code")

	_, err = io.ReadAll(resp.Body)
	rtest.Equals(t, errRequestTimeout, err, "response download not canceled")
	rtest.OK(t, resp.Body.Close())
}
