package httpserver

import (
	"bytes"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/pocket-id/pocket-id/backend/internal/apperror"
	"github.com/stretchr/testify/require"
	"golang.org/x/text/unicode/norm"
)

func TestBindJSONClassifiesMalformedBody(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"name":`))
	c.Request.Header.Set("Content-Type", "application/json")

	var input struct {
		Name string `json:"name"`
	}
	err := BindJSON(c, &input)

	require.True(t, apperror.IsCode(err, apperror.CodeInvalidRequestBody))
	require.Contains(t, err.Error(), "unexpected EOF")
	var appErr *apperror.Error
	require.ErrorAs(t, err, &appErr)
	require.NotContains(t, appErr.ClientMessage(), "unexpected end")
}

func TestBindJSONNormalizesTaggedFieldsRecursively(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	c.Request = httptest.NewRequestWithContext(
		t.Context(),
		http.MethodPost,
		"/",
		strings.NewReader(`{"name":"Cafe\u0301","email":"user@cafe\u0301.example","items":[{"label":"Re\u0301sume\u0301"}]}`),
	)
	c.Request.Header.Set("Content-Type", "application/json")

	type embeddedInput struct {
		Name string `json:"name" unorm:"nfc"`
	}
	type itemInput struct {
		Label string `json:"label" unorm:"nfc"`
	}
	var input struct {
		embeddedInput
		Email *string     `json:"email" unorm:"nfc"`
		Items []itemInput `json:"items"`
	}
	err := BindJSON(c, &input)

	require.NoError(t, err)
	require.Equal(t, norm.NFC.String("Café"), input.Name)
	require.NotNil(t, input.Email)
	require.Equal(t, norm.NFC.String("user@café.example"), *input.Email)
	require.Equal(t, norm.NFC.String("Résumé"), input.Items[0].Label)
}

func TestBindJSONRejectsFormCompatibleContentTypes(t *testing.T) {
	for _, contentType := range []string{"text/plain", "application/x-www-form-urlencoded", "multipart/form-data; boundary=test"} {
		t.Run(contentType, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"name":"admin"}`))
			c.Request.Header.Set("Content-Type", contentType)

			var input struct {
				Name string `json:"name"`
			}
			err := BindJSON(c, &input)

			require.True(t, apperror.IsCode(err, apperror.CodeInvalidRequestBody))
			require.Empty(t, input.Name)
		})
	}
}

func TestBindJSONAcceptsStructuredJSONContentType(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"name":"admin"}`))
	c.Request.Header.Set("Content-Type", "application/scim+json")

	var input struct {
		Name string `json:"name"`
	}
	err := BindJSON(c, &input)

	require.NoError(t, err)
	require.Equal(t, "admin", input.Name)
}

func TestBindOptionalJSONDoesNotRelyOnContentLength(t *testing.T) {
	for _, test := range []struct {
		name          string
		contentLength int64
	}{
		{name: "zero", contentLength: 0},
		{name: "unknown", contentLength: -1},
	} {
		t.Run(test.name, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"secret":"custom-secret"}`))
			c.Request.ContentLength = test.contentLength
			c.Request.Header.Set("Content-Type", "application/json")

			var input struct {
				Secret string `json:"secret"`
			}
			err := BindOptionalJSON(c, &input)

			require.NoError(t, err)
			require.Equal(t, "custom-secret", input.Secret)
		})
	}
}

func TestFormFileClassifiesMissingField(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	require.NoError(t, writer.Close())
	c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", &body)
	c.Request.Header.Set("Content-Type", writer.FormDataContentType())

	_, err := FormFile(c, "file")

	require.True(t, apperror.IsCode(err, apperror.CodeValidationFailed))
	var appErr *apperror.Error
	require.ErrorAs(t, err, &appErr)
	require.Equal(t, []apperror.FieldError{{
		Field:   "file",
		Code:    "required",
		Message: "is required",
	}}, appErr.Fields())
}
