// +build go1.18

/*
 * Copyright 2021 ByteDance Inc.
 *
 * 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 sonic_fuzz

import (
	`encoding/json`
	`math/rand`
	`fmt`
	`reflect`
	`strconv`
	`strings`
	`testing`

	`github.com/bytedance/sonic`
	`github.com/stretchr/testify/require`
)

func generateNullType() reflect.Type {
	tab := []reflect.Type {
		reflect.TypeOf(int(0)),
		reflect.TypeOf(uint(0)),
		reflect.TypeOf("string"),
		reflect.TypeOf(struct{}{}),
		reflect.TypeOf(json.Number("0")),
		reflect.TypeOf([]interface{}{}),
		reflect.TypeOf(map[string]interface{}{}),
	}
	return tab[int(rand.Int() % len(tab))]
}

func generateNumberType() reflect.Type {
	tab := []reflect.Type {
		reflect.TypeOf(float64(0)),
		reflect.TypeOf(int64(0)),
		reflect.TypeOf(uintptr(0)),
		reflect.TypeOf(uint64(0)),
		reflect.TypeOf(json.Number("0")),
	}
	return tab[int(rand.Int() % len(tab))]
}

func generatePointerType(ft reflect.Type) reflect.Type {
	if ft == nil {
		ft = generateNullType()
	}
	ftp := reflect.PtrTo(ft)
	ftpp := reflect.PtrTo(ftp)
	ftppp := reflect.PtrTo(ftpp)
	tab := []reflect.Type { ft, ftp, ftpp, ftppp }
	return tab[int(rand.Int() % len(tab))]
}

func generateJSONTag(name string) reflect.StructTag {
	var opt string
	name = strings.Split(name, ",")[0] // remove origin "," in tag name
	switch int(rand.Int() % 5) {
		case 0: return reflect.StructTag(`json:"-"`) // always omitted
		case 1: return reflect.StructTag("") // empty tag
		case 2: opt = "" // empty opt
		case 3: opt = "omitempty"
		case 4: opt = "string"
	}
	return reflect.StructTag(fmt.Sprintf(`json:"%s,%s"`, name, opt))
}

// Map2StructType generate random dynamic Golang Struct by Golang Map
func Map2StructType(m map[string]interface{}, maxDepth int) reflect.Type {
	if maxDepth <= 0 {
		return reflect.TypeOf(map[string]interface{}{})
	}
	fields := make([]reflect.StructField, 0)
	i := 0
	for k, v := range m {
		/* skip the empty key */
		if k == "" {
			continue
		}

		/* set exported field name */
		fn := "F" + strconv.Itoa(i)

		/* set field type */
		ft := reflect.TypeOf(v)
		if ft == nil {
			ft = generateNullType()

		} else {
			switch ft.Kind() {
				case reflect.Map: ft = Map2StructType(v.(map[string]interface{}), maxDepth-1)
				case reflect.Float64: ft = generateNumberType()
			}
		}
		ft = generatePointerType(ft)
		ef := reflect.StructField {
			Name: fn,
			Type: ft,
			Tag : generateJSONTag(k),
		}
		fields = append(fields, ef)
		i += 1

		/* insert some private field randomly */
		if int(rand.Int() % 3) != 0 {
			continue
		}
		fn = "p" + strconv.Itoa(i)
		pf := reflect.StructField {
			Name: fn,
			Type: ft,
			PkgPath: "sonic_fuzz",
		}
		fields = append(fields, pf)

	}
	rt := reflect.StructOf(fields)
	return rt
}

const _MAX_STRUCT_DEPTH = 30

// fuzzDynamicStruct is schema-based fuzz testing, 
// a struct type is a JSON schema.
func fuzzDynamicStruct(t *testing.T, data []byte, v map[string]interface{}) {
	typ  := Map2StructType(v, _MAX_STRUCT_DEPTH)
	sv  := reflect.New(typ).Interface()
	jv  := reflect.New(typ).Interface()

	// Pretouch fuzz
	err := sonic.Pretouch(typ)
	require.NoErrorf(t, err, "error in sonic pretouch struct %v", typ)

	// Unmarshal fuzz
	serr := sonic.Unmarshal(data, &sv)
	jerr := json.Unmarshal(data, &jv)
	require.Equalf(t, serr != nil, jerr != nil, "different error in sonic unmarshal %v", typ)
	if serr != nil {
		return
	}
	require.Equal(t, sv, jv, "different result in sonic unmarshal %v", typ)

	// Marshal fuzz
	sout, serr := sonic.Marshal(sv)
	jout, jerr := json.Marshal(jv)
	require.NoError(t, serr, "error in sonic marshal %v", typ)
	require.NoError(t, jerr, "error in json marshal %v", typ)
	// not comparing here because sonic marshal is different from encoding/json, as:
	// require.Equalf(t, sout, jout, "different in sonic marshal %#v", typ)
	var _, _ = sout, jout
}