// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
// SPDX-License-Identifier: MIT

package validator

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"sync"
	"testing"
	"unicode"

	"github.com/dlclark/regexp2"
	"github.com/pb33f/libopenapi"
	"github.com/santhosh-tekuri/jsonschema/v6"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	v3 "github.com/pb33f/libopenapi/datamodel/high/v3"

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

func TestNewValidator(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))

	v, _ := NewValidator(doc)
	assert.NotNil(t, v.GetParameterValidator())
	assert.NotNil(t, v.GetResponseBodyValidator())
	assert.NotNil(t, v.GetRequestBodyValidator())
}

func TestNewValidator_concurrent(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
  /burgers/createBurger/{burgerId}:
    post:
      parameters:
        - in: path
          name: burgerId
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

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

	assert.Empty(t, err)

	v, _ := NewValidator(doc)

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

	bodyBytes, _ := json.Marshal(body)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
			bytes.NewBuffer(bodyBytes))
		request.Header.Set("Content-Type", "application/json")

		valid, errors := v.ValidateHttpRequest(request)

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

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

		valid, errors := v.ValidateHttpRequest(request)

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

	wg.Wait()
}

func TestNewValidator_ValidateDocument(t *testing.T) {
	doc, _ := libopenapi.NewDocument(petstoreBytes)
	v, _ := NewValidator(doc)
	valid, errs := v.ValidateDocument()
	assert.True(t, valid)
	assert.Len(t, errs, 0)
}

type dlclarkRegexp regexp2.Regexp

func (re *dlclarkRegexp) MatchString(s string) bool {
	matched, err := (*regexp2.Regexp)(re).MatchString(s)
	return err == nil && matched
}

func (re *dlclarkRegexp) String() string {
	return (*regexp2.Regexp)(re).String()
}

func dlclarkCompile(s string) (jsonschema.Regexp, error) {
	re, err := regexp2.Compile(s, regexp2.ECMAScript)
	if err != nil {
		return nil, err
	}
	return (*dlclarkRegexp)(re), nil
}

func TestNewValidator_WithRegex(t *testing.T) {
	doc, err := libopenapi.NewDocument(petstoreBytes)
	require.Nil(t, err, "Failed to load spec")

	v, errs := NewValidator(doc, config.WithRegexEngine(dlclarkCompile))
	require.Empty(t, errs, "Failed to build validator")
	require.NotNil(t, v, "Failed to build validator")

	valid, valErrs := v.ValidateDocument()
	assert.True(t, valid)
	assert.Empty(t, valErrs)
}

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

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err, "Failed to load spec")
	require.NotNil(t, doc, "Failed to load spec")

	v, errs := NewValidator(
		doc,
		config.WithFormatAssertions(),
		config.WithCustomFormat("capital", func(v any) error {
			s, ok := v.(string)
			if !ok {
				return fmt.Errorf("expected string")
			}

			if s == "" {
				return nil
			}

			r := []rune(s)[0]

			if !unicode.IsUpper(r) {
				return fmt.Errorf("expected first latter to be uppercase")
			}

			return nil
		}),
	)
	require.Empty(t, errs, "Failed to build validator")
	require.NotNil(t, v, "Failed to build validator")

	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.ValidateHttpRequest(request)

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

func TestStrictMode_ValidateHttpRequestIntegration(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /things/{id}:
    post:
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - in: query
          name: q
          schema:
            type: string
        - in: header
          name: X-Known
          schema:
            type: string
        - in: cookie
          name: session
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc, config.WithStrictMode())
	require.Empty(t, errs)

	body := map[string]any{
		"name":  "ok",
		"extra": "nope",
	}
	bodyBytes, _ := json.Marshal(body)

	request, _ := http.NewRequest(http.MethodPost, "https://things.com/things/123?q=ok&extra=1", bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("X-Known", "known")
	request.Header.Set("X-Extra", "nope")
	request.AddCookie(&http.Cookie{Name: "session", Value: "ok"})
	request.AddCookie(&http.Cookie{Name: "other", Value: "nope"})

	valid, valErrs := v.ValidateHttpRequest(request)
	assert.False(t, valid)

	strictSubTypes := make(map[string]bool)
	for _, vErr := range valErrs {
		if vErr.ValidationType == errors.StrictValidationType {
			strictSubTypes[vErr.ValidationSubType] = true
		}
	}

	assert.True(t, strictSubTypes[errors.StrictSubTypeProperty])
	assert.True(t, strictSubTypes[errors.StrictSubTypeHeader])
	assert.True(t, strictSubTypes[errors.StrictSubTypeQuery])
	assert.True(t, strictSubTypes[errors.StrictSubTypeCookie])
}

func TestStrictMode_ValidateHttpResponseHeadersIntegration(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /things/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: ok
          headers:
            X-Res:
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc, config.WithStrictMode())
	require.Empty(t, errs)

	request, _ := http.NewRequest(http.MethodGet, "https://things.com/things/123", http.NoBody)

	body := map[string]any{"ok": true}
	bodyBytes, _ := json.Marshal(body)

	response := &http.Response{
		StatusCode: 200,
		Header: http.Header{
			"Content-Type": {"application/json"},
			"X-Res":        {"ok"},
			"X-Extra":      {"nope"},
		},
		Body: io.NopCloser(bytes.NewBuffer(bodyBytes)),
	}

	valid, valErrs := v.ValidateHttpResponse(request, response)
	assert.False(t, valid)

	foundStrictHeader := false
	for _, vErr := range valErrs {
		if vErr.ValidationType == errors.StrictValidationType &&
			vErr.ValidationSubType == errors.StrictSubTypeHeader {
			foundStrictHeader = true
			break
		}
	}
	assert.True(t, foundStrictHeader)
}

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

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err, "Failed to load spec")
	require.NotNil(t, doc, "Failed to load spec")

	v, errs := NewValidator(
		doc,
		config.WithFormatAssertions(),
		config.WithCustomFormat("capital", func(v any) error {
			s, ok := v.(string)
			if !ok {
				return fmt.Errorf("expected string")
			}

			if s == "" {
				return nil
			}

			r := []rune(s)[0]

			if !unicode.IsUpper(r) {
				return fmt.Errorf("expected first latter to be uppercase")
			}

			return nil
		}),
	)
	require.Empty(t, errs, "Failed to build validator")
	require.NotNil(t, v, "Failed to build validator")

	v.GetRequestBodyValidator()

	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.ValidateHttpRequest(request)

	assert.False(t, valid)
	require.Len(t, errors, 1)
	assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
	require.Len(t, errors[0].SchemaValidationErrors, 1)
	require.NotNil(t, errors[0].SchemaValidationErrors[0])
	assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].KeywordLocation)
	assert.Equal(t, "'big mac' is not valid capital: expected first latter to be uppercase", errors[0].SchemaValidationErrors[0].Reason)
}

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

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err, "Failed to load spec")
	require.NotNil(t, doc, "Failed to load spec")

	v, errs := NewValidator(
		doc,
		config.WithCustomFormat("capital", func(v any) error {
			s, ok := v.(string)
			if !ok {
				return fmt.Errorf("expected string")
			}

			if s == "" {
				return nil
			}

			r := []rune(s)[0]

			if !unicode.IsUpper(r) {
				return fmt.Errorf("expected first latter to be uppercase")
			}

			return nil
		}),
	)
	require.Empty(t, errs, "Failed to build validator")
	require.NotNil(t, v, "Failed to build validator")

	v.GetRequestBodyValidator()

	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.ValidateHttpRequest(request)

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

func TestNewValidator_BadDoc(t *testing.T) {
	spec := `swagger: 2.0`

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

	_, errs := NewValidator(doc)

	assert.Len(t, errs, 1)
}

func TestNewValidator_ValidateHttpRequest_BadPath(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))

	v, _ := NewValidator(doc)

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

	bodyBytes, _ := json.Marshal(body)

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

	valid, errors := v.ValidateHttpRequest(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message)
}

func TestNewValidator_ValidateHttpRequestSync_BadPath(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))

	v, _ := NewValidator(doc)

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

	bodyBytes, _ := json.Marshal(body)

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

	valid, errors := v.ValidateHttpRequestSync(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message)
}

func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(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))

	v, _ := NewValidator(doc)

	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.ValidateHttpRequest(request)

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

func TestNewValidator_ValidateHttpRequestSync_ValidPostSimpleSchema(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))

	v, _ := NewValidator(doc)

	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.ValidateHttpRequestSync(request)

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

func TestNewValidator_ValidateHttpRequestSync_ValidPostSimpleSchema_FoundPath(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))

	v, _ := NewValidator(doc)

	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.ValidateHttpRequestSyncWithPathItem(request, &v3.PathItem{
		Post: &v3.Operation{},
	}, "/burgers/createBurger")

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

func TestNewValidator_slash_server_url(t *testing.T) {
	spec := `openapi: 3.1.0
servers:
  - url: /
paths:
  /burgers/{burgerId}/locate:
    patch:
      operationId: locateBurger
      parameters:
        - name: burgerId
          in: path
          required: true
          schema:
            type: string
            format: uuid
`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/edd0189c-420b-489c-98f2-0facc5a26f3a/locate", nil)
	v, _ := NewValidator(doc)

	valid, errors := v.ValidateHttpRequest(request)

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

func TestNewValidator_ValidateHttpRequest_SetPath_ValidPostSimpleSchema(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))

	v, _ := NewValidator(doc)

	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.ValidateHttpRequest(request)

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

func TestNewValidator_ValidateHttpRequestSync_SetPath_ValidPostSimpleSchema(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))

	v, _ := NewValidator(doc)

	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.ValidateHttpRequestSync(request)

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

func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(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))

	v, _ := NewValidator(doc)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false, // wrong.
		"vegetarian": 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.ValidateHttpRequest(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason)
}

func TestNewValidator_ValidateHttpRequestSync_InvalidPostSchema(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))

	v, _ := NewValidator(doc)

	// mix up the primitives to fire two schema violations.
	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    false, // wrong.
		"vegetarian": 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.ValidateHttpRequestSync(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason)
}

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

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

	v, _ := NewValidator(doc)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2, // wrong.
		"vegetarian": 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.ValidateHttpRequest(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message)
}

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

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

	v, _ := NewValidator(doc)

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    2, // wrong.
		"vegetarian": 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.ValidateHttpRequestSync(request)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message)
}

var petstoreBytes []byte

func init() {
	petstoreBytes, _ = os.ReadFile("test_specs/petstorev3.json")
}

func TestNewValidator_PetStore_MissingContentType(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, "application/not-json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "PUT / 200 operation response content type 'application/not-json' does not exist",
		errors[0].Message)

	assert.Equal(t, "The content type 'application/not-json' of the PUT response received "+
		"has not been defined, it's an unknown type",
		errors[0].Reason)
}

func TestNewValidator_PetStore_PetPost200_Valid_PathPreset(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestNewValidator_PetStore_PetPost200_Valid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestNewValidator_PetStore_PetPost200_Invalid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet, but is missing the photoUrls field
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet",
		bytes.NewBuffer(bodyBytes))
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusProxyAuthRequired) // this is not defined by the contract, so it should fail.
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// we have a schema violation, and a response code violation, our validator should have picked them
	// both up.
	assert.False(t, valid)
	assert.Len(t, errors, 2)

	// check errors
	for i := range errors {
		if errors[i].SchemaValidationErrors != nil {
			assert.Equal(t, "missing property 'photoUrls'", errors[i].SchemaValidationErrors[0].Reason)
		} else {
			assert.Equal(t, "POST operation request response code '407' does not exist", errors[i].Message)
		}
	}
}

func TestNewValidator_PetStore_PetFindByStatusGet200_Valid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/findByStatus?status=sold", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestNewValidator_PetStore_PetFindByStatusGet200_BadEnum(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/findByStatus?status=invalidEnum", nil) // enum is invalid
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with a pet
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.Header().Set("Herbs-And-Spice", helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "Query parameter 'status' does not match allowed values", errors[0].Message)
	assert.Equal(t, "Instead of 'invalidEnum', use one of the allowed values: 'available, pending, sold'", errors[0].HowToFix)
}

func TestNewValidator_PetStore_PetFindByTagsGet200_Valid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy&tags=wuzzy", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// should all be perfectly valid.
	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestNewValidator_PetStore_PetFindByTagsGet200_InvalidExplode(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy,wuzzy", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	// will fail.
	assert.False(t, valid)
	assert.Len(t, errors, 2) // will fire allow reserved error, and explode error.
}

func TestNewValidator_PetStore_PetGet200_Valid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body) // operation returns pet

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/12345", nil)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("api_key", "12345")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

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

func TestNewValidator_PetStore_PetGet200_PathNotFound(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal(body) // operation returns pet

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/IamNotANumber", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	assert.False(t, valid)
	// Note: /pet/{petId} allows api_key OR petstore_auth (OAuth2). Since OAuth2 is not validated,
	// security passes. Only the path parameter validation fails.
	assert.Len(t, errors, 1)
	assert.Equal(t, "Path parameter 'petId' is not a valid integer", errors[0].Message)
}

func TestNewValidator_PetStore_PetGet200(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/112233", nil)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("api_key", "12345")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)

		// create a pet
		body := map[string]interface{}{
			"id":   123,
			"name": "cotton",
			"category": map[string]interface{}{
				"id":   123,
				"name": "dogs",
			},
			"photoUrls": []string{"https://example.com"},
		}

		// marshal the body into bytes.
		bodyBytes, _ := json.Marshal(body) // operation returns pet

		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

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

func TestNewValidator_PetStore_PetGet200_ServerBadMediaType(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/112233", nil)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("api_key", "12345")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, "hot-diggity/coffee; charset=cakes") // wut?
		w.WriteHeader(http.StatusOK)

		// create a pet
		body := map[string]interface{}{
			"id":   123,
			"name": "cotton",

			"category": map[string]interface{}{
				"id":   123,
				"name": "dogs",
			},
			"photoUrls": []string{"https://example.com"},
		}

		// marshal the body into bytes.
		bodyBytes, _ := json.Marshal(body) // operation returns pet

		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "GET / 200 operation response content type 'hot-diggity/coffee' does not exist", errors[0].Message)
}

func TestNewValidator_PetStore_PetWithIdPost200_Missing200(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/pet/112233?name=peter&query=thing", nil)
	request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "POST operation request response code '200' does not exist", errors[0].Message)
}

func TestNewValidator_PetStore_UploadImage200_InvalidRequestBodyType(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil)
	request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		// forget to write an API response
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

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

func TestNewValidator_PetStore_UploadImage200_Valid(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil)
	request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)

		// create an API response
		body := map[string]interface{}{
			"code":    200,
			"type":    "herbs",
			"message": "smoke them every day.",
		}

		// marshal the body into bytes.
		bodyBytes, _ := json.Marshal(body) // operation returns APIResponse

		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

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

func TestNewValidator_PetStore_UploadImage200_InvalidAPIResponse(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil)
	request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)

		// create an API response
		body := map[string]interface{}{
			"code":    200,
			"type":    false,
			"message": "smoke them every day.",
		}

		// marshal the body into bytes.
		bodyBytes, _ := json.Marshal(body) // operation returns APIResponse

		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "200 response body for '/pet/112233/uploadImage' failed to validate schema", errors[0].Message)
}

func TestNewValidator_CareRequest_WrongContentType(t *testing.T) {
	careRequestBytes, _ := os.ReadFile("test_specs/care_request.yaml")
	doc, _ := libopenapi.NewDocument(careRequestBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/requests/d4bc1a0c-c4ee-4be5-9281-26b1a041634d", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response,
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, "application/not-json")
		w.WriteHeader(http.StatusOK)

		// create a CareRequest
		body := map[string]interface{}{
			"id":     "d4bc1a0c-c4ee-4be5-9281-26b1a041634d",
			"status": "active",
		}

		// marshal the body into bytes.
		bodyBytes, _ := json.Marshal(body)

		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Equal(t, "GET / 200 operation response content type 'application/not-json' does not exist",
		errors[0].Message)

	assert.Equal(t, "The content type 'application/not-json' "+
		"of the GET response received has not been defined, it's an unknown type",
		errors[0].Reason)
}

func TestNewValidator_PetStore_InvalidPath_Response(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/missing", nil)
	request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
	}

	// fire the request
	handler(res, request)

	// validate the response
	valid, errors := v.ValidateHttpResponse(request, res.Result())

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

func TestNewValidator_PetStore_InvalidPath_RequestResponse(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a new put request with an invalid path
	request, _ := http.NewRequest(http.MethodPost,
		"https://hyperspace-superherbs.com/nonexistent", nil)
	request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)

	// simulate a request/response
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
	}

	// fire the request
	handler(res, request)

	// validate both request and response - should fail because path not found
	valid, errors := v.ValidateHttpRequestResponse(request, res.Result())

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

func TestNewValidator_PetStore_PetFindByStatusGet200_Valid_responseOnly(t *testing.T) {
	// create a new document from the petstore spec
	doc, _ := libopenapi.NewDocument(petstoreBytes)

	// create a doc
	v, _ := NewValidator(doc)

	// create a pet
	body := map[string]interface{}{
		"id":   123,
		"name": "cotton",
		"category": map[string]interface{}{
			"id":   123,
			"name": "dogs",
		},
		"photoUrls": []string{"https://example.com"},
	}

	// marshal the body into bytes.
	bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets

	// create a new put request
	request, _ := http.NewRequest(http.MethodGet,
		"https://hyperspace-superherbs.com/pet/findByStatus?status=sold", nil)
	request.Header.Set("Content-Type", "application/json")

	// simulate a request/response, in this case the contract returns a 200 with the pet we just created.
	res := httptest.NewRecorder()
	handler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(bodyBytes)
	}

	// fire the request
	handler(res, request)

	// validate the response (should be clean)
	valid, errors := v.ValidateHttpResponse(request, res.Result())

	// should all be perfectly valid.
	assert.True(t, valid)
	assert.Len(t, errors, 0)
}

func TestNewValidator_ValidateHttpResponse_RangeResponseCode(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    patties:
                      type: integer
                    vegetarian:
                      type: boolean
        '4XX':
          description: Bad request
        '5XX':
          description: Server error`

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

	v, _ := NewValidator(doc)

	request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers", nil)
	request.Header.Set("Content-Type", "application/json")
	response := &http.Response{
		StatusCode: 400,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
	}
	valid, errors := v.ValidateHttpResponse(request, response)

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

// https://github.com/pb33f/libopenapi-validator/issues/107
// https://github.com/pb33f/libopenapi-validator/issues/103
func TestNewValidator_TestCircularRefsInValidation_Request(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Panic at response validation
  version: 1.0.0
paths:
  /operations:
    delete:
      description: Delete operations
      responses:
        default:
          description: Any response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    Error:
      type: object
      properties:
        code:
          type: string
        details:
          type: array
          items:
            $ref: '#/components/schemas/Error'`

	document, err := libopenapi.NewDocument([]byte(spec))
	if err != nil {
		panic(fmt.Sprintf("failed to create new document: %v\n", err))
	}

	model, errs := document.BuildV3Model()
	circ := model.Index.GetCircularReferences()
	fmt.Println("Circular references: ", len(circ))

	if errs != nil {
		panic(fmt.Sprintf("failed to create v3 model from document: %e", errs))
	}

	fmt.Println("Successfully parsed OpenAPI spec")

	oapiValidator, merrs := NewValidator(document)
	if merrs != nil {
		panic(fmt.Sprintf("failed to create validator: %v", merrs))
	}
	if ok, merrs := oapiValidator.ValidateDocument(); !ok {
		panic(fmt.Sprintf("document validation errors: %v", merrs))
	}

	req := &http.Request{
		Method: http.MethodDelete,
		URL: &url.URL{
			Path: "/operations",
		},
	}
	res := &http.Response{
		StatusCode: http.StatusOK,
		Header: map[string][]string{
			"Content-Type": {"application/json"},
		},
		Body: io.NopCloser(strings.NewReader(`{"code":"abc","details":[{"code":"def"}]}`)),
	}
	if ok, errs := oapiValidator.ValidateHttpResponse(req, res); !ok {
		assert.Equal(t, 1, len(errs))
		// Error message can vary depending on whether schema was cached during warming or not:
		// - "schema render failure, circular reference" (if caught during validation)
		// - "JSON schema compile failed: json-pointer...not found" (if schema was pre-warmed but has circular refs)
		// Both indicate the same underlying issue - circular reference in the schema
		assert.True(t,
			strings.Contains(errs[0].Reason, "circular reference") ||
				strings.Contains(errs[0].Reason, "json-pointer") ||
				strings.Contains(errs[0].Reason, "not found"),
			"Expected error about circular reference or compilation failure, got: %s", errs[0].Reason)
	}
}

// https://github.com/pb33f/libopenapi-validator/issues/107
// https://github.com/pb33f/libopenapi-validator/issues/103
func TestNewValidator_TestCircularRefsInValidation_Response(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Panic at response validation
  version: 1.0.0
paths:
  /operations:
    delete:
      description: Delete operations
      responses:
        default:
          description: Any response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    Error:
      type: object
      properties:
        code:
          type: string
        details:
          type: array
          items:
            $ref: '#/components/schemas/Error'`

	document, err := libopenapi.NewDocument([]byte(spec))
	if err != nil {
		panic(fmt.Sprintf("failed to create new document: %v\n", err))
	}

	model, errs := document.BuildV3Model()
	circ := model.Index.GetCircularReferences()
	fmt.Println("Circular references: ", len(circ))

	if errs != nil {
		panic(fmt.Sprintf("failed to create v3 model from document: %e", errs))
	}

	fmt.Println("Successfully parsed OpenAPI spec")

	oapiValidator, merrs := NewValidator(document)
	if merrs != nil {
		panic(fmt.Sprintf("failed to create validator: %v", merrs))
	}
	if ok, merrs := oapiValidator.ValidateDocument(); !ok {
		panic(fmt.Sprintf("document validation errors: %v", merrs))
	}

	req := &http.Request{
		Method: http.MethodDelete,
		URL: &url.URL{
			Path: "/operations",
		},
	}
	if ok, errs := oapiValidator.ValidateHttpRequest(req); !ok {
		assert.Equal(t, 1, len(errs))
		assert.Equal(t, "cannot render circular reference: #/components/schemas/Error", errs[0].Reason)

	}
}

// https://github.com/pb33f/libopenapi-validator/issues/86
func TestNewValidator_HaveYourModelAndEatIt(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Panic at response validation
  version: 1.0.0
paths:
  /operations:
    delete:
      description: Delete operations
      responses:
        default:
          description: Any response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    Error:
      type: object
      properties:
        code:
          type: string
        details:
          type: array
          items:
            $ref: '#/components/schemas/Error'`

	document, err := libopenapi.NewDocument([]byte(spec))
	if err != nil {
		panic(fmt.Sprintf("failed to create new document: %v\n", err))
	}

	model, errs := document.BuildV3Model()

	if errs != nil {
		panic(fmt.Sprintf("failed to create v3 model from document: %e", errs))
	}

	v := NewValidatorFromV3Model(&model.Model)

	valid, vErrs := v.ValidateDocument()
	assert.False(t, valid)
	assert.Len(t, vErrs, 1)
	assert.Equal(t, "The document cannot be validated as it is not set", vErrs[0].Reason)

	v.SetDocument(document)

	valid, vErrs = v.ValidateDocument()
	assert.True(t, valid)
	assert.Len(t, vErrs, 0)
}

func TestCacheWarming_PopulatesCache(t *testing.T) {
	spec, err := os.ReadFile("test_specs/petstorev3.json")
	require.NoError(t, err)

	doc, err := libopenapi.NewDocument(spec)
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Nil(t, errs)

	validator := v.(*validator)

	// Check that caches were populated
	// Access cache directly from validator options
	require.NotNil(t, validator.options)
	require.NotNil(t, validator.options.SchemaCache)

	count := 0
	validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool {
		count++
		assert.NotNil(t, value.CompiledSchema, "Cache entry should have compiled schema")
		assert.NotEmpty(t, value.ReferenceSchema, "Cache entry should have pre-converted ReferenceSchema string")
		assert.Equal(t, string(value.RenderedInline), value.ReferenceSchema, "ReferenceSchema should match string conversion of RenderedInline")
		return true
	})
	assert.Greater(t, count, 0, "Schema cache should have entries from request and response bodies")
}

func TestCacheWarming_EdgeCases(t *testing.T) {
	// Test nil document
	warmSchemaCaches(nil, nil)

	// Test empty document
	doc := &v3.Document{}
	warmSchemaCaches(doc, nil)

	// Test document with nil PathItems
	doc = &v3.Document{Paths: &v3.Paths{}}
	warmSchemaCaches(doc, nil)
}

func TestCacheWarming_NilOperations(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /test:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	m, _ := doc.BuildV3Model()

	// Manually set operations to nil to test edge cases
	for pair := m.Model.Paths.PathItems.First(); pair != nil; pair = pair.Next() {
		pathItem := pair.Value()
		// Force GetOperations to return something with nil operation
		pathItem.Post = nil
		pathItem.Put = nil
		pathItem.Delete = nil
		pathItem.Patch = nil
		pathItem.Head = nil
		pathItem.Options = nil
		pathItem.Trace = nil
		pathItem.Query = nil
		pathItem.AdditionalOperations = nil
	}

	// This should not panic even with nil operations
	v := NewValidatorFromV3Model(&m.Model)
	assert.NotNil(t, v)
}

func TestCacheWarming_NilSchema(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /test:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	m, _ := doc.BuildV3Model()

	// Manually set schema to nil to test edge case in warmMediaTypeSchema
	for pathPair := m.Model.Paths.PathItems.First(); pathPair != nil; pathPair = pathPair.Next() {
		pathItem := pathPair.Value()
		if pathItem.Post != nil && pathItem.Post.RequestBody != nil && pathItem.Post.RequestBody.Content != nil {
			for contentPair := pathItem.Post.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() {
				mediaType := contentPair.Value()
				// Set schema to nil to trigger the schema == nil check
				mediaType.Schema = nil
			}
		}
	}

	// This should not panic even with nil schemas
	v := NewValidatorFromV3Model(&m.Model)
	assert.NotNil(t, v)
}

func TestCacheWarming_DefaultResponse(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /test:
    get:
      responses:
        default:
          description: Default response
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Nil(t, errs)

	validator := v.(*validator)

	// Check that response cache was populated with default response schema
	require.NotNil(t, validator.options)
	require.NotNil(t, validator.options.SchemaCache)

	count := 0
	validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool {
		count++
		return true
	})
	assert.Greater(t, count, 0, "Schema cache should have entries from default response")
}

// TestCacheWarming_InvalidSchema tests cache warming gracefully skips invalid schemas
func TestCacheWarming_InvalidSchema(t *testing.T) {
	// This spec intentionally has an invalid schema that will fail to compile
	spec := `openapi: 3.1.0
paths:
  /test:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: invalid-type-that-does-not-exist
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	// Should not panic even with invalid schema
	v, errs := NewValidator(doc)
	require.Nil(t, errs)
	assert.NotNil(t, v)
}

// TestCacheWarming_ParameterWithContent tests cache warming for parameters with content property
func TestCacheWarming_ParameterWithContent(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /test:
    get:
      parameters:
        - name: filter
          in: query
          content:
            application/json:
              schema:
                type: object
                properties:
                  value:
                    type: string
      responses:
        '200':
          description: Success`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Nil(t, errs)

	validator := v.(*validator)

	// Check that parameter cache was populated with content schema
	require.NotNil(t, validator.options)
	require.NotNil(t, validator.options.SchemaCache)

	count := 0
	validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool {
		count++
		return true
	})
	assert.Greater(t, count, 0, "Schema cache should have entries from parameter content property")
}

// TestCacheWarming_PathLevelParameters tests cache warming for path-level parameters
func TestCacheWarming_PathLevelParameters(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /test/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          pattern: "^[0-9]+$"
    get:
      responses:
        '200':
          description: Success`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Nil(t, errs)

	validator := v.(*validator)

	// Check that parameter cache was populated with path-level parameter
	require.NotNil(t, validator.options)
	require.NotNil(t, validator.options.SchemaCache)

	count := 0
	validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool {
		count++
		return true
	})
	assert.Greater(t, count, 0, "Schema cache should have entries from path-level parameters")
}

// TestSortValidationErrors tests that validation errors are sorted deterministically
func TestSortValidationErrors(t *testing.T) {
	// Create errors in random order
	errs := []*errors.ValidationError{
		{ValidationType: helpers.SecurityValidation, Message: "API Key missing"},
		{ValidationType: helpers.ParameterValidation, Message: "Path param invalid"},
		{ValidationType: helpers.RequestValidation, Message: "Body invalid"},
		{ValidationType: helpers.ParameterValidation, Message: "Header missing"},
		{ValidationType: helpers.SecurityValidation, Message: "Auth header missing"},
	}

	sortValidationErrors(errs)

	// Verify sorted by validation type first, then by message
	assert.Equal(t, helpers.ParameterValidation, errs[0].ValidationType)
	assert.Equal(t, "Header missing", errs[0].Message)
	assert.Equal(t, helpers.ParameterValidation, errs[1].ValidationType)
	assert.Equal(t, "Path param invalid", errs[1].Message)
	assert.Equal(t, helpers.RequestValidation, errs[2].ValidationType)
	assert.Equal(t, "Body invalid", errs[2].Message)
	assert.Equal(t, helpers.SecurityValidation, errs[3].ValidationType)
	assert.Equal(t, "API Key missing", errs[3].Message)
	assert.Equal(t, helpers.SecurityValidation, errs[4].ValidationType)
	assert.Equal(t, "Auth header missing", errs[4].Message)
}

// TestSortValidationErrors_Empty tests sorting empty slice
func TestSortValidationErrors_Empty(t *testing.T) {
	errs := []*errors.ValidationError{}
	sortValidationErrors(errs)
	assert.Empty(t, errs)
}

// TestSortValidationErrors_SingleElement tests sorting single element slice
func TestSortValidationErrors_SingleElement(t *testing.T) {
	errs := []*errors.ValidationError{
		{ValidationType: helpers.ParameterValidation, Message: "Invalid value"},
	}
	sortValidationErrors(errs)
	assert.Len(t, errs, 1)
	assert.Equal(t, helpers.ParameterValidation, errs[0].ValidationType)
}

func TestHEAD_ExplicitOperation_ResponseValidation(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /resource:
    head:
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
`
	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Empty(t, errs)

	// create a HEAD request
	request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil)

	// simulate a server response that includes a JSON body matching the schema
	bodyBytes, _ := json.Marshal(map[string]bool{"ok": true})
	response := &http.Response{
		StatusCode: 200,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(bytes.NewBuffer(bodyBytes)),
	}

	valid, valErrs := v.ValidateHttpResponse(request, response)
	assert.False(t, valid)
	assert.Len(t, valErrs, 1)

	// Also validate a response without a body (common for HEAD)
	responseNoBody := &http.Response{
		StatusCode: 200,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       http.NoBody,
	}

	validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody)
	assert.True(t, validNoBody)
	assert.Len(t, valErrsNoBody, 0)
}

func TestHEAD_ImplicitViaGET_ResponseValidation(t *testing.T) {
	// This spec defines only GET for /resource. Ensure a HEAD request that returns the same body
	// as GET will still validate against the documented GET response.
	spec := `openapi: 3.1.0
paths:
  /resource:
    get:
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
`
	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Empty(t, errs)

	// create a HEAD request (no explicit HEAD operation in the spec)
	request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil)

	// simulate a server response that includes a JSON body like the GET response would.
	bodyBytes, _ := json.Marshal(map[string]bool{"ok": true})
	response := &http.Response{
		StatusCode: 200,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(bytes.NewBuffer(bodyBytes)),
	}

	valid, valErrs := v.ValidateHttpResponse(request, response)
	// Expect validation to succeed when HEAD responses are validated against GET response definitions.
	assert.False(t, valid)
	assert.Len(t, valErrs, 1)

	responseNoBody := &http.Response{
		StatusCode: 200,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       http.NoBody,
	}

	validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody)
	// Expect validation to succeed when HEAD responses are validated against GET response definitions.
	assert.True(t, validNoBody)
	assert.Len(t, valErrsNoBody, 0)
}

func TestHEAD_BothGETAndHEAD_SameSchema_Valid(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /resource:
    get:
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
    head:
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
`
	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Empty(t, errs)

	// HEAD request to /resource
	request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil)

	// server returns a JSON body that matches the schema
	bodyBytes, _ := json.Marshal(map[string]bool{"ok": true})
	response := &http.Response{
		StatusCode: 200,
		Header:     http.Header{helpers.ContentTypeHeader: []string{"application/json"}},
		Body:       io.NopCloser(bytes.NewBuffer(bodyBytes)),
	}

	valid, valErrs := v.ValidateHttpResponse(request, response)
	assert.False(t, valid)
	assert.Len(t, valErrs, 1)

	responseNoBody := &http.Response{
		StatusCode: 200,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       http.NoBody,
	}

	validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody)
	// Expect validation to succeed when HEAD responses are validated against GET response definitions.
	assert.True(t, validNoBody)
	assert.Len(t, valErrsNoBody, 0)
}

func TestHEAD_BothGETAndHEAD_DifferentSchemas_HeadPreferred(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /resource:
    get:
      responses:
        "200":
          description: get schema
          content:
            application/json:
              schema:
                type: object
                required: ["g"]
                properties:
                  g:
                    type: string
    head:
      responses:
        "200":
          description: head schema
          headers:
            content-length:
              description: size of the file
              schema:
                type: integer
`
	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc)
	require.Empty(t, errs)

	// Case A: response matches HEAD schema has content-length header but response contains content-type
	reqA, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil)
	respA := &http.Response{
		StatusCode: 200,
		Header:     http.Header{helpers.ContentTypeHeader: []string{"application/json"}},
		Body:       http.NoBody,
	}
	// No support for response headers validation.
	// Only validates response content-type: json
	validA, errsA := v.ValidateHttpResponse(reqA, respA)
	assert.True(t, validA)
	assert.Len(t, errsA, 0)

	// Case B: response matches GET schema (has "g") but not HEAD (missing required "h") -> should fail
	reqB, _ := http.NewRequest(http.MethodGet, "https://example.com/resource", nil)
	bodyB, _ := json.Marshal(map[string]string{"g": "get-value"})
	respB := &http.Response{
		StatusCode: 200,
		Header:     http.Header{helpers.ContentTypeHeader: []string{"application/json"}},
		Body:       io.NopCloser(bytes.NewBuffer(bodyB)),
	}
	validB, errsB := v.ValidateHttpResponse(reqB, respB)
	assert.True(t, validB)
	assert.Len(t, errsB, 0)
}

// TestValidateHttpResponse_DiscriminatorOneOf tests that response validation works correctly
// when the response schema uses oneOf with a discriminator and $ref to component schemas.
// This is a regression test for https://github.com/pb33f/libopenapi-validator/issues/247
// where the validator would fail with "json-pointer not found" because discriminator refs
// were preserved (Bundle mode) instead of fully inlined (Validation mode).
func TestValidateHttpResponse_DiscriminatorOneOf(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Discriminator OneOf Response Test
  version: 1.0.0
paths:
  /fields:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Field'
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Field'
      responses:
        '201':
          description: Created
components:
  schemas:
    BooleanField:
      type: object
      required:
        - field_type
        - default_value
      properties:
        field_type:
          type: string
          const: boolean
        default_value:
          type: boolean
    StringField:
      type: object
      required:
        - field_type
        - max_length
      properties:
        field_type:
          type: string
          const: string
        max_length:
          type: integer
          minimum: 1
    Field:
      oneOf:
        - $ref: '#/components/schemas/BooleanField'
        - $ref: '#/components/schemas/StringField'
      discriminator:
        propertyName: field_type`

	// Subtest with cache enabled (exercises cache-warming paths in validator.go)
	t.Run("cached", func(t *testing.T) {
		doc, err := libopenapi.NewDocument([]byte(spec))
		require.NoError(t, err)

		v, errs := NewValidator(doc)
		require.Empty(t, errs)

		body, _ := json.Marshal(map[string]interface{}{
			"field_type":    "boolean",
			"default_value": true,
		})
		request, _ := http.NewRequest(http.MethodGet, "https://example.com/fields", nil)
		response := &http.Response{
			StatusCode: 200,
			Header:     http.Header{"Content-Type": []string{"application/json"}},
			Body:       io.NopCloser(bytes.NewBuffer(body)),
		}

		valid, validationErrors := v.ValidateHttpResponse(request, response)
		assert.True(t, valid, "response validation should pass for valid discriminator oneOf payload")
		assert.Empty(t, validationErrors, "no validation errors expected")
	})

	// Subtest with cache disabled (exercises uncached compile paths in
	// validate_response.go, validate_request.go)
	t.Run("uncached", func(t *testing.T) {
		doc, err := libopenapi.NewDocument([]byte(spec))
		require.NoError(t, err)

		v, errs := NewValidator(doc, config.WithSchemaCache(nil))
		require.Empty(t, errs)

		// Response validation (validate_response.go cache-miss path)
		body, _ := json.Marshal(map[string]interface{}{
			"field_type": "string",
			"max_length": 255,
		})
		request, _ := http.NewRequest(http.MethodGet, "https://example.com/fields", nil)
		response := &http.Response{
			StatusCode: 200,
			Header:     http.Header{"Content-Type": []string{"application/json"}},
			Body:       io.NopCloser(bytes.NewBuffer(body)),
		}

		valid, validationErrors := v.ValidateHttpResponse(request, response)
		assert.True(t, valid, "uncached response validation should pass")
		assert.Empty(t, validationErrors, "no validation errors expected (uncached response)")

		// Request validation (validate_request.go cache-miss path)
		reqBody, _ := json.Marshal(map[string]interface{}{
			"field_type":    "boolean",
			"default_value": false,
		})
		postReq, _ := http.NewRequest(http.MethodPost, "https://example.com/fields",
			bytes.NewBuffer(reqBody))
		postReq.Header.Set("Content-Type", "application/json")

		validReq, reqErrors := v.ValidateHttpRequest(postReq)
		assert.True(t, validReq, "uncached request validation should pass")
		assert.Empty(t, reqErrors, "no validation errors expected (uncached request)")
	})
}

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

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc, config.WithStrictMode(), config.WithStrictRejectReadOnly())
	require.Empty(t, errs)

	body := map[string]any{
		"id":   "user-123",
		"name": "John",
	}
	bodyBytes, _ := json.Marshal(body)

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

	valid, valErrs := v.ValidateHttpRequest(request)
	assert.False(t, valid)

	foundReadOnly := false
	for _, vErr := range valErrs {
		if vErr.ValidationType == errors.StrictValidationType &&
			vErr.ValidationSubType == errors.StrictSubTypeReadOnlyProperty {
			foundReadOnly = true
			break
		}
	}
	assert.True(t, foundReadOnly, "should report readOnly violation")
}

func TestStrictMode_RejectWriteOnly_ResponseIntegration(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /users/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  password:
                    type: string
                    writeOnly: true`

	doc, err := libopenapi.NewDocument([]byte(spec))
	require.NoError(t, err)

	v, errs := NewValidator(doc, config.WithStrictMode(), config.WithStrictRejectWriteOnly())
	require.Empty(t, errs)

	request, _ := http.NewRequest(http.MethodGet, "https://example.com/users/123", http.NoBody)

	body := map[string]any{
		"name":     "John",
		"password": "secret",
	}
	bodyBytes, _ := json.Marshal(body)

	response := &http.Response{
		StatusCode: 200,
		Header: http.Header{
			"Content-Type": {"application/json"},
		},
		Body: io.NopCloser(bytes.NewBuffer(bodyBytes)),
	}

	valid, valErrs := v.ValidateHttpResponse(request, response)
	assert.False(t, valid)

	foundWriteOnly := false
	for _, vErr := range valErrs {
		if vErr.ValidationType == errors.StrictValidationType &&
			vErr.ValidationSubType == errors.StrictSubTypeWriteOnlyProperty {
			foundWriteOnly = true
			break
		}
	}
	assert.True(t, foundWriteOnly, "should report writeOnly violation")
}
