package matchers

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/onsi/gomega/format"
)

// missingFieldError represents a missing field extraction error that
// HaveExistingFieldMatcher can ignore, as opposed to other, sever field
// extraction errors, such as nil pointers, et cetera.
type missingFieldError string

func (e missingFieldError) Error() string {
	return string(e)
}

func extractField(actual interface{}, field string, matchername string) (any, error) {
	fields := strings.SplitN(field, ".", 2)
	actualValue := reflect.ValueOf(actual)

	if actualValue.Kind() == reflect.Ptr {
		actualValue = actualValue.Elem()
	}
	if actualValue == (reflect.Value{}) {
		return nil, fmt.Errorf("%s encountered nil while dereferencing a pointer of type %T.", matchername, actual)
	}

	if actualValue.Kind() != reflect.Struct {
		return nil, fmt.Errorf("%s encountered:\n%s\nWhich is not a struct.", matchername, format.Object(actual, 1))
	}

	var extractedValue reflect.Value

	if strings.HasSuffix(fields[0], "()") {
		extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()"))
		if extractedValue == (reflect.Value{}) && actualValue.CanAddr() {
			extractedValue = actualValue.Addr().MethodByName(strings.TrimSuffix(fields[0], "()"))
		}
		if extractedValue == (reflect.Value{}) {
			ptr := reflect.New(actualValue.Type())
			ptr.Elem().Set(actualValue)
			extractedValue = ptr.MethodByName(strings.TrimSuffix(fields[0], "()"))
			if extractedValue == (reflect.Value{}) {
				return nil, missingFieldError(fmt.Sprintf("%s could not find method named '%s' in struct of type %T.", matchername, fields[0], actual))
			}
		}
		t := extractedValue.Type()
		if t.NumIn() != 0 || t.NumOut() != 1 {
			return nil, fmt.Errorf("%s found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", matchername, fields[0], actual)
		}
		extractedValue = extractedValue.Call([]reflect.Value{})[0]
	} else {
		extractedValue = actualValue.FieldByName(fields[0])
		if extractedValue == (reflect.Value{}) {
			return nil, missingFieldError(fmt.Sprintf("%s could not find field named '%s' in struct:\n%s", matchername, fields[0], format.Object(actual, 1)))
		}
	}

	if len(fields) == 1 {
		return extractedValue.Interface(), nil
	} else {
		return extractField(extractedValue.Interface(), fields[1], matchername)
	}
}

type HaveFieldMatcher struct {
	Field    string
	Expected interface{}
}

func (matcher *HaveFieldMatcher) expectedMatcher() omegaMatcher {
	var isMatcher bool
	expectedMatcher, isMatcher := matcher.Expected.(omegaMatcher)
	if !isMatcher {
		expectedMatcher = &EqualMatcher{Expected: matcher.Expected}
	}
	return expectedMatcher
}

func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) {
	extractedField, err := extractField(actual, matcher.Field, "HaveField")
	if err != nil {
		return false, err
	}

	return matcher.expectedMatcher().Match(extractedField)
}

func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) {
	extractedField, err := extractField(actual, matcher.Field, "HaveField")
	if err != nil {
		// this really shouldn't happen
		return fmt.Sprintf("Failed to extract field '%s': %s", matcher.Field, err)
	}
	message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field)
	message += matcher.expectedMatcher().FailureMessage(extractedField)

	return message
}

func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	extractedField, err := extractField(actual, matcher.Field, "HaveField")
	if err != nil {
		// this really shouldn't happen
		return fmt.Sprintf("Failed to extract field '%s': %s", matcher.Field, err)
	}
	message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field)
	message += matcher.expectedMatcher().NegatedFailureMessage(extractedField)

	return message
}
