// Copyright 2014 Martini Authors
// Copyright 2014 The Macaron Authors
// Copyright 2024 The Forgejo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package binding

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"runtime"
	"strings"
	"testing"

	chi "github.com/go-chi/chi/v5"
	"github.com/stretchr/testify/assert"
)

var jsonTestCases = []jsonTestCase{
	{
		description:         "Happy path",
		shouldSucceedOnJSON: true,
		payload:             `{"title": "Glorious Post Title", "content": "Lorem ipsum dolor sit amet"}`,
		contentType:         jsonContentType,
		expected:            Post{Title: "Glorious Post Title", Content: "Lorem ipsum dolor sit amet"},
	},
	{
		description:         "Happy path with interface",
		shouldSucceedOnJSON: true,
		withInterface:       true,
		payload:             `{"title": "Glorious Post Title", "content": "Lorem ipsum dolor sit amet"}`,
		contentType:         jsonContentType,
		expected:            Post{Title: "Glorious Post Title", Content: "Lorem ipsum dolor sit amet"},
	},
	{
		description:         "Nil payload",
		shouldSucceedOnJSON: false,
		payload:             `-nil-`,
		contentType:         jsonContentType,
		expected:            Post{},
	},
	{
		description:         "Empty payload",
		shouldSucceedOnJSON: false,
		payload:             ``,
		contentType:         jsonContentType,
		expected:            Post{},
	},
	{
		description:         "Empty content type",
		shouldSucceedOnJSON: true,
		shouldFailOnBind:    true,
		payload:             `{"title": "Glorious Post Title", "content": "Lorem ipsum dolor sit amet"}`,
		contentType:         ``,
		expected:            Post{Title: "Glorious Post Title", Content: "Lorem ipsum dolor sit amet"},
	},
	{
		description:         "Unsupported content type",
		shouldSucceedOnJSON: true,
		shouldFailOnBind:    true,
		payload:             `{"title": "Glorious Post Title", "content": "Lorem ipsum dolor sit amet"}`,
		contentType:         `BoGuS`,
		expected:            Post{Title: "Glorious Post Title", Content: "Lorem ipsum dolor sit amet"},
	},
	{
		description:         "Malformed JSON",
		shouldSucceedOnJSON: false,
		payload:             `{"title":"foo"`,
		contentType:         jsonContentType,
		expected:            Post{Title: "foo"},
	},
	{
		description:         "Deserialization with nested and embedded struct",
		shouldSucceedOnJSON: true,
		payload:             `{"title":"Glorious Post Title", "id":1, "author":{"name":"Matt Holt"}}`,
		contentType:         jsonContentType,
		expected:            BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:         "Deserialization with nested and embedded struct with interface",
		shouldSucceedOnJSON: true,
		withInterface:       true,
		payload:             `{"title":"Glorious Post Title", "id":1, "author":{"name":"Matt Holt"}}`,
		contentType:         jsonContentType,
		expected:            BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:         "Required nested struct field not specified",
		shouldSucceedOnJSON: false,
		payload:             `{"title":"Glorious Post Title", "id":1, "author":{}}`,
		contentType:         jsonContentType,
		expected:            BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1},
	},
	{
		description:         "Required embedded struct field not specified",
		shouldSucceedOnJSON: false,
		payload:             `{"id":1, "author":{"name":"Matt Holt"}}`,
		contentType:         jsonContentType,
		expected:            BlogPost{Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:         "Slice of Posts",
		shouldSucceedOnJSON: true,
		payload:             `[{"title": "First Post"}, {"title": "Second Post"}]`,
		contentType:         jsonContentType,
		expected:            []Post{{Title: "First Post"}, {Title: "Second Post"}},
	},
	{
		description:         "Slice of structs",
		shouldSucceedOnJSON: true,
		payload:             `{"name": "group1", "people": [{"name":"awoods"}, {"name": "anthony"}]}`,
		contentType:         jsonContentType,
		expected:            Group{Name: "group1", People: []Person{{Name: "awoods"}, {Name: "anthony"}}},
	},
}

func Test_Json(t *testing.T) {
	for _, testCase := range jsonTestCases {
		performJSONTest(t, JSON, testCase)
	}
}

func performJSONTest(t *testing.T, binder handlerFunc, testCase jsonTestCase) {
	fnName := runtime.FuncForPC(reflect.ValueOf(binder).Pointer()).Name()
	t.Run(testCase.description, func(t *testing.T) {
		var payload io.Reader
		httpRecorder := httptest.NewRecorder()
		m := chi.NewRouter()

		jsonTestHandler := func(actual any, errs Errors) {
			switch fnName {
			case "JSON":
				if testCase.shouldSucceedOnJSON {
					assert.Empty(t, errs, errs)
					assert.Equal(t, fmt.Sprintf("%+v", testCase.expected), fmt.Sprintf("%+v", actual))
				} else {
					assert.NotEmpty(t, errs)
				}
			case "Bind":
				if !testCase.shouldFailOnBind {
					assert.Empty(t, errs, errs)
				} else {
					assert.NotEmpty(t, errs)
					assert.Equal(t, fmt.Sprintf("%+v", testCase.expected), fmt.Sprintf("%+v", actual))
				}
			}
		}

		switch p := testCase.expected.(type) {
		case []Post:
			if testCase.withInterface {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual []Post
					errs := binder(req, &actual)
					for i, a := range actual {
						assert.Equal(t, p[i].Title, a.Title)
						jsonTestHandler(a, errs)
					}
				})
			} else {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual []Post
					errs := binder(req, &actual)
					jsonTestHandler(actual, errs)
				})
			}

		case Post:
			if testCase.withInterface {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual Post
					errs := binder(req, &actual)
					assert.Equal(t, p.Title, actual.Title)
					jsonTestHandler(actual, errs)
				})
			} else {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual Post
					errs := binder(req, &actual)
					jsonTestHandler(actual, errs)
				})
			}

		case BlogPost:
			if testCase.withInterface {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual BlogPost
					errs := binder(req, &actual)
					assert.Equal(t, p.Title, actual.Title)
					jsonTestHandler(actual, errs)
				})
			} else {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual BlogPost
					errs := binder(req, &actual)
					jsonTestHandler(actual, errs)
				})
			}
		case Group:
			if testCase.withInterface {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual Group
					errs := binder(req, &actual)
					assert.Equal(t, p.Name, actual.Name)
					jsonTestHandler(actual, errs)
				})
			} else {
				m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
					var actual Group
					errs := binder(req, &actual)
					jsonTestHandler(actual, errs)
				})
			}
		}

		if testCase.payload == "-nil-" {
			payload = nil
		} else {
			payload = strings.NewReader(testCase.payload)
		}

		req, err := http.NewRequest("POST", testRoute, payload)
		if err != nil {
			panic(err)
		}
		req.Header.Set("Content-Type", testCase.contentType)

		m.ServeHTTP(httpRecorder, req)

		switch httpRecorder.Code {
		case http.StatusNotFound:
			panic("Routing is messed up in test fixture (got 404): check method and path")
		case http.StatusInternalServerError:
			panic("Something bad happened on '" + testCase.description + "'")
		default:
			if testCase.shouldSucceedOnJSON &&
				httpRecorder.Code != http.StatusOK &&
				!testCase.shouldFailOnBind {
				assert.Equal(t, http.StatusOK, httpRecorder.Code)
			}
		}
	})
}

type (
	jsonTestCase struct {
		description         string
		withInterface       bool
		shouldSucceedOnJSON bool
		shouldFailOnBind    bool
		payload             string
		contentType         string
		expected            any
	}
)
