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

package schema_validation

import (
	"encoding/json"
	"testing"

	"github.com/pb33f/libopenapi"
	"github.com/stretchr/testify/assert"
	"go.yaml.in/yaml/v4"
)

func TestLocateSchemaPropertyNodeByJSONPath(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`

	var node yaml.Node
	_ = yaml.Unmarshal([]byte(spec), &node)

	foundNode := LocateSchemaPropertyNodeByJSONPath(node.Content[0],
		"/paths/~1burgers~1createBurger/post/requestBody/content/application~1json/schema/properties/vegetarian")

	assert.Equal(t, "boolean", foundNode.Content[1].Value)

	foundNode = LocateSchemaPropertyNodeByJSONPath(node.Content[0],
		"/i/do/not/exist")

	assert.Nil(t, foundNode)
}

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

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

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes))

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

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

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

	// create a schema validator
	v := NewSchemaValidator()

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

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

	body := map[string]interface{}{
		"name":       "Big Mac",
		"patties":    "I am not a number", // will fail
		"vegetarian": 23,                  // will fail
	}

	// create a schema validator
	v := NewSchemaValidator()

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

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

	body := struct{ name string }{"hello world"}

	// create a schema validator
	v := NewSchemaValidator()

	// bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors := v.ValidateSchemaObject(sch.Schema(), body)

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Len(t, errors[0].SchemaValidationErrors, 1)
	assert.Equal(t, "invalid jsonType struct { name string }", errors[0].SchemaValidationErrors[0].Reason)
}

func TestValidateSchema_ReffyComplex_Valid(t *testing.T) {
	spec := `openapi: 3.1.0
components:
  schemas:
    Death:
      type: object
      required: [cakeOrDeath]
      properties:
        cakeOrDeath:
          type: string
          enum: [death]
    Cake:
      type: object
      required: [cakeOrDeath]
      properties:
        cakeOrDeath:
          type: string
          enum: [cake please]
    Four:
      type: object
      oneOf:
        - $ref: '#/components/schemas/Cake'
        - $ref: '#/components/schemas/Death'
    Three:
      type: object
      properties:
        name:
          type: string
        four:
          $ref: '#/components/schemas/Four'
    Two:
      type: object
      properties:
        name:
          type: string
        three:
          $ref: '#/components/schemas/Three'
    One:
      type: object
      properties:
        name:
          type: string
        two:
          $ref: '#/components/schemas/Two'
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/One'`

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

	m, _ := doc.BuildV3Model()

	cakePlease := map[string]interface{}{
		"two": map[string]interface{}{
			"three": map[string]interface{}{
				"four": map[string]interface{}{
					"cakeOrDeath": "cake please",
				},
			},
		},
	}

	death := map[string]interface{}{
		"two": map[string]interface{}{
			"three": map[string]interface{}{
				"four": map[string]interface{}{
					"cakeOrDeath": "death",
				},
			},
		},
	}

	// cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0)
	bodyBytes, _ := json.Marshal(cakePlease)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

	// or death!
	bodyBytes, _ = json.Marshal(death)
	sch = m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

func TestValidateSchema_ReffyComplex_Invalid(t *testing.T) {
	spec := `openapi: 3.1.0
components:
  schemas:
    Death:
      type: object
      required: [cakeOrDeath]
      properties:
        cakeOrDeath:
          type: string
          enum: [death]
    Cake:
      type: object
      required: [cakeOrDeath]
      properties:
        cakeOrDeath:
          type: string
          enum: [cake please]
    Four:
      type: object
      oneOf:
        - $ref: '#/components/schemas/Cake'
        - $ref: '#/components/schemas/Death'
    Three:
      type: object
      properties:
        name:
          type: string
        four:
          $ref: '#/components/schemas/Four'
    Two:
      type: object
      properties:
        name:
          type: string
        three:
          $ref: '#/components/schemas/Three'
    One:
      type: object
      properties:
        name:
          type: string
        two:
          $ref: '#/components/schemas/Two'
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/One'`

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

	m, _ := doc.BuildV3Model()

	cakePlease := map[string]interface{}{
		"two": map[string]interface{}{
			"three": map[string]interface{}{
				"four": map[string]interface{}{
					"cakeOrDeath": "no more cake? so the choice is 'or death?'",
				},
			},
		},
	}

	death := map[string]interface{}{
		"two": map[string]interface{}{
			"three": map[string]interface{}{
				"four": map[string]interface{}{
					"cakeOrDeath": "i'll have the chicken",
				},
			},
		},
	}

	// cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0)
	bodyBytes, _ := json.Marshal(cakePlease)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

	valid, errors = v.ValidateSchemaObject(sch.Schema(), cakePlease)

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

	// or death!
	bodyBytes, _ = json.Marshal(death)
	sch = m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

	valid, errors = v.ValidateSchemaObject(sch.Schema(), death)

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

func TestValidateSchema_EmptySchema(t *testing.T) {
	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaObject(nil, nil)

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

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

	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)

	// create a schema validator
	v := NewSchemaValidator()

	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// validate!
	valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes)

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

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

	bodyBytes := []byte("{\"bad\": \"json\",}")
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes))

	assert.False(t, valid)
	assert.Len(t, errors, 1)
	assert.Contains(t, errors[0].Reason, "invalid character '}' looking for beginning of object key string")
	assert.Nil(t, errors[0].SchemaValidationErrors)
}

func TestValidateSchema_CompilationFailure(t *testing.T) {
	// Test that schema compilation failure doesn't create SchemaValidationErrors
	// This uses an extremely complex regex that might fail compilation in some regex engines
	spec := `openapi: 3.1.0
paths:
  /test:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
                  pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}(?:(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,})*'`

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

	sch := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator with strict regex engine that might fail on complex patterns
	v := NewSchemaValidator()

	// Try to validate - if compilation fails, we should get an error without SchemaValidationErrors
	validData := `{"password": "ValidPass123!"}`
	valid, errors := v.ValidateSchemaString(sch.Schema(), validData)

	// This test is environment-dependent - compilation might succeed or fail depending on regex engine
	// If it fails, we want to ensure SchemaValidationErrors is nil
	if !valid && len(errors) > 0 {
		for _, err := range errors {
			if err.Message == "schema compilation failed" {
				// Compilation failure should NOT have SchemaValidationErrors
				assert.Nil(t, err.SchemaValidationErrors, "Schema compilation errors should not have SchemaValidationErrors")
				t.Logf("Schema compilation failed as expected: %s", err.Reason)
			}
		}
	} else {
		t.Skip("Regex engine handled the complex pattern - skipping compilation failure test")
	}
}

//// https://github.com/pb33f/libopenapi-validator/issues/26
//func TestValidateSchema_v3_0_BooleanExclusiveMinimum(t *testing.T) {
//
//	spec := `openapi: 3.0.0
//paths:
//  /burgers/createBurger:
//    post:
//      requestBody:
//        content:
//          application/json:
//            schema:
//              type: object
//              properties:
//                amount:
//                  type: number
//                  minimum: 0
//                  exclusiveMinimum: true`
//
//	doc, _ := libopenapi.NewDocument([]byte(spec))
//
//	m, _ := doc.BuildV3Model()
//
//	body := map[string]interface{}{"amount": 3}
//
//	bodyBytes, _ := json.Marshal(body)
//	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger"].Post.RequestBody.Content.GetOrZero("application/json"].Schema
//
//	// create a schema validator
//	v := NewSchemaValidator()
//
//	// validate!
//	valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes))
//
//	assert.True(t, valid)
//	assert.Empty(t, errors)
//
//}

// https://github.com/pb33f/libopenapi-validator/issues/26
func TestValidateSchema_v3_0_NumericExclusiveMinimum(t *testing.T) {
	spec := `openapi: 3.0.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount:
                  type: number
                  exclusiveMinimum: 0`

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

	m, _ := doc.BuildV3Model()

	body := map[string]interface{}{"amount": 3}

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes))

	assert.False(t, valid)
	assert.NotEmpty(t, errors)
}

// https://github.com/pb33f/libopenapi-validator/issues/26
func TestValidateSchema_v3_1_NumericExclusiveMinimum(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount:
                  type: number
                  exclusiveMinimum: 0`

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

	m, _ := doc.BuildV3Model()

	body := map[string]interface{}{"amount": 3}

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes))

	assert.True(t, valid)
	assert.Empty(t, errors)
}

func TestValidateSchema_NullableEnum(t *testing.T) {
	spec := `openapi: 3.0.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  enum: [mcbird, mcbeef, veggie, null]
                  nullable: true
                patties:
                  type: integer
                vegetarian:
                  type: boolean`

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

	m, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

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

	bodyBytes, _ := json.Marshal(body)
	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate with OpenAPI 3.0 version - should pass with nullable support!
	valid, errors := v.ValidateSchemaStringWithVersion(sch.Schema(), string(bodyBytes), 3.0)

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

	// validate with default behavior (3.1+) - should fail due to nullable keyword
	valid, errors = v.ValidateSchemaString(sch.Schema(), string(bodyBytes))
	assert.False(t, valid)
	assert.NotEmpty(t, errors)
}

// https://github.com/pb33f/libopenapi/issues/415
func TestValidateSchema_v3_1_DependentSchemas(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                fishCake:
                  type: object
                  properties:
                    bones:
                      type: boolean
                  dependentSchemas:
                    fishCake:
                      type: object
                      properties:
                        cream:
                          type: number
                          format: double
                      required:
                        - cream`

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

	m, _ := doc.BuildV3Model()

	exp := `{
  "fishCake": {
    "bones": true,
    "cream": 2.5
  }
}`

	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), exp)

	assert.True(t, valid)
	assert.Empty(t, errors)
}

// https://github.com/pb33f/libopenapi/issues/415
func TestValidateSchema_v3_1_DependentSchemas_NotValid(t *testing.T) {
	spec := `openapi: 3.1.0
paths:
  /burgers/createBurger:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                fishCake:
                  type: object
                  properties:
                    bones:
                      type: boolean
                  dependentSchemas:
                    fishCake:
                      type: object
                      properties:
                        cream:
                          type: number
                          format: double
                      required:
                        - cream`

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

	m, _ := doc.BuildV3Model()

	exp := `{
  "fishCake": {
    "bones": true,
  }
  "cream": 2.5
}`

	sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema

	// create a schema validator
	v := NewSchemaValidator()

	// validate!
	valid, errors := v.ValidateSchemaString(sch.Schema(), exp)

	assert.False(t, valid)
	assert.NotEmpty(t, errors)
}

// Additional comprehensive tests for version-aware validation

func TestSchemaValidator_ValidateSchemaStringWithVersion_NilSchema(t *testing.T) {
	v := NewSchemaValidator()

	valid, errors := v.ValidateSchemaStringWithVersion(nil, `{"test": "value"}`, 3.0)

	assert.False(t, valid, "Should fail with nil schema")
	assert.Empty(t, errors, "Should not return errors for nil schema")
}

func TestSchemaValidator_ValidateSchemaObjectWithVersion_NilSchema(t *testing.T) {
	v := NewSchemaValidator()

	testObj := map[string]interface{}{
		"test": "value",
	}

	valid, errors := v.ValidateSchemaObjectWithVersion(nil, testObj, 3.0)

	assert.False(t, valid, "Should fail with nil schema")
	assert.Empty(t, errors, "Should not return errors for nil schema")
}

func TestSchemaValidator_ValidateSchemaBytesWithVersion_NilSchema(t *testing.T) {
	v := NewSchemaValidator()

	payload := []byte(`{"test": "value"}`)

	valid, errors := v.ValidateSchemaBytesWithVersion(nil, payload, 3.0)

	assert.False(t, valid, "Should fail with nil schema")
	assert.Empty(t, errors, "Should not return errors for nil schema")
}

// https://github.com/daveshanley/vacuum/issues/520
func TestValidateSchema_OneOf_MultipleMatches_Issue520(t *testing.T) {
	// This test reproduces the issue from vacuum #520
	// The example matches BOTH oneOf alternatives which should fail validation
	// but the error details are not being populated correctly

	spec := `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      type: object
      oneOf:
        - properties:
            pim:
              type: string
        - properties:
            pam:
              type: string
`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema()

	testData := map[string]interface{}{
		"pam": "nop",
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData)

	assert.False(t, valid, "validation should fail because example matches both oneOf alternatives")
	assert.NotEmpty(t, validationErrors, "validation errors should be present")

	if len(validationErrors) > 0 {

		assert.NotEmpty(t, validationErrors[0].SchemaValidationErrors,
			"SchemaValidationErrors should contain detailed error information about the oneOf violation")

		if len(validationErrors[0].SchemaValidationErrors) > 0 {
			firstError := validationErrors[0].SchemaValidationErrors[0]

			assert.Contains(t, firstError.Reason, "oneOf",
				"error should mention oneOf constraint violation")
		}
	}
}

// https://github.com/daveshanley/vacuum/issues/520
func TestValidateSchema_OneOf_Discriminant_Valid(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      type: object
      oneOf:
        - properties:
            type:
              const: pim
            pim:
              type: string
          required: [type, pim]
        - properties:
            type:
              const: pam
            pam:
              type: string
          required: [type, pam]`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema()

	testData := map[string]interface{}{
		"type": "pam",
		"pam":  "nop",
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData)

	assert.True(t, valid, "validation should pass for discriminant oneOf")
	assert.Empty(t, validationErrors, "no validation errors should be present")
}

// https://github.com/daveshanley/vacuum/issues/520
func TestValidateSchema_OneOf_NoMatches(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      type: object
      oneOf:
        - properties:
            foo:
              type: string
          required: [foo]
        - properties:
            bar:
              type: integer
          required: [bar]`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema()

	testData := map[string]interface{}{
		"baz": "invalid",
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData)

	assert.False(t, valid, "validation should fail because example matches no oneOf alternatives")
	assert.NotEmpty(t, validationErrors, "validation errors should be present")

	if len(validationErrors) > 0 {
		assert.NotEmpty(t, validationErrors[0].SchemaValidationErrors,
			"SchemaValidationErrors should contain detailed error information")
	}
}

// https://github.com/daveshanley/vacuum/issues/520
func TestValidateSchema_OneOf_SimpleTypes(t *testing.T) {
	testCases := []struct {
		name        string
		spec        string
		value       interface{}
		shouldPass  bool
		errorDetail string
	}{
		{
			name: "valid string",
			spec: `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      oneOf:
        - type: string
        - type: integer`,
			value:      "hello",
			shouldPass: true,
		},
		{
			name: "valid integer",
			spec: `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      oneOf:
        - type: string
        - type: integer`,
			value:      42,
			shouldPass: true,
		},
		{
			name: "invalid - matches both (ambiguous pattern)",
			spec: `openapi: 3.1.0
info:
  title: Test
  version: 1.0.0
components:
  schemas:
    Test:
      oneOf:
        - type: string
        - type: string
          pattern: '^[0-9]+$'`,
			value:       "123",
			shouldPass:  false,
			errorDetail: "oneOf",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			doc, err := libopenapi.NewDocument([]byte(tc.spec))
			assert.NoError(t, err)

			model, errs := doc.BuildV3Model()
			assert.Empty(t, errs)

			testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema()

			validator := NewSchemaValidator()
			valid, validationErrors := validator.ValidateSchemaObject(testSchema, tc.value)

			if tc.shouldPass {
				assert.True(t, valid, "validation should pass")
				assert.Empty(t, validationErrors, "no validation errors expected")
			} else {
				assert.False(t, valid, "validation should fail")
				assert.NotEmpty(t, validationErrors, "validation errors should be present")

				if len(validationErrors) > 0 && tc.errorDetail != "" {
					if len(validationErrors[0].SchemaValidationErrors) > 0 {
						firstError := validationErrors[0].SchemaValidationErrors[0]
						assert.Contains(t, firstError.Reason, tc.errorDetail,
							"error should contain expected detail")
					}
				}
			}
		})
	}
}

// TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788 tests that validation works correctly
// when a schema has discriminator + oneOf with $ref to component schemas.
// This was a regression in vacuum v0.21.2+ where the validator would fail with
// "JSON schema compile failed: json-pointer not found" because discriminator refs
// were being preserved instead of inlined.
// https://github.com/daveshanley/vacuum/issues/788
func TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Test Discriminator OneOf With Refs
  version: 1.0.0
components:
  schemas:
    ProductWidget:
      type: object
      required:
        - productName
        - quantity
        - color
      properties:
        productName:
          type: string
          enum:
            - Widget
        quantity:
          type: integer
          minimum: 1
        color:
          type: string
          enum:
            - Red
            - Blue
            - Green
    ProductGadget:
      type: object
      required:
        - productName
        - quantity
        - size
      properties:
        productName:
          type: string
          enum:
            - Gadget
        quantity:
          type: integer
          minimum: 1
        size:
          type: string
          enum:
            - Small
            - Medium
            - Large
    Product:
      oneOf:
        - $ref: '#/components/schemas/ProductWidget'
        - $ref: '#/components/schemas/ProductGadget'
      discriminator:
        propertyName: productName`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema()

	// Valid Widget product
	validWidget := map[string]interface{}{
		"productName": "Widget",
		"quantity":    1,
		"color":       "Green",
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(productSchema, validWidget)

	// This should pass without "json-pointer not found" error
	assert.True(t, valid, "validation should pass for valid product with discriminator oneOf refs")
	assert.Empty(t, validationErrors, "no validation errors should be present")
}

// TestValidateSchema_Discriminator_AnyOf_WithRefs tests anyOf with discriminator and $refs
func TestValidateSchema_Discriminator_AnyOf_WithRefs(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Test Discriminator AnyOf With Refs
  version: 1.0.0
components:
  schemas:
    Cat:
      type: object
      required:
        - petType
        - meow
      properties:
        petType:
          type: string
          const: cat
        meow:
          type: boolean
    Dog:
      type: object
      required:
        - petType
        - bark
      properties:
        petType:
          type: string
          const: dog
        bark:
          type: boolean
    Pet:
      anyOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
      discriminator:
        propertyName: petType`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	petSchema := model.Model.Components.Schemas.GetOrZero("Pet").Schema()

	// Valid cat
	validCat := map[string]interface{}{
		"petType": "cat",
		"meow":    true,
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(petSchema, validCat)

	assert.True(t, valid, "validation should pass for valid cat with discriminator anyOf refs")
	assert.Empty(t, validationErrors, "no validation errors should be present")
}

// TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData tests that invalid data
// still fails validation correctly (not a false negative)
func TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData(t *testing.T) {
	spec := `openapi: 3.1.0
info:
  title: Test Discriminator OneOf Invalid
  version: 1.0.0
components:
  schemas:
    ProductWidget:
      type: object
      required:
        - productName
        - quantity
        - color
      properties:
        productName:
          type: string
          enum:
            - Widget
        quantity:
          type: integer
          minimum: 1
        color:
          type: string
          enum:
            - Red
            - Blue
    ProductGadget:
      type: object
      required:
        - productName
        - quantity
        - size
      properties:
        productName:
          type: string
          enum:
            - Gadget
        quantity:
          type: integer
          minimum: 1
        size:
          type: string
    Product:
      oneOf:
        - $ref: '#/components/schemas/ProductWidget'
        - $ref: '#/components/schemas/ProductGadget'
      discriminator:
        propertyName: productName`

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

	model, errs := doc.BuildV3Model()
	assert.Empty(t, errs)

	productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema()

	// Invalid product - missing required field 'color' for Widget
	invalidProduct := map[string]interface{}{
		"productName": "Widget",
		"quantity":    1,
		// missing required 'color' field
	}

	validator := NewSchemaValidator()
	valid, validationErrors := validator.ValidateSchemaObject(productSchema, invalidProduct)

	// This should fail because 'color' is required for Widget
	assert.False(t, valid, "validation should fail for invalid product")
	assert.NotEmpty(t, validationErrors, "validation errors should be present")
}
