/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2006-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kordamp.json.test;

import junit.framework.Assert;
import org.apache.commons.lang3.StringUtils;
import org.kordamp.ezmorph.Morpher;
import org.kordamp.ezmorph.object.IdentityObjectMorpher;
import org.kordamp.json.JSON;
import org.kordamp.json.JSONArray;
import org.kordamp.json.JSONException;
import org.kordamp.json.JSONFunction;
import org.kordamp.json.JSONNull;
import org.kordamp.json.JSONObject;
import org.kordamp.json.JSONSerializer;
import org.kordamp.json.util.JSONUtils;

import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Provides assertions on equality for JSON strings and JSON types.
 *
 * @author Andres Almiray
 */
public class JSONAssert extends Assert {
    /**
     * Asserts that two JSON values are equal.
     */
    public static void assertEquals(JSON expected, JSON actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(JSONArray expected, JSONArray actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(JSONArray expected, String actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONFunctions are equal.
     */
    public static void assertEquals(JSONFunction expected, String actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONNulls are equal.
     */
    public static void assertEquals(JSONNull expected, String actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(JSONObject expected, JSONObject actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(JSONObject expected, String actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSON values are equal.
     */
    public static void assertEquals(String message, JSON expected, JSON actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected was null");
        }
        if (actual == null) {
            fail(header + "actual was null");
        }
        if (expected == actual || expected.equals(actual)) {
            return;
        }

        if (expected instanceof JSONArray) {
            if (actual instanceof JSONArray) {
                assertEquals(header, (JSONArray) expected, (JSONArray) actual);
            } else {
                fail(header + "actual is not a JSONArray");
            }
        } else if (expected instanceof JSONObject) {
            if (actual instanceof JSONObject) {
                assertEquals(header, (JSONObject) expected, (JSONObject) actual);
            } else {
                fail(header + "actual is not a JSONObject");
            }
        } else if (expected instanceof JSONNull) {
            if (actual instanceof JSONNull) {
                return;
            } else {
                fail(header + "actual is not a JSONNull");
            }
        }
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(String expected, JSONArray actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(String message, JSONArray expected, JSONArray actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected array was null");
        }
        if (actual == null) {
            fail(header + "actual array was null");
        }
        if (expected == actual || expected.equals(actual)) {
            return;
        }
        if (actual.size() != expected.size()) {
            fail(header + "arrays sizes differed, expected.length()=" + expected.size()
                + " actual.length()=" + actual.size());
        }

        int max = expected.size();
        for (int i = 0; i < max; i++) {
            Object o1 = expected.get(i);
            Object o2 = actual.get(i);

            // handle nulls
            if (JSONNull.getInstance()
                .equals(o1)) {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    continue;
                } else {
                    fail(header + "arrays first differed at element [" + i + "];");
                }
            } else {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    fail(header + "arrays first differed at element [" + i + "];");
                }
            }

            if (o1 instanceof JSONArray && o2 instanceof JSONArray) {
                JSONArray e = (JSONArray) o1;
                JSONArray a = (JSONArray) o2;
                assertEquals(header + "arrays first differed at element " + i + ";", e, a);
            } else {
                if (o1 instanceof String && o2 instanceof JSONFunction) {
                    assertEquals(header + "arrays first differed at element [" + i + "];", (String) o1,
                        (JSONFunction) o2);
                } else if (o1 instanceof JSONFunction && o2 instanceof String) {
                    assertEquals(header + "arrays first differed at element [" + i + "];",
                        (JSONFunction) o1, (String) o2);
                } else if (o1 instanceof JSONObject && o2 instanceof JSONObject) {
                    assertEquals(header + "arrays first differed at element [" + i + "];",
                        (JSONObject) o1, (JSONObject) o2);
                } else if (o1 instanceof JSONArray && o2 instanceof JSONArray) {
                    assertEquals(header + "arrays first differed at element [" + i + "];",
                        (JSONArray) o1, (JSONArray) o2);
                } else if (o1 instanceof JSONFunction && o2 instanceof JSONFunction) {
                    assertEquals(header + "arrays first differed at element [" + i + "];",
                        (JSONFunction) o1, (JSONFunction) o2);
                } else {
                    if (o1 instanceof String) {
                        assertEquals(header + "arrays first differed at element [" + i + "];",
                            (String) o1, String.valueOf(o2));
                    } else if (o2 instanceof String) {
                        assertEquals(header + "arrays first differed at element [" + i + "];",
                            String.valueOf(o1), (String) o2);
                    } else {
                        Morpher m1 = JSONUtils.getMorpherRegistry()
                            .getMorpherFor(o1.getClass());
                        Morpher m2 = JSONUtils.getMorpherRegistry()
                            .getMorpherFor(o2.getClass());
                        if (m1 != null && m1 != IdentityObjectMorpher.getInstance()) {
                            assertEquals(header + "arrays first differed at element [" + i + "];", o1,
                                JSONUtils.getMorpherRegistry()
                                    .morph(o1.getClass(), o2));
                        } else if (m2 != null && m2 != IdentityObjectMorpher.getInstance()) {
                            assertEquals(header + "arrays first differed at element [" + i + "];",
                                JSONUtils.getMorpherRegistry()
                                    .morph(o1.getClass(), o1), o2);
                        } else {
                            assertEquals(header + "arrays first differed at element [" + i + "];", o1, o2);
                        }
                    }
                }
            }
        }
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(String message, JSONArray expected, String actual) {
        try {
            assertEquals(message, expected, JSONArray.fromObject(actual));
        } catch (JSONException e) {
            String header = StringUtils.isBlank(message) ? "" : message + ": ";
            fail(header + "actual is not a JSONArray");
        }
    }

    /**
     * Asserts that two JSONFunctions are equal.
     */
    public static void assertEquals(String expected, JSONFunction actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONFunctions are equal.
     */
    public static void assertEquals(String message, JSONFunction expected, String actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected function was null");
        }
        if (actual == null) {
            fail(header + "actual string was null");
        }

        try {
            assertEquals(header, expected, JSONFunction.parse(actual));
        } catch (JSONException jsone) {
            fail(header + "'" + actual + "' is not a function");
        }
    }

    /**
     * Asserts that two JSONNulls are equal.
     */
    public static void assertEquals(String expected, JSONNull actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONNulls are equal.
     */
    public static void assertEquals(String message, JSONNull expected, String actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (actual == null) {
            fail(header + "actual string was null");
        } else if (expected == null) {
            assertEquals(header, "null", actual);
        } else {
            assertEquals(header, expected.toString(), actual);
        }
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(String expected, JSONObject actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(String message, JSONObject expected, JSONObject actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected object was null");
        }
        if (actual == null) {
            fail(header + "actual object was null");
        }
        if (expected == actual /* || expected.equals( actual ) */) {
            return;
        }

        if (expected.isNullObject()) {
            if (actual.isNullObject()) {
                return;
            } else {
                fail(header + "actual is not a null JSONObject");
            }
        } else {
            if (actual.isNullObject()) {
                fail(header + "actual is a null JSONObject");
            }
        }

        if (expected.names().size() != actual.names().size()) {
            fail(header + missingAndUnexpectedNames(expected, actual));
        }

        for (Iterator keys = expected.keys(); keys.hasNext(); ) {
            String key = (String) keys.next();
            Object o1 = expected.opt(key);
            Object o2 = actual.opt(key);

            if (JSONNull.getInstance()
                .equals(o1)) {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    continue;
                } else {
                    fail(header + "objects differed at key [" + key + "];");
                }
            } else {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    fail(header + "objects differed at key [" + key + "];");
                }
            }

            if (o1 instanceof String && o2 instanceof JSONFunction) {
                assertEquals(header + "objects differed at key [" + key + "];", (String) o1,
                    (JSONFunction) o2);
            } else if (o1 instanceof JSONFunction && o2 instanceof String) {
                assertEquals(header + "objects differed at key [" + key + "];", (JSONFunction) o1,
                    (String) o2);
            } else if (o1 instanceof JSONObject && o2 instanceof JSONObject) {
                assertEquals(header + "objects differed at key [" + key + "];", (JSONObject) o1,
                    (JSONObject) o2);
            } else if (o1 instanceof JSONArray && o2 instanceof JSONArray) {
                assertEquals(header + "objects differed at key [" + key + "];", (JSONArray) o1,
                    (JSONArray) o2);
            } else if (o1 instanceof JSONFunction && o2 instanceof JSONFunction) {
                assertEquals(header + "objects differed at key [" + key + "];", (JSONFunction) o1,
                    (JSONFunction) o2);
            } else {
                if (o1 instanceof String) {
                    assertEquals(header + "objects differed at key [" + key + "];", (String) o1,
                        String.valueOf(o2));
                } else if (o2 instanceof String) {
                    assertEquals(header + "objects differed at key [" + key + "];",
                        String.valueOf(o1), (String) o2);
                } else {
                    Morpher m1 = JSONUtils.getMorpherRegistry()
                        .getMorpherFor(o1.getClass());
                    Morpher m2 = JSONUtils.getMorpherRegistry()
                        .getMorpherFor(o2.getClass());
                    if (m1 != null && m1 != IdentityObjectMorpher.getInstance()) {
                        assertEquals(header + "objects differed at key [" + key + "];", o1,
                            JSONUtils.getMorpherRegistry()
                                .morph(o1.getClass(), o2));
                    } else if (m2 != null && m2 != IdentityObjectMorpher.getInstance()) {
                        assertEquals(header + "objects differed at key [" + key + "];",
                            JSONUtils.getMorpherRegistry()
                                .morph(o1.getClass(), o1), o2);
                    } else {
                        assertEquals(header + "objects differed at key [" + key + "];", o1, o2);
                    }
                }
            }
        }
    }

    private static String missingAndUnexpectedNames(JSONObject expected,
                                                    JSONObject actual) {
        String missingExpectedNames = missingExpectedNames(expected, actual);
        String unexpectedNames = unexpectedNames(expected, actual);
        if (missingExpectedNames != null && unexpectedNames != null) {
            return missingExpectedNames + ", " + unexpectedNames;
        } else if (missingExpectedNames != null) {
            return missingExpectedNames;
        } else {
            return unexpectedNames;
        }
    }

    private static String missingExpectedNames(JSONObject expected,
                                               JSONObject actual) {
        SortedSet keysInExpectedButNotInActual = new TreeSet(expected.keySet());
        keysInExpectedButNotInActual.removeAll(actual.keySet());
        if (keysInExpectedButNotInActual.isEmpty()) {
            return null;
        } else {
            return "missing expected names: [" + StringUtils.join(keysInExpectedButNotInActual, ", ") + "]";
        }
    }

    private static String unexpectedNames(JSONObject expected, JSONObject actual) {
        SortedSet keysInActualButNotInExpected = new TreeSet(actual.keySet());
        keysInActualButNotInExpected.removeAll(expected.keySet());
        if (keysInActualButNotInExpected.isEmpty()) {
            return null;
        } else {
            return "unexpected names: [" + StringUtils.join(keysInActualButNotInExpected, ", ") + "]";
        }
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(String message, JSONObject expected, String actual) {
        try {
            assertEquals(message, expected, JSONObject.fromObject(actual));
        } catch (JSONException e) {
            String header = StringUtils.isBlank(message) ? "" : message + ": ";
            fail(header + "actual is not a JSONObject");
        }
    }

    /**
     * Asserts that two JSONArrays are equal.
     */
    public static void assertEquals(String message, String expected, JSONArray actual) {
        try {
            assertEquals(message, JSONArray.fromObject(expected), actual);
        } catch (JSONException e) {
            String header = StringUtils.isBlank(message) ? "" : message + ": ";
            fail(header + "expected is not a JSONArray");
        }
    }

    /**
     * Asserts that two JSONFunctions are equal.
     */
    public static void assertEquals(String message, String expected, JSONFunction actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected string was null");
        }
        if (actual == null) {
            fail(header + "actual function was null");
        }

        try {
            assertEquals(header, JSONFunction.parse(expected), actual);
        } catch (JSONException jsone) {
            fail(header + "'" + expected + "' is not a function");
        }
    }

    /**
     * Asserts that two JSONNulls are equal.
     */
    public static void assertEquals(String message, String expected, JSONNull actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected was null");
        } else if (actual == null) {
            assertEquals(header, expected, "null");
        } else {
            assertEquals(header, expected, actual.toString());
        }
    }

    /**
     * Asserts that two JSONObjects are equal.
     */
    public static void assertEquals(String message, String expected, JSONObject actual) {
        try {
            assertEquals(message, JSONObject.fromObject(expected), actual);
        } catch (JSONException e) {
            String header = StringUtils.isBlank(message) ? "" : message + ": ";
            fail(header + "expected is not a JSONObject");
        }
    }

    /**
     * Asserts that two JSON strings are equal.
     */
    public static void assertJsonEquals(String expected, String actual) {
        assertJsonEquals(null, expected, actual);
    }

    /**
     * Asserts that two JSON strings are equal.
     */
    public static void assertJsonEquals(String message, String expected, String actual) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (expected == null) {
            fail(header + "expected was null");
        }
        if (actual == null) {
            fail(header + "actual was null");
        }

        JSON json1 = null;
        JSON json2 = null;
        try {
            json1 = JSONSerializer.toJSON(expected);
        } catch (JSONException jsone) {
            fail(header + "expected is not a valid JSON string");
        }
        try {
            json2 = JSONSerializer.toJSON(actual);
        } catch (JSONException jsone) {
            fail(header + "actual is not a valid JSON string");
        }
        assertEquals(header, json1, json2);
    }

    /**
     * Asserts that a JSON value is not null.<br>
     * Fails if:
     * <ul>
     * <li>JSONNull.getInstance().equals( json )</li>
     * <li>((JSONObject) json).isNullObject()</li>
     * </ul>
     */
    public static void assertNotNull(JSON json) {
        assertNotNull(null, json);
    }

    /**
     * Asserts that a JSON value is not null.<br>
     * Fails if:
     * <ul>
     * <li>JSONNull.getInstance().equals( json )</li>
     * <li>((JSONObject) json).isNullObject()</li>
     * </ul>
     */
    public static void assertNotNull(String message, JSON json) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (json instanceof JSONObject) {
            assertFalse(header + "Object is null", ((JSONObject) json).isNullObject());
        } else if (JSONNull.getInstance()
            .equals(json)) {
            fail(header + "Object is null");
        }
    }

    /**
     * Asserts that a JSON value is null.<br>
     * Fails if:
     * <ul>
     * <li>!JSONNull.getInstance().equals( json )</li>
     * <li>!((JSONObject) json).isNullObject()</li>
     * </ul>
     */
    public static void assertNull(JSON json) {
        assertNull(null, json);
    }

    /**
     * Asserts that a JSON value is null.<br>
     * Fails if:
     * <ul>
     * <li>!JSONNull.getInstance().equals( json )</li>
     * <li>!((JSONObject) json).isNullObject()</li>
     * </ul>
     */
    public static void assertNull(String message, JSON json) {
        String header = StringUtils.isBlank(message) ? "" : message + ": ";
        if (json instanceof JSONObject) {
            assertTrue(header + "Object is not null", ((JSONObject) json).isNullObject());
        } else if (!JSONNull.getInstance()
            .equals(json)) {
            fail(header + "Object is not null");
        }
    }
}