package httptest

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"

	"github.com/labstack/echo/v4"
	"gopkg.in/yaml.v3"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/services/tcms/tests/configtest"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

const defaultUserID = 0

var jwtKey []byte = configtest.NewTestConfig(&testing.T{}).JWTKey

// Serve runs test on the API.
// B is the expected response body type, pass `any` if you don't care about it.
func Serve[B any](e *echo.Echo, method, path string, opts ...requestOpts) *ResponseRecorder[B] {
	req := httptest.NewRequest(method, path, nil)
	respOpt := &respOpt{}
	for _, opt := range opts {
		if opt != nil {
			opt(req, respOpt)
		}
	}
	// The userID will be set to 0 by default.
	if req.Header.Get(echo.HeaderAuthorization) == "" {
		req.Header.Set(echo.HeaderAuthorization, genJWTHeader(jwtKey, defaultUserID))
	}
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	return &ResponseRecorder[B]{rec, respOpt}
}

// WithJSON sets JSON body to request.
// `body` will be serialized to JSON if it's not a []byte.
func WithJSON(body any) requestOpts {
	return func(r *http.Request, _ *respOpt) {
		var reader *bytes.Reader
		switch body := body.(type) {
		case []byte:
			reader = bytes.NewReader(body)
		default:
			reader = bytes.NewReader(utils.ToJSON(body))
		}
		r.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
		r.ContentLength = int64(reader.Len())
		r.Body = io.NopCloser(reader)
	}
}

// WithUser sets JWT in request header to pass authentication.
func WithUser[N model.ID | int](userID N) requestOpts {
	return func(r *http.Request, _ *respOpt) {
		v := genJWTHeader(jwtKey, model.ID(userID))
		r.Header.Set(echo.HeaderAuthorization, v)
	}
}

func WithHeader(k, v string) requestOpts {
	return func(r *http.Request, _ *respOpt) {
		r.Header.Set(k, v)
	}
}

// Unmarshals response body to Multi-YAML.
// Multi-YAML is multiple YAML documents separated by "---".
func WithResponseMultiYAML(multiple bool) requestOpts {
	return func(_ *http.Request, opt *respOpt) {
		opt.multiYAML = multiple
	}
}

type ResponseRecorder[B any] struct {
	*httptest.ResponseRecorder
	opt *respOpt
}

// GetBody unmarshals the response body to the given type.
func (rec *ResponseRecorder[B]) GetBody() B {
	body := new(B)
	// if B is any or interface{}, skip unmarshalling the response body.
	if reflect.TypeOf(body).Elem().Kind() == reflect.Interface {
		return *new(B)
	}

	// determine the unmarshal function via ContentType
	var unmarshal UnmarshalBodyF
	ty := rec.Header().Get(echo.HeaderContentType)
	switch ty {
	case echo.MIMEApplicationJSON, echo.MIMEApplicationJSONCharsetUTF8:
		unmarshal = json.Unmarshal
	case "application/x-yaml", "text/yaml", "text/vnd.yaml", "text/x-yaml":
		if rec.opt.multiYAML {
			unmarshal = unmarshalMultiYAML
		} else {
			unmarshal = yaml.Unmarshal
		}
	case echo.MIMEApplicationXML, echo.MIMEApplicationXMLCharsetUTF8:
		unmarshal = xml.Unmarshal
	case echo.MIMETextPlain:
		unmarshal = func(b []byte, v interface{}) error {
			*v.(*string) = string(b)
			return nil
		}
	default:
		panic("unknown ContentType: " + ty)
	}

	if err := unmarshal(rec.Body.Bytes(), body); err != nil {
		panic("failed to unmarshal response body: " + err.Error())
	}
	return *body
}

func unmarshalMultiYAML(in []byte, out any) error {
	sliceV := reflect.ValueOf(out).Elem()
	if sliceV.Kind() != reflect.Slice {
		return errors.New("MultiYAML: out must be a slice, but got " + sliceV.Kind().String())
	}
	itemT := sliceV.Type().Elem()

	decoder := yaml.NewDecoder(bytes.NewReader(in))
	for {
		item := reflect.New(itemT)
		if err := decoder.Decode(item.Interface()); err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		reflect.Append(sliceV, item.Elem())
	}
	return nil
}

type requestOpts func(r *http.Request, respOpt *respOpt)

type respOpt struct {
	multiYAML bool
}

type UnmarshalBodyF func(in []byte, out any) error

func genJWTHeader(jwtKey []byte, userID model.ID) string {
	return "Bearer " + utils.JWTClaims{
		User: utils.SessionUser{
			ID: userID,
		},
	}.Encode(jwtKey)
}

func must200[B any](target string, rec *ResponseRecorder[B]) {
	if rec.Code != http.StatusOK {
		panic(fmt.Sprintf("Error %d on %s: %s", rec.Code, target, rec.Body.String()))
	}
}
