package httpexpect

import (
	"encoding/json"
	"fmt"
	"reflect"
	"regexp"

	"github.com/xeipuuv/gojsonschema"
	"github.com/yalp/jsonpath"
	"github.com/yudai/gojsondiff"
	"github.com/yudai/gojsondiff/formatter"
)

func toString(str interface{}) (s string, ok bool) {
	ok = true
	defer func() {
		if err := recover(); err != nil {
			ok = false
		}
	}()
	s = reflect.ValueOf(str).Convert(reflect.TypeOf("")).String()
	return
}

func getPath(chain *chain, value interface{}, path string) *Value {
	if chain.failed() {
		return &Value{*chain, nil}
	}

	result, err := jsonpath.Read(value, path)
	if err != nil {
		chain.fail(err.Error())
		return &Value{*chain, nil}
	}

	return &Value{*chain, result}
}

func checkSchema(chain *chain, value, schema interface{}) {
	if chain.failed() {
		return
	}

	valueLoader := gojsonschema.NewGoLoader(value)

	var schemaLoader gojsonschema.JSONLoader

	if str, ok := toString(schema); ok {
		if ok, _ := regexp.MatchString(`^\w+://`, str); ok {
			schemaLoader = gojsonschema.NewReferenceLoader(str)
		} else {
			schemaLoader = gojsonschema.NewStringLoader(str)
		}
	} else {
		schemaLoader = gojsonschema.NewGoLoader(schema)
	}

	result, err := gojsonschema.Validate(schemaLoader, valueLoader)
	if err != nil {
		chain.fail("\n%s\n\nschema:\n%s\n\nvalue:\n%s",
			err.Error(),
			dumpSchema(schema),
			dumpValue(value))
		return
	}

	if !result.Valid() {
		errors := ""
		for _, err := range result.Errors() {
			errors += fmt.Sprintf(" %s\n", err)
		}

		chain.fail(
			"\njson schema validation failed, schema:\n%s\n\nvalue:%s\n\nerrors:\n%s",
			dumpSchema(schema),
			dumpValue(value),
			errors)

		return
	}
}

func dumpSchema(schema interface{}) string {
	if s, ok := toString(schema); ok {
		schema = s
	}
	return regexp.MustCompile(`(?m:^)`).
		ReplaceAllString(fmt.Sprintf("%v", schema), " ")
}

func canonNumber(chain *chain, number interface{}) (f float64, ok bool) {
	ok = true
	defer func() {
		if err := recover(); err != nil {
			chain.fail("%v", err)
			ok = false
		}
	}()
	f = reflect.ValueOf(number).Convert(reflect.TypeOf(float64(0))).Float()
	return
}

func canonArray(chain *chain, in interface{}) ([]interface{}, bool) {
	var out []interface{}
	data, ok := canonValue(chain, in)
	if ok {
		out, ok = data.([]interface{})
		if !ok {
			chain.fail("expected array, got %v", out)
		}
	}
	return out, ok
}

func canonMap(chain *chain, in interface{}) (map[string]interface{}, bool) {
	var out map[string]interface{}
	data, ok := canonValue(chain, in)
	if ok {
		out, ok = data.(map[string]interface{})
		if !ok {
			chain.fail("expected map, got %v", out)
		}
	}
	return out, ok
}

func canonValue(chain *chain, in interface{}) (interface{}, bool) {
	b, err := json.Marshal(in)
	if err != nil {
		chain.fail(err.Error())
		return nil, false
	}

	var out interface{}
	if err := json.Unmarshal(b, &out); err != nil {
		chain.fail(err.Error())
		return nil, false
	}

	return out, true
}

func dumpValue(value interface{}) string {
	b, err := json.MarshalIndent(value, " ", "  ")
	if err != nil {
		return " " + fmt.Sprintf("%#v", value)
	}
	return " " + string(b)
}

func diffValues(expected, actual interface{}) string {
	differ := gojsondiff.New()

	var diff gojsondiff.Diff

	if ve, ok := expected.(map[string]interface{}); ok {
		if va, ok := actual.(map[string]interface{}); ok {
			diff = differ.CompareObjects(ve, va)
		} else {
			return " (unavailable)"
		}
	} else if ve, ok := expected.([]interface{}); ok {
		if va, ok := actual.([]interface{}); ok {
			diff = differ.CompareArrays(ve, va)
		} else {
			return " (unavailable)"
		}
	} else {
		return " (unavailable)"
	}

	config := formatter.AsciiFormatterConfig{
		ShowArrayIndex: true,
	}
	f := formatter.NewAsciiFormatter(expected, config)

	str, err := f.Format(diff)
	if err != nil {
		return " (unavailable)"
	}

	return "--- expected\n+++ actual\n" + str
}
