package test

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"reflect"
	"strconv"
	"testing"
	"time"

	"github.com/joho/godotenv"
	decort "repository.basistech.ru/BASIS/decort-golang-sdk"
	"repository.basistech.ru/BASIS/decort-golang-sdk/config"
)

func getClient() (*decort.DecortClient, error) {
	cfg := config.Config{
		Retries:       5,
		SSLSkipVerify: true,
	}

	err := godotenv.Load(".env")
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	appID, ok := os.LookupEnv("AppID")
	if !ok {
		return &decort.DecortClient{}, fmt.Errorf("AppID is not set")
	}
	cfg.AppID = appID

	AppSecret, ok := os.LookupEnv("AppSecret")
	if !ok {
		return &decort.DecortClient{}, fmt.Errorf("AppSecret is not set")
	}
	cfg.AppSecret = AppSecret

	SSOURL, ok := os.LookupEnv("SSOURL")
	if !ok {
		return &decort.DecortClient{}, fmt.Errorf("SSOURL is not set")
	}
	cfg.SSOURL = SSOURL

	DecortURL, ok := os.LookupEnv("DecortURL")
	if !ok {
		return &decort.DecortClient{}, fmt.Errorf("DecortURL is not set")
	}
	cfg.DecortURL = DecortURL

	cfg.SetTimeout(5 * time.Minute)
	c := decort.New(cfg)
	return c, nil
}

// getResult checks how json-structure of bytes is different from golang structure and return t.Error if there are any differences.
func getResult(testName string, bytes []byte, structure any, t *testing.T) string {
	// convert bytes to map[string]interface{}
	bytesMap, err := GetMapFromBytes(bytes)
	if err != nil {
		t.Errorf("%s: can not unmarshal json", testName)
	}

	// convert structure to map[string]interface{}
	structMap := GetMapFromStructure(structure)

	// assert if they are equal
	if !reflect.DeepEqual(bytesMap, structMap) {
		var result = getDifference(testName, bytesMap, structMap)
		t.Errorf("\n%s\n", result)
		return result
	}
	result := fmt.Sprint(testName, ": ok")
	t.Log(result)
	return result
}

// getDifference tells which fields are present in bytesMap and not present in structMap and vice versa.
func getDifference(testName string, bytesMap, structMap map[string]interface{}) string {
	var result string
	bytesFields, _ := evaluate(bytesMap, structMap)
	// structFields, _ := evaluate(structMap, bytesMap)
	result += fmt.Sprintf("%s: ", testName)

	if len(bytesFields) > 0 {
		result += fmt.Sprintf("\nPlatform has these fields that golang struct doesn't: %v\n", bytesFields)
	}

	// if len(structFields) > 0 {
	// 	result += fmt.Sprintf("Golang struct has these fields that platform doesn't: %v\n", structFields)
	// }

	if len(bytesFields) == 0 {
		result += "OK"
	}

	return result
}

func evaluate(m1, m2 map[string]interface{}) ([]string, []string) {
	var s1, s2 []string

	for k1, v1 := range m1 {
		v2, ok := m2[k1]
		if !ok {
			s1 = append(s1, k1)
			continue
		}

		v1Map := v1.(map[string]interface{})
		v2Map := v2.(map[string]interface{})

		len1 := len(v1Map)
		len2 := len(v2Map)

		if len1 == 0 && len2 == 0 {
			continue
		} else {
			toAdd1, toAdd2 := evaluate(v1Map, v2Map)
			s1 = append(s1, toAdd1...)
			s2 = append(s2, toAdd2...)
		}
	}

	for k2, v2 := range m2 {
		v1, ok := m1[k2]
		if !ok {
			if !contains(s2, k2) {
				s2 = append(s2, k2)
			}
			continue
		}

		v1Map := v1.(map[string]interface{})
		v2Map := v2.(map[string]interface{})

		len1 := len(v1Map)
		len2 := len(v2Map)

		if len1 == 0 && len2 == 0 {
			continue
		} else {
			toAdd1, toAdd2 := evaluate(v1Map, v2Map)
			s1 = appendSelectedElements(s1, toAdd1)
			s2 = appendSelectedElements(s2, toAdd2)
		}
	}

	return s1, s2
}

func contains(slice []string, element string) bool {
	for _, elem := range slice {
		if elem == element {
			return true
		}
	}
	return false
}

func appendSelectedElements(slice []string, elements []string) []string {
	result := make([]string, 0, len(slice))
	result = append(result, slice...)

	for _, el := range elements {
		if !contains(slice, el) {
			result = append(result, el)
		}
	}

	return result
}

// GetMapFromBytes converts []byte to map[string]interface{}
func GetMapFromBytes(bytes []byte) (map[string]interface{}, error) {
	var unmarshalMaps map[string]interface{}
	err := json.Unmarshal(bytes, &unmarshalMaps)
	if err != nil {
		var unmarshalSlice []interface{}
		err = json.Unmarshal(bytes, &unmarshalSlice)
		if err != nil {
			return nil, err
		}
		if len(unmarshalSlice) == 0 {
			return map[string]interface{}{}, nil
		}
		t, ok := unmarshalSlice[0].(map[string]interface{})
		if !ok {
			return nil, err
		}
		unmarshalMaps = t
	}
	if err != nil {
		return nil, err
	}

	return getMapBytes(unmarshalMaps), nil
}

func getMapBytes(value map[string]interface{}) map[string]interface{} {
	temporary := make(map[string]interface{})
	for k, v := range value {
		list, ok := v.([]interface{})
		if ok && len(list) > 0 {
			elem, ok := list[0].(map[string]interface{})
			if ok {
				temporary[k] = getMapBytes(elem)
				continue
			}
			temporary[k] = map[string]interface{}{}
			continue
		}

		m, ok := v.(map[string]interface{})
		if ok {
			temporary[k] = getMapBytes(m)
			continue
		}

		if _, err := strconv.Atoi(k); err != nil {
			temporary[k] = map[string]interface{}{}
		}
	}

	return temporary
}

// GetMapFromStructure converts golang structure to map[string]interface{}
func GetMapFromStructure(structure any) map[string]interface{} {
	typ := reflect.TypeOf(structure)

	for typ.Kind() == reflect.Slice || typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
	}

	return getMapStruct(typ, make(map[reflect.Type]bool))
}

func getMapStruct(t reflect.Type, visited map[reflect.Type]bool) map[string]interface{} {
	if visited[t] {
		return map[string]interface{}{}
	}
	visited[t] = true
	defer func() { visited[t] = false }()

	temporary := make(map[string]interface{})
	for i := 0; i < t.NumField(); i++ {
		tag := t.Field(i).Tag.Get("json")
		kind := t.Field(i).Type.Kind()

		//nolint
		switch kind {
		case reflect.Array, reflect.Slice:
			elem := t.Field(i).Type.Elem()
			switch elem.Kind() {
			case reflect.Struct:
				temporary[tag] = getMapStruct(elem, visited)
			default:
				temporary[tag] = map[string]interface{}{}
			}

		case reflect.Struct:
			if tag == "" {
				return getMapStruct(t.Field(i).Type, visited)
			} else {
				temporary[tag] = getMapStruct(t.Field(i).Type, visited)
			}
			// в этом месте работает некорректно для вложенных структур без json тегов (scope - все в cloudbroker)

		case reflect.Map, reflect.Interface, reflect.String, reflect.Bool, reflect.Int64, reflect.Int, reflect.Uint,
			reflect.Int32, reflect.Uint64, reflect.Float32, reflect.Float64:
			temporary[tag] = map[string]interface{}{}
		}
	}

	return temporary
}

// getMapFromFile converts json to map[string]interface{}
func getMapFromFile(bytes []byte) (map[string]interface{}, error) {
	var x map[string]interface{}
	err := json.Unmarshal(bytes, &x)
	if err != nil {
		return nil, err
	}

	paths := x["paths"].(map[string]interface{})

	return paths, nil
}
