// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package requests

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sync"
	"testing"

	"github.com/pb33f/libopenapi"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/pb33f/libopenapi-validator/config"
	"github.com/pb33f/libopenapi-validator/helpers"
	"github.com/pb33f/libopenapi-validator/paths"
)

func TestValidateBody_NotRequiredBody(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody)

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_MediaRangeContentType_Wildcard_end(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          thomas/*:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this?

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_MediaRangeContentType_Wildcards(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          "*/*":
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)
	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this?

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_InvalidBasicSchema_MediaRangeContentType_Wildcard_Required(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: false
        content:
          "*/json":
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "foo/json")

	valid, errors := v.ValidateRequestBody(request)

	// double-tap to hit the cache
	_, _ = v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
}

func TestValidateBody_UnknownContentType(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this?

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST operation request content type 'thomas/tank-engine' does not exist", errors[0].Message)
	assert.Equal(t, "The content type is invalid, Use one of the 1 "+
		"supported types for this operation: application/json", errors[0].HowToFix)
	assert.Equal(t, request.Method, errors[0].RequestMethod)
	assert.Equal(t, request.URL.Path, errors[0].RequestPath)
	assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath)
}

func TestValidateBody_SkipValidationForNonJSON(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/yaml:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/yaml")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_PathNotFound(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST Path '/I do not exist' not found", errors[0].Message)
	assert.Equal(t, request.Method, errors[0].RequestMethod)
	assert.Equal(t, request.URL.Path, errors[0].RequestPath)
	assert.Equal(t, "", errors[0].SpecPath)
}

func TestValidateBody_OperationNotFound(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil)
	assert.Len(t, validationErrors, 0)

	request2, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request2.Header.Set("Content-Type", "application/json")
	valid, errors := v.ValidateRequestBodyWithPathItem(request2, pathItem, pathValue)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "GET operation request content type 'GET' does not exist", errors[0].Message)
	assert.Equal(t, request2.Method, errors[0].RequestMethod)
	assert.Equal(t, request.URL.Path, errors[0].RequestPath)
	assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath)
}

func TestValidateBody_SetPath(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBodyWithPathItem(request, nil, "")

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST Path '/burgers/createBurger' not found", errors[0].Message)
}

func TestValidateBody_ContentTypeNotFound(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("content-type", "application/not-json")

	pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil)
	assert.Len(t, validationErrors, 0)
	valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
}

func TestValidateBody_ContentTypeNotSet(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))

	pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}})
	assert.Len(t, validationErrors, 0)
	valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
}

func TestValidateBody_InvalidBasicSchema_NotRequired(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	// double-tap to hit the cache
	_, _ = v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
}

func TestValidateBody_InvalidBasicSchema_Required(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	// double-tap to hit the cache
	_, _ = v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
}

func TestValidateBody_ValidBasicSchema(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_UsesGetBodyWhenBodyAlreadyConsumed(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, patties, vegetarian]
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}
	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewReader(bodyBytes))
	request.Header.Set("Content-Type", "application/json")
	_, _ = io.ReadAll(request.Body)

	valid, validationErrors := v.ValidateRequestBody(request)
	require.True(t, valid)
	require.Empty(t, validationErrors)

	restoredBody, err := io.ReadAll(request.Body)
	require.NoError(t, err)
	require.JSONEq(t, string(bodyBytes), string(restoredBody))

	replayedBody, err := request.GetBody()
	require.NoError(t, err)
	replayedBytes, err := io.ReadAll(replayedBody)
	require.NoError(t, err)
	require.NoError(t, replayedBody.Close())
	require.JSONEq(t, string(bodyBytes), string(replayedBytes))
}

func TestValidateBody_PrefersAssignedBodyOverStaleGetBody(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, patties, vegetarian]
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	staleBodyBytes, _ := json.Marshal(map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false,
		"vegetarian": true,
	})
	currentBodyBytes, _ := json.Marshal(map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	})

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewReader(staleBodyBytes))
	request.Header.Set("Content-Type", "application/json")
	request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes))

	valid, validationErrors := v.ValidateRequestBody(request)
	require.True(t, valid)
	require.Empty(t, validationErrors)
}

func TestValidateBody_DoesNotUseStaleGetBodyForConsumedDifferentBodySameLength(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [patties]
              properties:
                patties:
                  type: integer`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	staleBodyBytes := []byte(`{"patties":12345}`)
	currentBodyBytes := []byte(`{"patties":false}`)
	require.Len(t, currentBodyBytes, len(staleBodyBytes))

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewReader(staleBodyBytes))
	request.Header.Set("Content-Type", "application/json")
	request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes))
	_, _ = io.ReadAll(request.Body)

	valid, validationErrors := v.ValidateRequestBody(request)
	require.False(t, valid)
	require.Len(t, validationErrors, 1)
	require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message)

	replayedBody, err := request.GetBody()
	require.NoError(t, err)
	replayedBytes, err := io.ReadAll(replayedBody)
	require.NoError(t, err)
	require.NoError(t, replayedBody.Close())
	require.Empty(t, replayedBytes)
}

func TestValidateBody_DoesNotUseStaleGetBodyForExplicitEmptyBody(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, patties, vegetarian]
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	staleBodyBytes, _ := json.Marshal(map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	})

	tests := []struct {
		name string
		body io.ReadCloser
	}{
		{
			name: "http no body",
			body: http.NoBody,
		},
		{
			name: "empty reader",
			body: io.NopCloser(bytes.NewReader(nil)),
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
				bytes.NewReader(staleBodyBytes))
			request.Header.Set("Content-Type", "application/json")
			request.Body = tc.body

			valid, validationErrors := v.ValidateRequestBody(request)
			require.False(t, valid)
			require.Len(t, validationErrors, 1)
			require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message)

			replayedBody, err := request.GetBody()
			require.NoError(t, err)
			replayedBytes, err := io.ReadAll(replayedBody)
			require.NoError(t, err)
			require.NoError(t, replayedBody.Close())
			require.Empty(t, replayedBytes)
		})
	}
}

func TestRequestBodyHelpers_NilRequest(t *testing.T) {
	setRequestBody(nil, []byte(`{"ok":true}`))
	require.Nil(t, readAndResetRequestBody(nil))
}

type requestBodyReaderTestBody struct{}

func (r *requestBodyReaderTestBody) Read(_ []byte) (int, error) {
	return 0, io.EOF
}

func (r *requestBodyReaderTestBody) Close() error {
	return nil
}

type failingReplayableBody struct{}

func (r *failingReplayableBody) Read(_ []byte) (int, error) {
	return 0, io.EOF
}

func (r *failingReplayableBody) Close() error {
	return nil
}

func (r *failingReplayableBody) ReadAt(_ []byte, _ int64) (int, error) {
	return 0, io.ErrUnexpectedEOF
}

func (r *failingReplayableBody) Size() int64 {
	return 1
}

func TestRequestBodyReader_DefensiveBranches(t *testing.T) {
	require.Nil(t, requestBodyReader(nil))
	require.Nil(t, requestBodyReader(http.NoBody))

	var nilBody *requestBodyReaderTestBody
	require.Nil(t, requestBodyReader(nilBody))

	body := &requestBodyReaderTestBody{}
	require.Same(t, body, requestBodyReader(body))
}

func TestRequestBodySnapshot_DefensiveBranches(t *testing.T) {
	snapshot, ok := requestBodySnapshot(nil)
	require.False(t, ok)
	require.Nil(t, snapshot)

	snapshot, ok = requestBodySnapshot(&http.Request{Body: &requestBodyReaderTestBody{}})
	require.False(t, ok)
	require.Nil(t, snapshot)

	snapshot, ok = requestBodySnapshot(&http.Request{Body: io.NopCloser(bytes.NewReader(nil))})
	require.False(t, ok)
	require.Nil(t, snapshot)

	snapshot, ok = requestBodySnapshot(&http.Request{Body: &failingReplayableBody{}})
	require.False(t, ok)
	require.Nil(t, snapshot)
}

func TestValidateBody_ValidBasicSchema_WithFullContentTypeHeader(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json; charset=utf-8; boundary=12345")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_ValidSchemaUsingAllOf(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    Nutrients:
      type: object
      required: [fat, salt, meat]
      properties:
        fat:
          type: number
        salt:
          type: number
        meat:
          type: string
          enum:
            - beef
            - pork
            - lamb
            - vegetables
    TestBody:
      type: object
      allOf:
        - $ref: '#/components/schema_validation/Nutrients'
      properties:
        name:
          type: string
        patties:
          type: integer
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
		"fat":        10.0,
		"salt":       0.5,
		"meat":       "beef",
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_InvalidSchemaUsingAllOf(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    Nutrients:
      type: object
      required: [fat, salt, meat]
      properties:
        fat:
          type: number
        salt:
          type: number
        meat:
          type: string
          enum:
            - beef
            - pork
            - lamb
            - vegetables
    TestBody:
      type: object
      allOf:
        - $ref: '#/components/schema_validation/Nutrients'
      properties:
        name:
          type: string
        patties:
          type: integer
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
		"fat":        10.0,
		"salt":       false,    // invalid
		"meat":       "turkey", // invalid
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
}

func TestValidateBody_ValidSchemaUsingAllOfAnyOf(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    Uncooked:
      type: object
      required: [uncookedWeight, uncookedHeight]
      properties:
        uncookedWeight:
          type: number
        uncookedHeight:
          type: number
    Cooked:
      type: object
      required: [usedOil, usedAnimalFat]
      properties:
        usedOil:
          type: boolean
        usedAnimalFat:
          type: boolean
    Nutrients:
      type: object
      required: [fat, salt, meat]
      properties:
        fat:
          type: number
        salt:
          type: number
        meat:
          type: string
          enum:
            - beef
            - pork
            - lamb
            - vegetables
    TestBody:
      type: object
      oneOf:
        - $ref: '#/components/schema_validation/Uncooked'
        - $ref: '#/components/schema_validation/Cooked'
      allOf:
        - $ref: '#/components/schema_validation/Nutrients'
      properties:
        name:
          type: string
        patties:
          type: integer
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":          "Big Mac",
		"patties":       2,
		"vegetarian":    true,
		"fat":           10.0,
		"salt":          0.5,
		"meat":          "beef",
		"usedOil":       true,
		"usedAnimalFat": false,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_InvalidSchemaUsingOneOf(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    Uncooked:
      type: object
      required: [uncookedWeight, uncookedHeight]
      properties:
        uncookedWeight:
          type: number
        uncookedHeight:
          type: number
    Cooked:
      type: object
      required: [usedOil, usedAnimalFat]
      properties:
        usedOil:
          type: boolean
        usedAnimalFat:
          type: boolean
    Nutrients:
      type: object
      required: [fat, salt, meat]
      properties:
        fat:
          type: number
        salt:
          type: number
        meat:
          type: string
          enum:
            - beef
            - pork
            - lamb
            - vegetables
    TestBody:
      type: object
      oneOf:
        - $ref: '#/components/schema_validation/Uncooked'
        - $ref: '#/components/schema_validation/Cooked'
      allOf:
        - $ref: '#/components/schema_validation/Nutrients'
      properties:
        name:
          type: string
        patties:
          type: integer
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
		"fat":        10.0,
		"salt":       0.5,
		"meat":       "beef",
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
	assert.Equal(t, "missing properties 'uncookedWeight', 'uncookedHeight'", errors[0].SchemaValidationErrors[0].Reason)
	assert.Equal(t, "missing properties 'usedOil', 'usedAnimalFat'", errors[0].SchemaValidationErrors[1].Reason)
}

func TestValidateBody_InvalidSchemaMinMax(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    TestBody:
      type: object
      properties:
        name:
          type: string
        patties:
          type: integer
          maximum: 3
          minimum: 1
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    5,
		"vegetarian": true,
		"fat":        10.0,
		"salt":       0.5,
		"meat":       "beef",
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 1)
	assert.Equal(t, "maximum: got 5, want 3", errors[0].SchemaValidationErrors[0].Reason)
}

func TestValidateBody_InvalidSchemaMaxItems(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    TestBody:
      type: array
      maxItems: 2
      items:
        type: object
        properties:
          name:
            type: string
          patties:
            type: integer
            maximum: 3
            minimum: 1
          vegetarian:
            type: boolean
        required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": true,
		"fat":        10.0,
		"salt":       0.5,
		"meat":       "beef",
	}
	bodyArray := []interface{}{body, body, body, body} // two too many!
	bodyBytes, _ := json.Marshal(bodyArray)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 1)
	assert.Equal(t, "maxItems: got 4, want 2", errors[0].SchemaValidationErrors[0].Reason)
	assert.Equal(t, 2, errors[0].SchemaValidationErrors[0].Line)
	assert.Equal(t, "maxItems: got 4, want 2", errors[0].SchemaValidationErrors[0].Reason)
	assert.Equal(t, 11, errors[0].SchemaValidationErrors[0].Column)
}

func TestValidateBody_SchemaHasNoRequestBody(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		http.NoBody)
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_MediaTypeHasNullSchema(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		http.NoBody)
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_MissingBody(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    TestBody:
      type: array
      maxItems: 2
      items:
        type: object
        properties:
          name:
            type: string
          patties:
            type: integer
            maximum: 3
            minimum: 1
          vegetarian:
            type: boolean
        required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		http.NoBody)
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
}

func TestValidateBody_NoBodyNoNothing(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		http.NoBody)
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_InvalidSchemaMultipleItems(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                required:
                  - name
                properties:
                  name:
                    type: string
                  patties:
                    type: integer
                  vegetarian:
                    type: boolean`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	var items []map[string]interface{}
	items = append(items, map[string]interface{}{
		"patties":    1,
		"vegetarian": true,
	})
	items = append(items, map[string]interface{}{
		"name":       "Quarter Pounder",
		"patties":    true,
		"vegetarian": false,
	})
	items = append(items, map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2,
		"vegetarian": false,
	})

	bodyBytes, _ := json.Marshal(items)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	// double-tap to hit the cache
	_, _ = v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 2)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
}

func TestValidateBody_InvalidSchema_BadDecode(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/TestBody'
components:
  schema_validation:
    TestBody:
      type: object
      properties:
        name:
          type: string
        patties:
          type: integer
          maximum: 3
          minimum: 1
        vegetarian:
          type: boolean
      required: [name, patties, vegetarian]    `

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte("{\"bad\": \"json\",}")))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Nil(t, errors[0].SchemaValidationErrors)
	assert.Contains(t, errors[0].Reason, "cannot be decoded")
}

func TestValidateBody_SchemaNoType_Issue75(t *testing.T) {
	spec := `{
  "openapi": "3.0.1",
  "info": {
    "title": "testing",
    "description": "<p>This is for testing purpose</p>",
    "version": "1.0",
    "x-targetEndpoint": "https://mocktarget.apigee.net/json"
  },
  "servers": [
    {
      "url": "https://some-url.com"
    }
  ],
  "paths": {
    "/path1": {
      "put": {
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1
                      },
                      "age": {
                        "type": "integer"
                      }
                    },
                    "required": [
                      "name"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string"
                      },
                      "address": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "email"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/path2": {
      "get": {
        "parameters": [
          {
            "name": "X-My-Header",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/path3": {
      "get": {
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  }
}
`

	doc, err := libopenapi.NewDocument([]byte(spec))
	if err != nil {
		fmt.Println("error while creating open api spec document", err)
		return
	}

	req, err := http.NewRequest("PUT", "/path1", nil)
	if err != nil {
		fmt.Println("error while creating new HTTP request", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	v3Model, errs := doc.BuildV3Model()
	if errs != nil {
		fmt.Println("error while building a Open API spec V3 model", errs)
		return
	}

	reqBodyValidator := NewRequestBodyValidator(&v3Model.Model)
	isSuccess, valErrs := reqBodyValidator.ValidateRequestBody(req)

	assert.False(t, isSuccess)
	assert.Len(t, valErrs, 1)
	assert.Equal(t, "PUT request body is empty for '/path1'", valErrs[0].Message)
}

// https://github.com/pb33f/wiretap/issues/146
func TestValidateBody_OptionalRequestBody_EmptyBody(t *testing.T) {
	spec := `openapi: 3.0.1
info:
  title: test
  version: "1.0"
paths:
  /test:
    post:
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: OK`

	doc, _ := libopenapi.NewDocument([]byte(spec))
	v3Model, _ := doc.BuildV3Model()

	req, _ := http.NewRequest("POST", "/test", nil)
	req.Header.Set("Content-Type", "application/json")

	reqBodyValidator := NewRequestBodyValidator(&v3Model.Model)
	isSuccess, valErrs := reqBodyValidator.ValidateRequestBody(req)

	assert.True(t, isSuccess)
	assert.Empty(t, valErrs)
}

// https://github.com/pb33f/libopenapi-validator/issues/144
func TestValidateBody_InvalidSchema_EnsureOptionsPassthrough(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schema_validation/V1_UserRequest'
components:
  schema_validation:
    V1_UserRequest:
            type: object
            properties:
                email:
                    type: string
                    format: email
                    minLength: 1
                    maxLength: 320`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithFormatAssertions())

	items := make(map[string]interface{})
	items["email"] = "test"

	bodyBytes, _ := json.Marshal(items)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 1)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 1)
	assert.Equal(t, "'test' is not valid email: missing @", errors[0].SchemaValidationErrors[0].Reason)
}

func TestValidateBody_StrictMode_UndeclaredProperty(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithStrictMode())

	// Include an undeclared property 'extra'
	body := map[string]interface{}{
		"name":    "Big Mac",
		"patties": 2,
		"extra":   "undeclared property",
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Contains(t, errors[0].Message, "extra")
	assert.Contains(t, errors[0].Message, "not declared")
}

func TestValidateBody_StrictMode_ValidRequest(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithStrictMode())

	// Only declared properties
	body := map[string]interface{}{
		"name":    "Big Mac",
		"patties": 2,
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_StrictMode_ReadOnlyProperty(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /users:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: string
                  readOnly: true
                name:
                  type: string`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model,
		config.WithStrictMode(),
		config.WithStrictRejectReadOnly(),
	)

	body := map[string]interface{}{
		"id":   "user-123",
		"name": "John",
	}

	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/users",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Contains(t, errors[0].Message, "readOnly")
	assert.Contains(t, errors[0].Message, "id")
}

func TestValidateRequestBody_XMLMarshalError(t *testing.T) {
	spec := []byte(`
openapi: 3.1.0
info:
  title: Test Spec
  version: 1.0.0
paths:
  /test:
    post:
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: object
              properties:
                bad_number:
                  type: number
      responses:
        '200':
          description: Success
`)

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/test",
		bytes.NewBuffer([]byte("<bad_number>NaN</bad_number>")))
	request.Header.Set("Content-Type", "application/xml")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, errors[0].Message, "xml example is malformed")
}

func TestValidateRequestBody_URLEncodedMarshalError(t *testing.T) {
	spec := []byte(`
openapi: 3.1.0
info:
  title: Test Spec
  version: 1.0.0
paths:
  /test:
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bad_number:
                  type: number
      responses:
        '200':
          description: Success
`)

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithURLEncodedBodyValidation())

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/test",
		bytes.NewBuffer([]byte("bad_number=NaN")))
	request.Header.Set("Content-Type", helpers.URLEncodedContentType)

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, errors[0].Message, "Unable to parse form-urlencoded body")
}

func TestValidateBody_URLEncodedRequest(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                patties:
                  type: integer`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithURLEncodedBodyValidation())

	body := "name=cheeseburger&patties=23"

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte(body)))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	valid, errors := v.ValidateRequestBody(request)
	assert.True(t, valid)
	assert.Len(t, errors, 0)

	body = "name=cheeseburger&patties=23.4"

	request, _ = http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte(body)))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	valid, errors = v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
}

func TestValidateBody_XmlRequest(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/xml:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                patties:
                  type: integer
                  xml:
                    name: cost`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())

	body := "<name>cheeseburger</name><cost>23</cost>"

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte(body)))
	request.Header.Set("Content-Type", "application/xml")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestValidateBody_XmlMalformedRequest(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/xml:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                patties:
                  type: integer
                  xml:
                    name: cost`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())

	body := ""

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte(body)))
	request.Header.Set("Content-Type", "application/xml")

	valid, errors := v.ValidateRequestBody(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)

	err := errors[0]
	assert.Equal(t, helpers.XmlValidation, err.ValidationType)
	assert.Contains(t, err.Reason, "failed to parse xml")
}

func TestValidateBody_XmlRequestTransformations(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/xml:
            schema:
              type: object
              xml:
                name: Burger
              required:
                - name
                - patties
              properties:
                name:
                  type: string
                patties:
                  type: integer
                  xml:
                    name: cost`

	doc, _ := libopenapi.NewDocument([]byte(spec))

	m, _ := doc.BuildV3Model()
	v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())

	body := "<Burger><name>cheeseburger</name><cost>23</cost></Burger>"

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
		bytes.NewBuffer([]byte(body)))
	request.Header.Set("Content-Type", "application/xml")

	valid, errors := v.ValidateRequestBody(request)

	assert.True(t, valid)
	assert.Len(t, errors, 0)
}
