class Script {

    public static AssertionResult matchJsonOrObject(MatchType matchType, ScriptValue actual, String path, String expression, ScenarioContext context) {
        DocumentContext actualDoc;
        switch (actual.getType()) {
            case JSON:
            case MAP:
            case LIST:
                actualDoc = actual.getAsJsonDocument();
                break;
            case XML: // auto convert !
                actualDoc = XmlUtils.toJsonDoc(actual.getValue(Node.class));
                break;
            case STRING: // an edge case when the variable is a plain string not JSON, so switch to plain string compare
            case INPUT_STREAM:
                String actualString = actual.getAsString();
                ScriptValue expectedString = evalKarateExpression(expression, context);
                // exit the function early
                if (!expectedString.isStringOrStream()) {
                    return matchFailed(matchType, path, actualString, expectedString.getValue(),
                            "type of actual value is 'string' but that of expected is " + expectedString.getType());
                } else {
                    return matchStringOrPattern('.', path, matchType, null, null, actual, expectedString.getAsString(), context);
                }
            case PRIMITIVE: // an edge case when the variable is non-string, not-json (number / boolean)
                ScriptValue expected = evalKarateExpression(expression, context);
                if (expected.isStringOrStream()) { // fuzzy match macro
                    return matchStringOrPattern('.', path, matchType, null, null, actual, expected.getAsString(), context);
                } else {
                    return matchPrimitive(matchType, path, actual.getValue(), expected.getValue());
                }
            case NULL: // edge case, assume that this is the root variable that is null and the match is for an optional e.g. '##string'
                ScriptValue expectedNull = evalKarateExpression(expression, context);
                if (expectedNull.isNull()) {
                    if (matchType == MatchType.NOT_EQUALS) {
                        return matchFailed(matchType, path, null, null, "actual and expected values are both null");
                    }
                    return AssertionResult.PASS;
                } else if (!expectedNull.isStringOrStream()) { // primitive or anything which is not a string
                    if (matchType == MatchType.NOT_EQUALS) {
                        return AssertionResult.PASS;
                    }
                    return matchFailed(matchType, path, null, expectedNull.getValue(), "actual value is null but expected is " + expectedNull);
                } else {
                    return matchStringOrPattern('.', path, matchType, null, null, actual, expectedNull.getAsString(), context);
                }
            case BYTE_ARRAY:
                ScriptValue expectedBytesValue = evalKarateExpression(expression, context);
                byte[] expectedBytes = expectedBytesValue.getAsByteArray();
                byte[] actualBytes = actual.getAsByteArray();
                if (Arrays.equals(expectedBytes, actualBytes)) {
                    if (matchType == MatchType.NOT_EQUALS) {
                        return matchFailed(matchType, path, actualBytes, expectedBytes, "actual and expected byte-arrays are not equal");
                    }
                    return AssertionResult.PASS;
                } else {
                    if (matchType == MatchType.NOT_EQUALS) {
                        return AssertionResult.PASS;
                    }
                    return matchFailed(matchType, path, actualBytes, expectedBytes, "actual and expected byte-arrays are not equal");
                }
            default:
                throw new RuntimeException("not json, cannot do json path for value: " + actual + ", path: " + path);
        }
        ScriptValue expected = evalKarateExpressionForMatch(expression, context);
        Object actObject;
        try {
            actObject = actualDoc.read(path); // note that the path for actObject is 'reset' to '$' here
        } catch (PathNotFoundException e) {
            if (expected.isString()) {
                String expString = expected.getAsString();
                if (isOptionalMacro(expString) || "#notpresent".equals(expString) || "#ignore".equals(expString)) {
                    return AssertionResult.PASS;
                }
            }
            return matchFailed(matchType, path, null, expected.getValue(), "actual json-path does not exist");
        }
        Object expObject;
        switch (expected.getType()) {
            case JSON: // convert to map or list
                expObject = expected.getValue(DocumentContext.class).read("$");
                break;
            default:
                expObject = expected.getValue();
        }
        switch (matchType) {
            case CONTAINS:
            case NOT_CONTAINS:
            case CONTAINS_ONLY:
            case CONTAINS_ANY:
            case CONTAINS_DEEP:
                if (actObject instanceof List && !(expObject instanceof List)) { // if RHS is not a list, make it so
                    expObject = Collections.singletonList(expObject);
                }
            case NOT_EQUALS:
            case EQUALS:
                return matchNestedObject('.', path, matchType, actualDoc, null, actObject, expObject, context);
            case EACH_CONTAINS:
            case EACH_NOT_CONTAINS:
            case EACH_CONTAINS_ONLY:
            case EACH_CONTAINS_ANY:
            case EACH_NOT_EQUALS:
            case EACH_EQUALS:
                if (actObject instanceof List) {
                    List actList = (List) actObject;
                    MatchType listMatchType = getInnerMatchType(matchType);
                    int actSize = actList.size();
                    for (int i = 0; i < actSize; i++) {
                        Object actListObject = actList.get(i);
                        AssertionResult ar = matchNestedObject('.', "$[" + i + "]", listMatchType, actObject, actListObject, actListObject, expObject, context);
                        if (!ar.pass) {
                            if (matchType == MatchType.EACH_NOT_EQUALS) {
                                return AssertionResult.PASS; // exit early
                            } else {
                                return ar; // fail early
                            }
                        }
                    }
                    // if we reached here all list items (each) matched
                    if (matchType == MatchType.EACH_NOT_EQUALS) {
                        return matchFailed(matchType, path, actual.getValue(), expected.getValue(), "all list items matched");
                    }
                    return AssertionResult.PASS;
                } else {
                    throw new RuntimeException("'match each' failed, not a json array: + " + actual + ", path: " + path);
                }
            default: // dead code
                throw new RuntimeException("unexpected match type: " + matchType);
        }
    }

    private  Script();

}