package httpexpect

import (
	"reflect"
)

// Object provides methods to inspect attached map[string]interface{} object
// (Go representation of JSON object).
type Object struct {
	chain chain
	value map[string]interface{}
}

// NewObject returns a new Object given a reporter used to report failures
// and value to be inspected.
//
// Both reporter and value should not be nil. If value is nil, failure is
// reported.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
func NewObject(reporter Reporter, value map[string]interface{}) *Object {
	chain := makeChain(reporter)
	if value == nil {
		chain.fail("expected non-nil map value")
	} else {
		value, _ = canonMap(&chain, value)
	}
	return &Object{chain, value}
}

// Raw returns underlying value attached to Object.
// This is the value originally passed to NewObject, converted to canonical form.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  assert.Equal(t, map[string]interface{}{"foo": 123.0}, object.Raw())
func (o *Object) Raw() map[string]interface{} {
	return o.value
}

// Path is similar to Value.Path.
func (o *Object) Path(path string) *Value {
	return getPath(&o.chain, o.value, path)
}

// Schema is similar to Value.Schema.
func (o *Object) Schema(schema interface{}) *Object {
	checkSchema(&o.chain, o.value, schema)
	return o
}

// Keys returns a new Array object that may be used to inspect objects keys.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
//  object.Keys().ContainsOnly("foo", "bar")
func (o *Object) Keys() *Array {
	keys := []interface{}{}
	for k := range o.value {
		keys = append(keys, k)
	}
	return &Array{o.chain, keys}
}

// Values returns a new Array object that may be used to inspect objects values.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
//  object.Values().ContainsOnly(123, 456)
func (o *Object) Values() *Array {
	values := []interface{}{}
	for _, v := range o.value {
		values = append(values, v)
	}
	return &Array{o.chain, values}
}

// Value returns a new Value object that may be used to inspect single value
// for given key.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.Value("foo").Number().Equal(123)
func (o *Object) Value(key string) *Value {
	value, ok := o.value[key]
	if !ok {
		o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
			key, dumpValue(o.value))
		return &Value{o.chain, nil}
	}
	return &Value{o.chain, value}
}

// Empty succeeds if object is empty.
//
// Example:
//  object := NewObject(t, map[string]interface{}{})
//  object.Empty()
func (o *Object) Empty() *Object {
	return o.Equal(map[string]interface{}{})
}

// NotEmpty succeeds if object is non-empty.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.NotEmpty()
func (o *Object) NotEmpty() *Object {
	return o.NotEqual(map[string]interface{}{})
}

// Equal succeeds if object is equal to given Go map or struct.
// Before comparison, both object and value are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.Equal(map[string]interface{}{"foo": 123})
func (o *Object) Equal(value interface{}) *Object {
	expected, ok := canonMap(&o.chain, value)
	if !ok {
		return o
	}
	if !reflect.DeepEqual(expected, o.value) {
		o.chain.fail("\nexpected object equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
			dumpValue(expected),
			dumpValue(o.value),
			diffValues(expected, o.value))
	}
	return o
}

// NotEqual succeeds if object is not equal to given Go map or struct.
// Before comparison, both object and value are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.Equal(map[string]interface{}{"bar": 123})
func (o *Object) NotEqual(v interface{}) *Object {
	expected, ok := canonMap(&o.chain, v)
	if !ok {
		return o
	}
	if reflect.DeepEqual(expected, o.value) {
		o.chain.fail("\nexpected object not equal to:\n%s",
			dumpValue(expected))
	}
	return o
}

// ContainsKey succeeds if object contains given key.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.ContainsKey("foo")
func (o *Object) ContainsKey(key string) *Object {
	if !o.containsKey(key) {
		o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
			key, dumpValue(o.value))
	}
	return o
}

// NotContainsKey succeeds if object doesn't contain given key.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.NotContainsKey("bar")
func (o *Object) NotContainsKey(key string) *Object {
	if o.containsKey(key) {
		o.chain.fail(
			"\nexpected object not containing key '%s', but got:\n%s", key,
			dumpValue(o.value))
	}
	return o
}

// ContainsMap succeeds if object contains given Go value.
// Before comparison, both object and value are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// Example:
//  object := NewObject(t, map[string]interface{}{
//      "foo": 123,
//      "bar": []interface{}{"x", "y"},
//      "bar": map[string]interface{}{
//          "a": true,
//          "b": false,
//      },
//  })
//
//  object.ContainsMap(map[string]interface{}{  // success
//      "foo": 123,
//      "bar": map[string]interface{}{
//          "a": true,
//      },
//  })
//
//  object.ContainsMap(map[string]interface{}{  // failure
//      "foo": 123,
//      "qux": 456,
//  })
//
//  object.ContainsMap(map[string]interface{}{  // failure, slices should match exactly
//      "bar": []interface{}{"x"},
//  })
func (o *Object) ContainsMap(value interface{}) *Object {
	if !o.containsMap(value) {
		o.chain.fail("\nexpected object containing sub-object:\n%s\n\nbut got:\n%s",
			dumpValue(value), dumpValue(o.value))
	}
	return o
}

// NotContainsMap succeeds if object doesn't contain given Go value.
// Before comparison, both object and value are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
//  object.NotContainsMap(map[string]interface{}{"foo": 123, "bar": "no-no-no"})
func (o *Object) NotContainsMap(value interface{}) *Object {
	if o.containsMap(value) {
		o.chain.fail("\nexpected object not containing sub-object:\n%s\n\nbut got:\n%s",
			dumpValue(value), dumpValue(o.value))
	}
	return o
}

// ValueEqual succeeds if object's value for given key is equal to given Go value.
// Before comparison, both values are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.ValueEqual("foo", 123)
func (o *Object) ValueEqual(key string, value interface{}) *Object {
	if !o.containsKey(key) {
		o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
			key, dumpValue(o.value))
		return o
	}
	expected, ok := canonValue(&o.chain, value)
	if !ok {
		return o
	}
	if !reflect.DeepEqual(expected, o.value[key]) {
		o.chain.fail(
			"\nexpected value for key '%s' equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
			key,
			dumpValue(expected),
			dumpValue(o.value[key]),
			diffValues(expected, o.value[key]))
	}
	return o
}

// ValueNotEqual succeeds if object's value for given key is not equal to given
// Go value. Before comparison, both values are converted to canonical form.
//
// value should be map[string]interface{} or struct.
//
// If object doesn't contain any value for given key, failure is reported.
//
// Example:
//  object := NewObject(t, map[string]interface{}{"foo": 123})
//  object.ValueNotEqual("foo", "bad value")  // success
//  object.ValueNotEqual("bar", "bad value")  // failure! (key is missing)
func (o *Object) ValueNotEqual(key string, value interface{}) *Object {
	if !o.containsKey(key) {
		o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
			key, dumpValue(o.value))
		return o
	}
	expected, ok := canonValue(&o.chain, value)
	if !ok {
		return o
	}
	if reflect.DeepEqual(expected, o.value[key]) {
		o.chain.fail("\nexpected value for key '%s' not equal to:\n%s",
			key, dumpValue(expected))
	}
	return o
}

func (o *Object) containsKey(key string) bool {
	for k := range o.value {
		if k == key {
			return true
		}
	}
	return false
}

func (o *Object) containsMap(sm interface{}) bool {
	submap, ok := canonMap(&o.chain, sm)
	if !ok {
		return false
	}
	return checkContainsMap(o.value, submap)
}

func checkContainsMap(outer, inner map[string]interface{}) bool {
	for k, iv := range inner {
		ov, ok := outer[k]
		if !ok {
			return false
		}
		if ovm, ok := ov.(map[string]interface{}); ok {
			if ivm, ok := iv.(map[string]interface{}); ok {
				if !checkContainsMap(ovm, ivm) {
					return false
				}
				continue
			}
		}
		if !reflect.DeepEqual(ov, iv) {
			return false
		}
	}
	return true
}
