package inprochttp

import (
	"context"
	"io"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

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

func TestRoundTripperStreamsBeforeHandlerReturns(t *testing.T) {
	allowFinish := make(chan struct{})
	transport := RoundTripper{Handler: http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Content-Type", "text/event-stream")
		_, _ = writer.Write([]byte("first\n"))
		writer.(http.Flusher).Flush()
		<-allowFinish
		_, _ = writer.Write([]byte("second\n"))
	})}
	request, err := http.NewRequest(http.MethodGet, "http://embedded.meshx/events", nil)
	require.NoError(t, err)
	response, err := transport.RoundTrip(request)
	require.NoError(t, err)
	defer response.Body.Close()

	first := make(chan string, 1)
	go func() {
		payload := make([]byte, len("first\n"))
		_, _ = io.ReadFull(response.Body, payload)
		first <- string(payload)
	}()
	select {
	case payload := <-first:
		require.Equal(t, "first\n", payload)
	case <-time.After(time.Second):
		t.Fatal("first response chunk was buffered until handler completion")
	}
	close(allowFinish)
	remainder, err := io.ReadAll(response.Body)
	require.NoError(t, err)
	require.Equal(t, "second\n", string(remainder))
}

func TestRoundTripperPreservesRequestAndResponseSemantics(t *testing.T) {
	transport := RoundTripper{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/api/v1/memory", request.URL.Path)
		require.Equal(t, "Bearer trusted", request.Header.Get("Authorization"))
		body, err := io.ReadAll(request.Body)
		require.NoError(t, err)
		require.Equal(t, `{"name":"memory"}`, string(body))
		writer.Header().Set("Content-Type", "application/json")
		writer.WriteHeader(http.StatusCreated)
		_, _ = writer.Write([]byte(`{"ok":true}`))
	})}
	request, err := http.NewRequest(http.MethodPost, "http://embedded.meshx/api/v1/memory", strings.NewReader(`{"name":"memory"}`))
	require.NoError(t, err)
	request.Header.Set("Authorization", "Bearer trusted")
	response, err := transport.RoundTrip(request)
	require.NoError(t, err)
	defer response.Body.Close()
	require.Equal(t, http.StatusCreated, response.StatusCode)
	require.Equal(t, "application/json", response.Header.Get("Content-Type"))
	payload, err := io.ReadAll(response.Body)
	require.NoError(t, err)
	require.JSONEq(t, `{"ok":true}`, string(payload))
}

func TestRoundTripperFailsClosedWithoutHandler(t *testing.T) {
	request, err := http.NewRequest(http.MethodGet, "http://embedded.meshx/readyz", nil)
	require.NoError(t, err)
	_, err = (RoundTripper{}).RoundTrip(request)
	require.EqualError(t, err, "in-process HTTP handler is not configured")
}

type trackedBody struct {
	io.Reader
	closes atomic.Int32
}

func (body *trackedBody) Close() error { body.closes.Add(1); return nil }

func TestRoundTripperClosesRequestBodyOnSuccessAndPanic(t *testing.T) {
	for _, panicHandler := range []bool{false, true} {
		body := &trackedBody{Reader: strings.NewReader("payload")}
		transport := RoundTripper{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
			_, _ = io.Copy(io.Discard, request.Body)
			if panicHandler {
				panic("boom")
			}
			writer.WriteHeader(http.StatusNoContent)
		})}
		request, err := http.NewRequest(http.MethodPost, "http://embedded/", body)
		require.NoError(t, err)
		response, err := transport.RoundTrip(request)
		require.NoError(t, err)
		_, _ = io.Copy(io.Discard, response.Body)
		require.NoError(t, response.Body.Close())
		require.Eventually(t, func() bool { return body.closes.Load() == 1 }, time.Second, time.Millisecond)
	}
}

func TestRoundTripperResponseCloseCancelsHandlerAndClosesRequestBody(t *testing.T) {
	body := &trackedBody{Reader: strings.NewReader("payload")}
	handlerDone := make(chan struct{})
	transport := RoundTripper{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		defer close(handlerDone)
		writer.WriteHeader(http.StatusOK)
		<-request.Context().Done()
	})}
	request, err := http.NewRequest(http.MethodPost, "http://embedded/", body)
	require.NoError(t, err)
	response, err := transport.RoundTrip(request)
	require.NoError(t, err)
	require.NoError(t, response.Body.Close())
	select {
	case <-handlerDone:
	case <-time.After(time.Second):
		t.Fatal("handler did not exit after response close")
	}
	require.EqualValues(t, 1, body.closes.Load())
}

func TestRoundTripperRequestCancelClosesBody(t *testing.T) {
	body := &trackedBody{Reader: strings.NewReader("payload")}
	ctx, cancel := context.WithCancel(context.Background())
	transport := RoundTripper{Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) { <-ctx.Done() })}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://embedded/", body)
	require.NoError(t, err)
	cancel()
	_, err = transport.RoundTrip(request)
	require.ErrorIs(t, err, context.Canceled)
	require.EqualValues(t, 1, body.closes.Load())
}
