// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package spec

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"reflect"
	"testing"

	"github.com/google/go-cmp/cmp"
	fuzz "github.com/google/gofuzz"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/kube-openapi/pkg/internal"
	jsontesting "k8s.io/kube-openapi/pkg/util/jsontesting"
)

var spec = Swagger{
	SwaggerProps: SwaggerProps{
		ID:          "http://localhost:3849/api-docs",
		Swagger:     "2.0",
		Consumes:    []string{"application/json", "application/x-yaml"},
		Produces:    []string{"application/json"},
		Schemes:     []string{"http", "https"},
		Info:        &info,
		Host:        "some.api.out.there",
		BasePath:    "/",
		Paths:       &paths,
		Definitions: map[string]Schema{"Category": {SchemaProps: SchemaProps{Type: []string{"string"}}}},
		Parameters: map[string]Parameter{
			"categoryParam": {ParamProps: ParamProps{Name: "category", In: "query"}, SimpleSchema: SimpleSchema{Type: "string"}},
		},
		Responses: map[string]Response{
			"EmptyAnswer": {
				ResponseProps: ResponseProps{
					Description: "no data to return for this operation",
				},
			},
		},
		SecurityDefinitions: map[string]*SecurityScheme{
			"internalApiKey": &(SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{Type: "apiKey", Name: "api_key", In: "header"}}),
		},
		Security: []map[string][]string{
			{"internalApiKey": {}},
		},
		Tags:         []Tag{{TagProps: TagProps{Description: "", Name: "pets", ExternalDocs: nil}}},
		ExternalDocs: &ExternalDocumentation{Description: "the name", URL: "the url"},
	},
	VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{
		"x-some-extension": "vendor",
		"x-schemes":        []interface{}{"unix", "amqp"},
	}},
}

const specJSON = `{
	"id": "http://localhost:3849/api-docs",
	"consumes": ["application/json", "application/x-yaml"],
	"produces": ["application/json"],
	"schemes": ["http", "https"],
	"swagger": "2.0",
	"info": {
		"contact": {
			"name": "wordnik api team",
			"url": "http://developer.wordnik.com"
		},
		"description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0` +
	` specification",
		"license": {
			"name": "Creative Commons 4.0 International",
			"url": "http://creativecommons.org/licenses/by/4.0/"
		},
		"termsOfService": "http://helloreverb.com/terms/",
		"title": "Swagger Sample API",
		"version": "1.0.9-abcd",
		"x-framework": "go-swagger"
	},
	"host": "some.api.out.there",
	"basePath": "/",
	"paths": {"x-framework":"go-swagger","/":{"$ref":"cats"}},
	"definitions": { "Category": { "type": "string"} },
	"parameters": {
		"categoryParam": {
			"name": "category",
			"in": "query",
			"type": "string"
		}
	},
	"responses": { "EmptyAnswer": { "description": "no data to return for this operation" } },
	"securityDefinitions": {
		"internalApiKey": {
			"type": "apiKey",
			"in": "header",
			"name": "api_key"
		}
	},
	"security": [{"internalApiKey":[]}],
	"tags": [{"name":"pets"}],
	"externalDocs": {"description":"the name","url":"the url"},
	"x-some-extension": "vendor",
	"x-schemes": ["unix","amqp"]
}`

func TestSwaggerSpec_Serialize(t *testing.T) {
	expected := make(map[string]interface{})
	_ = json.Unmarshal([]byte(specJSON), &expected)
	b, err := spec.MarshalJSON()
	if assert.NoError(t, err) {
		var actual map[string]interface{}
		err := json.Unmarshal(b, &actual)
		if assert.NoError(t, err) {
			assert.EqualValues(t, actual, expected)
		}
	}
}

func TestSwaggerSpec_Deserialize(t *testing.T) {
	var actual Swagger
	err := json.Unmarshal([]byte(specJSON), &actual)
	if assert.NoError(t, err) {
		assert.EqualValues(t, actual, spec)
	}
}

func TestSwaggerRoundtrip(t *testing.T) {
	cases := []jsontesting.RoundTripTestCase{
		{
			// Show at least one field from each embededd struct sitll allows
			// roundtrips successfully
			Name: "UnmarshalEmbedded",
			Object: &Swagger{
				VendorExtensible{Extensions{
					"x-framework": "go-swagger",
				}},
				SwaggerProps{
					Swagger: "2.0.0",
				},
			},
		}, {
			Name:   "BasicCase",
			JSON:   specJSON,
			Object: &spec,
		},
	}

	for _, tcase := range cases {
		t.Run(tcase.Name, func(t *testing.T) {
			require.NoError(t, tcase.RoundTripTest(&Swagger{}))
		})
	}
}

func TestSwaggerSpec_Marshalv2Fuzzed(t *testing.T) {
	fuzzer := fuzz.
		NewWithSeed(1646791953).
		NilChance(0.075).
		MaxDepth(13).
		NumElements(1, 2)

	fuzzer.Funcs(
		SwaggerFuzzFuncs...,
	)

	for i := 0; i < 100; i++ {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			swagger := Swagger{}
			fuzzer.Fuzz(&swagger)

			internal.UseOptimizedJSONMarshaling = false
			want, err := json.Marshal(swagger)
			if err != nil {
				t.Errorf("failed to marshal swagger: %v", err)
			}
			internal.UseOptimizedJSONMarshaling = true
			got, err := swagger.MarshalJSON()
			if err != nil {
				t.Errorf("failed to marshal next swagger: %v", err)
			}
			if err := jsontesting.JsonCompare(want, got); err != nil {
				t.Errorf("fuzzed marshal doesn't match: %v", err)
			}
		})
	}
}

func TestSwaggerSpec_Marshalv2FuzzedIsStable(t *testing.T) {
	swagFile, err := os.Open("../../schemaconv/testdata/swagger.json")
	if err != nil {
		t.Fatal(err)
	}
	defer swagFile.Close()

	js, err := io.ReadAll(swagFile)
	if err != nil {
		t.Fatal(err)
	}

	swagger := Swagger{}
	assert.NoError(t, json.Unmarshal(js, &swagger))

	for i := 0; i < 5; i++ {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			want, err := swagger.MarshalJSON()
			if err != nil {
				t.Errorf("failed to marshal swagger: %v", err)
			}
			got, err := swagger.MarshalJSON()
			if err != nil {
				t.Errorf("failed to marshal swagger again: %v", err)
			}
			if err := cmp.Diff(want, got); err != "" {
				t.Fatalf("expected both marshal to be identical/stable: %v", err)
			}
		})
	}
}

func TestUnmarshalAdditionalProperties(t *testing.T) {
	cases := []string{
		`{}`,
		`{"description": "the description of this schema"}`,
		`false`,
		`true`,
	}

	for _, tc := range cases {
		t.Run(tc, func(t *testing.T) {
			var v1, v2 SchemaOrBool
			internal.UseOptimizedJSONUnmarshaling = true
			require.NoError(t, json.Unmarshal([]byte(tc), &v2))
			internal.UseOptimizedJSONUnmarshaling = false
			require.NoError(t, json.Unmarshal([]byte(tc), &v1))
			if !cmp.Equal(v1, v2, SwaggerDiffOptions...) {
				t.Fatal(cmp.Diff(v1, v2, SwaggerDiffOptions...))
			}
		})
	}
}

func TestSwaggerSpec_ExperimentalUnmarshal(t *testing.T) {
	fuzzer := fuzz.
		NewWithSeed(1646791953).
		NilChance(0.01).
		MaxDepth(10).
		NumElements(1, 2)

	fuzzer.Funcs(
		SwaggerFuzzFuncs...,
	)

	expected := Swagger{}
	fuzzer.Fuzz(&expected)

	// Serialize into JSON
	jsonBytes, err := json.Marshal(expected)
	require.NoError(t, err)

	t.Log("Specimen", string(jsonBytes))

	actual := Swagger{}
	internal.UseOptimizedJSONUnmarshaling = true

	err = json.Unmarshal(jsonBytes, &actual)
	require.NoError(t, err)

	if !cmp.Equal(expected, actual, SwaggerDiffOptions...) {
		t.Fatal(cmp.Diff(expected, actual, SwaggerDiffOptions...))
	}

	control := Swagger{}
	internal.UseOptimizedJSONUnmarshaling = false
	err = json.Unmarshal(jsonBytes, &control)
	require.NoError(t, err)

	if !reflect.DeepEqual(control, actual) {
		t.Fatal(cmp.Diff(control, actual, SwaggerDiffOptions...))
	}
}

func BenchmarkSwaggerSpec_ExperimentalUnmarshal(b *testing.B) {
	// Download kube-openapi swagger json
	swagFile, err := os.Open("../../schemaconv/testdata/swagger.json")
	if err != nil {
		b.Fatal(err)
	}
	defer swagFile.Close()

	originalJSON, err := io.ReadAll(swagFile)
	if err != nil {
		b.Fatal(err)
	}

	b.ResetTimer()

	// Parse into kube-openapi types
	b.Run("jsonv1", func(b2 *testing.B) {
		internal.UseOptimizedJSONUnmarshaling = false
		for i := 0; i < b2.N; i++ {
			var result *Swagger
			if err := json.Unmarshal(originalJSON, &result); err != nil {
				b2.Fatal(err)
			}
		}
	})

	b.Run("jsonv2 via jsonv1", func(b2 *testing.B) {
		internal.UseOptimizedJSONUnmarshaling = true
		for i := 0; i < b2.N; i++ {
			var result *Swagger
			if err := json.Unmarshal(originalJSON, &result); err != nil {
				b2.Fatal(err)
			}
		}
	})

	// Our UnmarshalJSON implementation which defers to jsonv2 causes the
	// text to be parsed/validated twice. This costs a significant amount of time.
	b.Run("jsonv2", func(b2 *testing.B) {
		internal.UseOptimizedJSONUnmarshaling = true
		for i := 0; i < b2.N; i++ {
			var result Swagger
			if err := result.UnmarshalJSON(originalJSON); err != nil {
				b2.Fatal(err)
			}
		}
	})
}

func BenchmarkSwaggerSpec_ExperimentalMarshal(b *testing.B) {
	// Load kube-openapi swagger json
	swagFile, err := os.Open("../../schemaconv/testdata/swagger.json")
	if err != nil {
		b.Fatal(err)
	}
	defer swagFile.Close()

	originalJSON, err := io.ReadAll(swagFile)
	if err != nil {
		b.Fatal(err)
	}

	var swagger *Swagger
	if err := json.Unmarshal(originalJSON, &swagger); err != nil {
		b.Fatal(err)
	}

	b.ResetTimer()

	// Serialize kube-openapi types
	b.Run("jsonv1", func(b2 *testing.B) {
		b2.ReportAllocs()
		internal.UseOptimizedJSONMarshaling = false
		for i := 0; i < b2.N; i++ {
			if _, err = json.Marshal(swagger); err != nil {
				b2.Fatal(err)
			}
		}
	})

	b.Run("jsonv2 via jsonv1", func(b2 *testing.B) {
		b2.ReportAllocs()
		internal.UseOptimizedJSONMarshaling = true
		for i := 0; i < b2.N; i++ {
			if _, err := json.Marshal(swagger); err != nil {
				b2.Fatal(err)
			}
		}
	})

	b.Run("jsonv2", func(b2 *testing.B) {
		b2.ReportAllocs()
		internal.UseOptimizedJSONUnmarshaling = true
		for i := 0; i < b2.N; i++ {
			if _, err = swagger.MarshalJSON(); err != nil {
				b2.Fatal(err)
			}
		}
	})
}
