package test

import (
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"
	"testing"
)

// extractTypeFromSchema extracts type from schema, handling oneOf cases
// When oneOf contains a type and null, it returns the non-null type
func extractTypeFromSchema(schema map[string]interface{}) string {
	// Check for direct type
	if paramType, ok := schema["type"].(string); ok {
		return paramType
	}

	// Check for oneOf
	if oneOf, ok := schema["oneOf"].([]interface{}); ok {
		for _, item := range oneOf {
			if itemMap, ok := item.(map[string]interface{}); ok {
				if itemType, ok := itemMap["type"].(string); ok {
					// Skip null type, return the first non-null type found
					if itemType != "null" {
						return itemType
					}
				}
			}
		}
	}

	return ""
}

func getParameters(input map[string]interface{}) []interface{} {
	var emptySlice []interface{}
	methods := []string{"get", "post", "put", "delete", "patch", "head", "options"}

	var methodData interface{}
	found := false

	for _, method := range methods {
		if data, ok := input[method]; ok {
			methodData = data
			found = true
			break
		}
	}

	if !found {
		return emptySlice
	}

	parameters, ok := methodData.(map[string]interface{})
	if !ok {
		return emptySlice
	}

	var result []interface{}

	if requestBody, ok := parameters["requestBody"].(map[string]interface{}); ok {
		if content, ok := requestBody["content"].(map[string]interface{}); ok {
			// Check for application/x-www-form-urlencoded, application/json, or multipart/form-data
			var schemaData map[string]interface{}
			var found bool

			if formData, ok := content["application/x-www-form-urlencoded"].(map[string]interface{}); ok {
				if schema, ok := formData["schema"].(map[string]interface{}); ok {
					schemaData = schema
					found = true
				}
			} else if jsonData, ok := content["application/json"].(map[string]interface{}); ok {
				if schema, ok := jsonData["schema"].(map[string]interface{}); ok {
					schemaData = schema
					found = true
				}
			} else if multipartData, ok := content["multipart/form-data"].(map[string]interface{}); ok {
				if schema, ok := multipartData["schema"].(map[string]interface{}); ok {
					schemaData = schema
					found = true
				}
			}

			if found && schemaData != nil {
				if properties, ok := schemaData["properties"].(map[string]interface{}); ok {
					requiredFields := make(map[string]bool)
					if req, ok := schemaData["required"].([]interface{}); ok {
						for _, r := range req {
							if reqStr, ok := r.(string); ok {
								requiredFields[reqStr] = true
							}
						}
					}
					for name, prop := range properties {
						propMap, ok := prop.(map[string]interface{})
						if !ok {
							continue
						}
						newParam := make(map[string]interface{})
						newParam["name"] = name
						propType := extractTypeFromSchema(propMap)
						if propType != "" {
							newParam["type"] = propType
						}
						newParam["required"] = requiredFields[name]
						if propType == "array" {
							// Try to get items from propMap first
							if items, ok := propMap["items"].(map[string]interface{}); ok {
								newParam["items"] = items
							} else if oneOf, ok := propMap["oneOf"].([]interface{}); ok {
								// If items not in propMap, try to get from oneOf array element
								for _, item := range oneOf {
									if itemMap, ok := item.(map[string]interface{}); ok {
										if itemType, ok := itemMap["type"].(string); ok && itemType == "array" {
											if items, ok := itemMap["items"].(map[string]interface{}); ok {
												newParam["items"] = items
												break
											}
										}
									}
								}
							}
						}
						result = append(result, newParam)
					}
				}
			}
		}
	}

	if params, ok := parameters["parameters"].([]interface{}); ok {
		for _, p := range params {
			param, ok := p.(map[string]interface{})
			if !ok {
				continue
			}

			newParam := make(map[string]interface{})
			if name, ok := param["name"].(string); ok {
				newParam["name"] = name
			}
			if schema, ok := param["schema"].(map[string]interface{}); ok {
				paramType := extractTypeFromSchema(schema)
				if paramType != "" {
					newParam["type"] = paramType
				}
				if paramType == "array" {
					if items, ok := schema["items"].(map[string]interface{}); ok {
						newParam["items"] = items
					} else if oneOf, ok := schema["oneOf"].([]interface{}); ok {
						for _, item := range oneOf {
							if itemMap, ok := item.(map[string]interface{}); ok {
								if itemType, ok := itemMap["type"].(string); ok && itemType == "array" {
									if items, ok := itemMap["items"].(map[string]interface{}); ok {
										newParam["items"] = items
										break
									}
								}
							}
						}
					}
				}
			}
			// Handle required field
			if required, ok := param["required"].(bool); ok {
				newParam["required"] = required
			} else {
				newParam["required"] = false
			}
			result = append(result, newParam)
		}
	}

	if len(result) > 0 {
		return result
	}

	return emptySlice
}

func getBytesFromJSON(fileName string, t *testing.T) []byte {
	jsonFile, err := os.Open(fileName)
	if err != nil {
		t.Error(err)
	}
	defer jsonFile.Close()

	bytes, err := io.ReadAll(jsonFile)
	if err != nil {
		t.Error(err)
	}

	return bytes
}

func getErrorsFromJSON(bytes []byte, t *testing.T, cloud string) {
	var requests map[string]interface{}

	switch cloud {
	case "cloudapi":
		requests = getRequestsMapCloudAPI()
	case "cloudbroker":
		requests = getRequestsMapCloudbroker()
	case "sdn":
		requests = getRequestsMapSDN()
	default:
		t.Fatalf("Wrong cloud provided, expected `cloudapi`, `cloudbroker` or `sdn`, got %s", cloud)
	}

	var dataLogs []string

	paths, err := getMapFromFile(bytes)
	if err != nil {
		t.Error(err)
	}
	i := 0
	for k, v := range paths {
		// exclude deprecated urls from analysis
		if !validateUrlFromJson(k) {
			continue
		}

		params := getParameters(v.(map[string]interface{}))
		structure, ok := requests[k]
		if !ok {
			continue
		}
		i++
		typStruct := reflect.TypeOf(structure)

		var errs []string
		// empty request case
		if len(params) == 0 && structure == nil {
			continue
		}
		if len(params) != typStruct.NumField() {
			errs = append(errs, fmt.Sprintf("Platform (%d) and golang structure (%d) have different amount of fields.", len(params), typStruct.NumField()))
		}
		paramMap := make(map[string]bool)
		paramRequiredMap := make(map[string]bool)
		for _, p := range params {
			param, ok := p.(map[string]interface{})
			if !ok {
				continue
			}
			name, ok := param["name"].(string)
			if ok {
				paramMap[name] = true
				required, ok := param["required"].(bool)
				if ok {
					paramRequiredMap[name] = required
				} else {
					paramRequiredMap[name] = false
				}
			}
		}

		for _, p := range params {
			param, ok := p.(map[string]interface{})
			if !ok {
				continue
			}

			name, ok := param["name"].(string)
			if !ok {
				name = ""
			}
			required, ok := param["required"].(bool)
			if !ok {
				required = false
			}
			typ, ok := p.(map[string]interface{})["type"].(string)
			if !ok {
				typ = ""
			}

			var items string
			if p.(map[string]interface{})["items"] != nil {
				itemsTemp := p.(map[string]interface{})["items"]
				if itemsTemp != nil {
					itemsType := itemsTemp.(map[string]interface{})["type"]
					if itemsType != nil {
						items, ok = itemsType.(string)
						if !ok {
							items = ""
						}
					}
				}
			}

			var found bool
			for i := 0; i < typStruct.NumField(); i++ {
				jsonTag := typStruct.Field(i).Tag.Get("json")
				validation, _ := typStruct.Field(i).Tag.Lookup("validate")

				if checkName(name, jsonTag) {
					if !checkRequired(required, validation, typ) {
						errs = append(errs, fmt.Sprintf("Field %s has different required parameters on the platform and in golang structure", name))
					}
					if !checkKind(typ, items, typStruct.Field(i).Type) {
						errs = append(errs, fmt.Sprintf("Field %s has different type parameters on the platform and in golang structure", name))
					}
					found = true
					break
				}
			}

			if !found {
				errs = append(errs, fmt.Sprintf("Platform has field %s that golang structure doesn't", name))
			}
		}

		// Check if required fields in Go structure are missing from platform JSON
		// or if they exist but are not required on platform
		for i := 0; i < typStruct.NumField(); i++ {
			jsonTag := typStruct.Field(i).Tag.Get("json")
			validation, _ := typStruct.Field(i).Tag.Lookup("validate")

			fieldName := strings.Split(jsonTag, ",")[0]
			if fieldName == "" || fieldName == "-" {
				continue
			}

			if strings.Contains(validation, "required") {
				if !paramMap[fieldName] {
					errs = append(errs, fmt.Sprintf("Golang structure has required field %s that platform doesn't", fieldName))
				} else {
					platformRequired := paramRequiredMap[fieldName]
					if !platformRequired {
						fieldType := typStruct.Field(i).Type
						if fieldType.Kind() == reflect.Bool {
							errs = append(errs, fmt.Sprintf("Golang structure has required field %s that platform doesn't", fieldName))
						}
					}
				}
			}
		}

		if len(errs) > 0 {
			msg := fmt.Sprintf("Path %s has following errors: %v", k, errs)
			t.Error(msg)
			dataLogs = append(dataLogs, msg)
		}
	}

	if len(requests) != i {
		msg := fmt.Sprintf("Amount of structure checked (%d) is not the same as amount of platform requests available (%d), please check getRequests func in code.",
			i, len(requests))
		t.Error(msg)
		dataLogs = append(dataLogs, msg)
	}
}

// checkName checks if name field from platform has the same value as json tag in golang structure (maybe including omitempty)
func checkName(name, tag string) bool {
	return strings.Contains(tag, name)
}

// checkRequired checks if required field from platform has the same value as validate tag in golang structure
func checkRequired(required bool, validation, fieldType string) bool {
	if required && strings.Contains(validation, "required") {
		return true
	}
	if required && (!strings.Contains(validation, "omitempty") && validation != "") {
		return true
	}
	if !required && (validation == "" || strings.Contains(validation, "omitempty")) {
		return true
	}

	if fieldType == "boolean" {
		return true // otherwise we have issues with setting false/true
	}

	return false
}

// checkKind checks if type field from platform has the same value as field type in golang structure
func checkKind(platformType, items string, typ reflect.Type) bool {
	//nolint
	switch typ.Kind() {
	case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Int16, reflect.Uint, reflect.Uint64, reflect.Uint16, reflect.Uint32, reflect.Uint8:
		if platformType == "integer" {
			return true
		}
		return false

	case reflect.String:
		if platformType == "string" {
			return true
		}
		return false

	case reflect.Bool:
		if platformType == "boolean" {
			return true
		}
		return false

	case reflect.Float32, reflect.Float64:
		if platformType == "number" {
			return true
		}
		return false

	case reflect.Array, reflect.Slice:
		if platformType != "array" {
			return false
		}

		elem := typ.Elem()
		switch elem.Kind() {
		case reflect.String:
			if items == "string" {
				return true
			}
			return false

		case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Int16, reflect.Uint, reflect.Uint64, reflect.Uint16, reflect.Uint32, reflect.Uint8:
			if items == "integer" {
				return true
			}
			return false

		default: // for cases like dataDisks etc.
			return true

		}

	default: // for cases like drivers etc
		return true
	}
}
