package proxy_test

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/jarcoal/httpmock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/proxy"
)

func TestReverseProxyCanForward(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	rp, err := proxy.NewReverseProxy("http://cluster-service.mock")
	require.NoError(t, err)
	rp = rp.WithPathPerfixRewrite("/api/dedicated/v1", "/api/v1")
	group := eng.Group("/api/dedicated/v1")
	group.GET("/clusters/:clusterId", gin.WrapH(rp))
	group.POST("/clusters/:clusterId", gin.WrapH(rp))
	group.POST("/clusters/:clusterId/pause", gin.WrapH(rp.WithActionFormat()))
	group.PATCH("/clusters/:clusterId", gin.WrapH(rp))
	group.DELETE("/clusters/:clusterId", gin.WrapH(rp))
	echoHandlerFunc := func(r *http.Request) (*http.Response, error) {
		out := map[string]string{}
		out["scheme"] = r.URL.Scheme
		out["host"] = r.URL.Host
		out["path"] = r.URL.Path
		out["raw_query"] = r.URL.RawQuery
		out["method"] = r.Method
		if r.Body != nil {
			b, err := io.ReadAll(r.Body)
			if err != nil {
				return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
			}
			defer r.Body.Close()
			out["body"] = string(b)
		}
		return httpmock.NewJsonResponse(http.StatusOK, out)
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		t.Logf("not found handler for url: %+v\n", r.URL.String())
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters", echoHandlerFunc)
	httpmock.RegisterResponder(http.MethodDelete, "=~^/api/v1/clusters", echoHandlerFunc)
	httpmock.RegisterResponder(http.MethodPatch, "=~^/api/v1/clusters", echoHandlerFunc)
	httpmock.RegisterResponder(http.MethodPost, "=~^/api/v1/clusters", echoHandlerFunc)
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	{
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters/foo?foo=bar", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := map[string]string{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
		require.Equal(t, "http", out["scheme"])
		require.Equal(t, "cluster-service.mock", out["host"])
		require.Equal(t, "/api/v1/clusters/foo", out["path"])
		require.Equal(t, "GET", out["method"])
		require.Equal(t, "foo=bar", out["raw_query"])
	}
	{
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		ctx.Request, _ = http.NewRequest(http.MethodDelete, "/api/dedicated/v1/clusters/foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := map[string]string{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
		require.Equal(t, "http", out["scheme"])
		require.Equal(t, "cluster-service.mock", out["host"])
		require.Equal(t, "/api/v1/clusters/foo", out["path"])
		require.Equal(t, "DELETE", out["method"])
	}
	{
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		body := bytes.NewReader([]byte(`{"foo":"bar"}`))
		ctx.Request, _ = http.NewRequest(http.MethodPost, "/api/dedicated/v1/clusters/foo", body)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := map[string]string{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
		require.Equal(t, "http", out["scheme"])
		require.Equal(t, "cluster-service.mock", out["host"])
		require.Equal(t, "/api/v1/clusters/foo", out["path"])
		require.Equal(t, "POST", out["method"])
		require.Equal(t, `{"foo":"bar"}`, out["body"])
	}
	{
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		body := bytes.NewReader([]byte(`{"foo":"baz"}`))
		ctx.Request, _ = http.NewRequest(http.MethodPatch, "/api/dedicated/v1/clusters/foo", body)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := map[string]string{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
		require.Equal(t, "http", out["scheme"])
		require.Equal(t, "cluster-service.mock", out["host"])
		require.Equal(t, "/api/v1/clusters/foo", out["path"])
		require.Equal(t, "PATCH", out["method"])
		require.Equal(t, `{"foo":"baz"}`, out["body"])
	}
	{
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		ctx.Request, _ = http.NewRequest(http.MethodPost, "/api/dedicated/v1/clusters/foo/pause", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := map[string]string{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
		require.Equal(t, "http", out["scheme"])
		require.Equal(t, "cluster-service.mock", out["host"])
		require.Equal(t, "/api/v1/clusters/foo:pause", out["path"])
		require.Equal(t, "POST", out["method"])
	}
}

// https://github.com/gin-gonic/gin/blob/ce20f107f5dc498ec7489d7739541a25dcd48463/context_test.go#L1747-L1765
// reverse porxy need CliseNotify
type testResponseRecorder struct {
	*httptest.ResponseRecorder
	closeChannel chan bool
}

func (r *testResponseRecorder) CloseNotify() <-chan bool {
	return r.closeChannel
}

func (r *testResponseRecorder) closeClient() {
	r.closeChannel <- true
}

func createTestResponseRecorder() *testResponseRecorder {
	return &testResponseRecorder{
		httptest.NewRecorder(),
		make(chan bool, 1),
	}
}
