// Copyright 2014 Martini Authors
// Copyright 2014 The Macaron 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 (
	"bytes"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"

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

var multipartFormTestCases = []multipartFormTestCase{
	{
		description:      "Happy multipart form path",
		shouldSucceed:    true,
		inputAndExpected: BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:         "FormValue called before req.MultipartReader(); see https://github.com/martini-contrib/csrf/issues/6",
		shouldSucceed:       true,
		callFormValueBefore: true,
		inputAndExpected:    BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:      "Empty payload",
		shouldSucceed:    false,
		inputAndExpected: BlogPost{},
	},
	{
		description:      "Missing required field (Id)",
		shouldSucceed:    false,
		inputAndExpected: BlogPost{Post: Post{Title: "Glorious Post Title"}, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:      "Required embedded struct field not specified",
		shouldSucceed:    false,
		inputAndExpected: BlogPost{Id: 1, Author: Person{Name: "Matt Holt"}},
	},
	{
		description:      "Required nested struct field not specified",
		shouldSucceed:    false,
		inputAndExpected: BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1},
	},
	{
		description:      "Multiple values",
		shouldSucceed:    true,
		inputAndExpected: BlogPost{Post: Post{Title: "Glorious Post Title"}, Id: 1, Author: Person{Name: "Matt Holt"}, Ratings: []int{3, 5, 4}},
	},
	{
		description:     "Bad multipart encoding",
		shouldSucceed:   false,
		malformEncoding: true,
	},
}

func Test_MultipartForm(t *testing.T) {
	for _, testCase := range multipartFormTestCases {
		performMultipartFormTest(t, MultipartForm, testCase)
	}
}

func performMultipartFormTest(t *testing.T, binder handlerFunc, testCase multipartFormTestCase) {
	httpRecorder := httptest.NewRecorder()
	m := chi.NewRouter()

	m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) {
		var actual BlogPost
		errs := binder(req, &actual)
		if testCase.shouldSucceed {
			assert.Empty(t, errs)
		} else if !testCase.shouldSucceed {
			assert.NotEmpty(t, errs)
		}
		assert.Equal(t, fmt.Sprintf("%+v", testCase.inputAndExpected), fmt.Sprintf("%+v", actual))
	})

	multipartPayload, mpWriter := makeMultipartPayload(testCase)

	req, err := http.NewRequest("POST", testRoute, multipartPayload)
	if err != nil {
		panic(err)
	}

	req.Header.Add("Content-Type", mpWriter.FormDataContentType())

	err = mpWriter.Close()
	if err != nil {
		panic(err)
	}

	if testCase.callFormValueBefore {
		req.FormValue("foo")
	}

	m.ServeHTTP(httpRecorder, req)

	switch httpRecorder.Code {
	case http.StatusNotFound:
		panic("Routing is messed up in test fixture (got 404): check methods and paths")
	case http.StatusInternalServerError:
		panic("Something bad happened on '" + testCase.description + "'")
	}
}

// Writes the input from a test case into a buffer using the multipart writer.
func makeMultipartPayload(testCase multipartFormTestCase) (*bytes.Buffer, *multipart.Writer) {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	if testCase.malformEncoding {
		// TODO: Break the multipart form parser which is apparently impervious!!
		// (Get it to return an error. Trying to get 100% test coverage.)
		body.Write([]byte(`--` + writer.Boundary() + `\nContent-Disposition: form-data; name="foo"\n\n--` + writer.Boundary() + `--`))
		return body, writer
	}
	_ = writer.WriteField("title", testCase.inputAndExpected.Title)
	_ = writer.WriteField("content", testCase.inputAndExpected.Content)
	_ = writer.WriteField("id", strconv.Itoa(testCase.inputAndExpected.Id))
	_ = writer.WriteField("ignored", testCase.inputAndExpected.Ignored)
	for _, value := range testCase.inputAndExpected.Ratings {
		_ = writer.WriteField("rating", strconv.Itoa(value))
	}
	_ = writer.WriteField("name", testCase.inputAndExpected.Author.Name)
	_ = writer.WriteField("email", testCase.inputAndExpected.Author.Email)
	return body, writer
}

type (
	multipartFormTestCase struct {
		description         string
		shouldSucceed       bool
		inputAndExpected    BlogPost
		malformEncoding     bool
		callFormValueBefore bool
	}
)
