package router

import (
	"bytes"
	"compress/gzip"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
)

func newCompressionTestRouter(handler gin.HandlerFunc) *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	router.Use(gzipCompressionMiddleware())
	router.GET("/test", handler)
	return router
}

func TestGzipCompressionMiddlewareCompressesLargeJSON(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"payload": strings.Repeat("diff-line\n", 512)})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "gzip", recorder.Header().Get("Content-Encoding"))
	require.Contains(t, recorder.Header().Values("Vary"), "Accept-Encoding")
	reader, err := gzip.NewReader(bytes.NewReader(recorder.Body.Bytes()))
	require.NoError(t, err)
	body, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())
	require.Contains(t, string(body), "diff-line")
}

func TestGzipCompressionMiddlewareStreamsLargeJSONWithoutBufferingWholeResponse(t *testing.T) {
	const chunk = "diff-line\n"
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.Header("Content-Type", "application/json")
		for range 4096 {
			_, err := ctx.Writer.WriteString(chunk)
			require.NoError(t, err)
		}
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, "gzip", recorder.Header().Get("Content-Encoding"))
	reader, err := gzip.NewReader(bytes.NewReader(recorder.Body.Bytes()))
	require.NoError(t, err)
	body, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())
	require.Equal(t, strings.Repeat(chunk, 4096), string(body))
}

func TestGzipCompressionMiddlewarePreservesErrorStatus(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"error": strings.Repeat("unsupported review target ", 128),
		})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusUnprocessableEntity, recorder.Code)
	require.Equal(t, "gzip", recorder.Header().Get("Content-Encoding"))
	reader, err := gzip.NewReader(bytes.NewReader(recorder.Body.Bytes()))
	require.NoError(t, err)
	body, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())
	require.Contains(t, string(body), "unsupported review target")
}

func TestGzipCompressionMiddlewarePreservesRedirectStatus(t *testing.T) {
	const redirectURL = "https://media.example.test/avatar"
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.Redirect(http.StatusTemporaryRedirect, redirectURL)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusTemporaryRedirect, recorder.Code)
	require.Equal(t, redirectURL, recorder.Header().Get("Location"))
	require.Empty(t, recorder.Header().Get("Content-Encoding"))
}

func TestGzipCompressionMiddlewareSkipsSmallResponses(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"ok": true})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Empty(t, recorder.Header().Get("Content-Encoding"))
	require.Contains(t, recorder.Body.String(), `"ok":true`)
}

func TestGzipCompressionMiddlewareSkipsStreamingContentTypes(t *testing.T) {
	for _, contentType := range []string{"text/event-stream", "application/x-ndjson"} {
		t.Run(contentType, func(t *testing.T) {
			router := newCompressionTestRouter(func(ctx *gin.Context) {
				ctx.Header("Content-Type", contentType)
				_, _ = ctx.Writer.Write([]byte(strings.Repeat("event\n", 512)))
			})

			recorder := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, "/test", nil)
			request.Header.Set("Accept-Encoding", "gzip")
			router.ServeHTTP(recorder, request)

			require.Equal(t, http.StatusOK, recorder.Code)
			require.Empty(t, recorder.Header().Get("Content-Encoding"))
			require.Contains(t, recorder.Body.String(), "event")
		})
	}
}

func TestGzipCompressionMiddlewareFlushesStreamingContentImmediately(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.Header("Content-Type", "text/event-stream")
		_, err := ctx.Writer.WriteString("data: ready\n\n")
		require.NoError(t, err)
		ctx.Writer.Flush()
		require.True(t, ctx.Writer.Written())
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Empty(t, recorder.Header().Get("Content-Encoding"))
	require.Equal(t, "data: ready\n\n", recorder.Body.String())
	require.True(t, recorder.Flushed)
}

func TestGzipCompressionMiddlewareHonorsQZero(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"payload": strings.Repeat("diff-line\n", 512)})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip;q=0, br")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Empty(t, recorder.Header().Get("Content-Encoding"))
	require.Contains(t, recorder.Body.String(), "diff-line")
}

func TestGzipCompressionMiddlewareSkipsWhenClientDoesNotAcceptGzip(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"payload": strings.Repeat("diff-line\n", 512)})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Empty(t, recorder.Header().Get("Content-Encoding"))
	require.Contains(t, recorder.Body.String(), "diff-line")
}

func TestGzipCompressionMiddlewareSkipsWebSocketUpgrade(t *testing.T) {
	router := newCompressionTestRouter(func(ctx *gin.Context) {
		ctx.JSON(http.StatusSwitchingProtocols, gin.H{"payload": strings.Repeat("diff-line\n", 512)})
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/test", nil)
	request.Header.Set("Accept-Encoding", "gzip")
	request.Header.Set("Connection", "Upgrade")
	request.Header.Set("Upgrade", "websocket")
	router.ServeHTTP(recorder, request)

	require.Empty(t, recorder.Header().Get("Content-Encoding"))
}
