package awstesting

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"net/url"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"testing"

	"github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil"
)

// Match is a testing helper to test for testing error by comparing expected
// with a regular expression.
func Match(t *testing.T, regex, expected string) {
	if !regexp.MustCompile(regex).Match([]byte(expected)) {
		t.Errorf("%q\n\tdoes not match /%s/", expected, regex)
	}
}

// AssertURL verifies the expected URL is matches the actual.
func AssertURL(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
	expectURL, err := url.Parse(expect)
	if err != nil {
		t.Errorf(errMsg("unable to parse expected URL", err, msgAndArgs))
		return false
	}
	actualURL, err := url.Parse(actual)
	if err != nil {
		t.Errorf(errMsg("unable to parse actual URL", err, msgAndArgs))
		return false
	}

	equal(t, expectURL.Host, actualURL.Host, msgAndArgs...)
	equal(t, expectURL.Scheme, actualURL.Scheme, msgAndArgs...)
	equal(t, expectURL.Path, actualURL.Path, msgAndArgs...)

	return AssertQuery(t, expectURL.Query().Encode(), actualURL.Query().Encode(), msgAndArgs...)
}

var queryMapKey = regexp.MustCompile("(.*?)\\.[0-9]+\\.key")

// AssertQuery verifies the expect HTTP query string matches the actual.
func AssertQuery(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
	expectQ, err := url.ParseQuery(expect)
	if err != nil {
		t.Errorf(errMsg("unable to parse expected Query", err, msgAndArgs))
		return false
	}
	actualQ, err := url.ParseQuery(actual)
	if err != nil {
		t.Errorf(errMsg("unable to parse actual Query", err, msgAndArgs))
		return false
	}

	// Make sure the keys are the same
	if !equal(t, queryValueKeys(expectQ), queryValueKeys(actualQ), msgAndArgs...) {
		return false
	}

	keys := map[string][]string{}
	for key, v := range expectQ {
		if queryMapKey.Match([]byte(key)) {
			submatch := queryMapKey.FindStringSubmatch(key)
			keys[submatch[1]] = append(keys[submatch[1]], v...)
		}
	}

	for k, v := range keys {
		// clear all keys that have prefix
		for key := range expectQ {
			if strings.HasPrefix(key, k) {
				delete(expectQ, key)
			}
		}

		sort.Strings(v)
		for i, value := range v {
			expectQ[fmt.Sprintf("%s.%d.key", k, i+1)] = []string{value}
		}
	}

	for k, expectQVals := range expectQ {
		sort.Strings(expectQVals)
		actualQVals := actualQ[k]
		sort.Strings(actualQVals)
		if !equal(t, expectQVals, actualQVals, msgAndArgs...) {
			return false
		}
	}

	return true
}

// AssertJSON verifies that the expect json string matches the actual.
func AssertJSON(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
	expectVal := map[string]interface{}{}
	if err := json.Unmarshal([]byte(expect), &expectVal); err != nil {
		t.Errorf(errMsg("unable to parse expected JSON", err, msgAndArgs...))
		return false
	}

	actualVal := map[string]interface{}{}
	if err := json.Unmarshal([]byte(actual), &actualVal); err != nil {
		t.Errorf(errMsg("unable to parse actual JSON", err, msgAndArgs...))
		return false
	}

	return equal(t, expectVal, actualVal, msgAndArgs...)
}

// AssertXML verifies that the expect xml string matches the actual.
// Elements in actual must match the order they appear in expect to match equally
func AssertXML(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
	buffer := bytes.NewReader([]byte(expect))
	decoder := xml.NewDecoder(buffer)

	expectVal, err := xmlutil.XMLToStruct(decoder, nil)
	if err != nil {
		t.Fatalf("failed to umarshal xml to struct: %v", err)
	}

	buffer = bytes.NewReader([]byte(actual))
	decoder = xml.NewDecoder(buffer)

	actualVal, err := xmlutil.XMLToStruct(decoder, nil)
	if err != nil {
		t.Fatalf("failed to umarshal xml to struct: %v", err)
	}

	return equal(t, expectVal, actualVal, msgAndArgs...)
}

// DidPanic returns if the function paniced and returns true if the function paniced.
func DidPanic(fn func()) (bool, interface{}) {
	var paniced bool
	var msg interface{}
	func() {
		defer func() {
			if msg = recover(); msg != nil {
				paniced = true
			}
		}()
		fn()
	}()

	return paniced, msg
}

// objectsAreEqual determines if two objects are considered equal.
//
// This function does no assertion of any kind.
//
// Based on github.com/stretchr/testify/assert.ObjectsAreEqual
// Copied locally to prevent non-test build dependencies on testify
func objectsAreEqual(expected, actual interface{}) bool {
	if expected == nil || actual == nil {
		return expected == actual
	}

	return reflect.DeepEqual(expected, actual)
}

// Equal asserts that two objects are equal.
//
//    assert.Equal(t, 123, 123, "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Based on github.com/stretchr/testify/assert.Equal
// Copied locally to prevent non-test build dependencies on testify
func equal(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) bool {
	if !objectsAreEqual(expected, actual) {
		t.Errorf("%s\n%s", messageFromMsgAndArgs(msgAndArgs),
			SprintExpectActual(expected, actual))
		return false
	}

	return true
}

func errMsg(baseMsg string, err error, msgAndArgs ...interface{}) string {
	message := messageFromMsgAndArgs(msgAndArgs)
	if message != "" {
		message += ", "
	}
	return fmt.Sprintf("%s%s, %v", message, baseMsg, err)
}

// Based on github.com/stretchr/testify/assert.messageFromMsgAndArgs
// Copied locally to prevent non-test build dependencies on testify
func messageFromMsgAndArgs(msgAndArgs []interface{}) string {
	if len(msgAndArgs) == 0 || msgAndArgs == nil {
		return ""
	}
	if len(msgAndArgs) == 1 {
		return msgAndArgs[0].(string)
	}
	if len(msgAndArgs) > 1 {
		return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
	}
	return ""
}

func queryValueKeys(v url.Values) []string {
	keys := make([]string, 0, len(v))
	for k := range v {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

// SprintExpectActual returns a string for test failure cases when the actual
// value is not the same as the expected.
func SprintExpectActual(expect, actual interface{}) string {
	return fmt.Sprintf("expect: %+v\nactual: %+v\n", expect, actual)
}
