/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *   http://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.apache.johnzon.core;

import static java.util.Collections.singletonMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.StringReader;
import java.io.StringWriter;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonPatch;
import jakarta.json.JsonReader;
import jakarta.json.JsonValue;
import jakarta.json.JsonWriter;
import jakarta.json.stream.JsonGenerator;

import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

public class JsonPatchDiffTest {
    @Test
    public void nestedObjects() {
        final String source = "{\n" +
                "  \"caseDiscussionDailySchedule\":{\n" +
                "    \"schedule\":{\n" +
                "      \"TUESDAY\":[\n" +
                "        {\n" +
                "          \"start\":\"07:00+03:00\",\n" +
                "          \"end\":\"08:00+03:00\"\n" +
                "        }\n" +
                "      ],\n" +
                "      \"MONDAY\":[\n" +
                "        {\n" +
                "          \"start\":\"07:00+03:00\",\n" +
                "          \"end\":\"08:00+03:00\"\n" +
                "        }\n" +
                "      ]\n" +
                "    }\n" +
                "  }\n" +
                "}";
        final String operation = "[" +
                "{\"op\":\"replace\",\"path\":\"/caseDiscussionDailySchedule/schedule/MONDAY/0/start\",\"value\":null}" +
                "]";
        final String expectedResult = "{\n" +
                "  \"caseDiscussionDailySchedule\":{\n" +
                "    \"schedule\":{\n" +
                "      \"TUESDAY\":[\n" +
                "        {\n" +
                "          \"start\":\"07:00+03:00\",\n" +
                "          \"end\":\"08:00+03:00\"\n" +
                "        }\n" +
                "      ],\n" +
                "      \"MONDAY\":[\n" +
                "        {\n" +
                "          \"end\":\"08:00+03:00\",\n" +
                "          \"start\":null\n" +
                "        }\n" +
                "      ]\n" +
                "    }\n" +
                "  }\n" +
                "}";
        final JsonReader reader1 = Json.createReader(new StringReader(source));
        final JsonObject src = reader1.readObject();
        reader1.close();
        final JsonReader reader2 = Json.createReader(new StringReader(operation));
        final JsonArray op = reader2.readArray();
        reader2.close();
        final JsonObject result = Json.createPatch(op).apply(src);
        final StringWriter formattedResult = new StringWriter();
        final JsonWriter writer = Json.createWriterFactory(singletonMap(JsonGenerator.PRETTY_PRINTING, true))
                .createWriter(formattedResult);
        writer.write(result);
        writer.close();
        assertEquals(expectedResult, formattedResult.toString());
    }
    @Test
    public void fromEmptyArray() {
        final JsonObject from = Json.createObjectBuilder().add("testEmpty", JsonValue.EMPTY_JSON_ARRAY).build();
        final JsonObject to = Json.createObjectBuilder()
                .add("testEmpty", Json.createArrayBuilder().add("something"))
                .build();
        final  JsonPatch diff = Json.createDiff(from, to);
        assertEquals("[{\"op\":\"add\",\"path\":\"/testEmpty/0\",\"value\":\"something\"}]", diff.toString());
    }

    @Test
    public void toEmptyArray() {
        final JsonObject from = Json.createObjectBuilder()
                .add("testEmpty", Json.createArrayBuilder().add("something"))
                .build();
        final JsonObject to = Json.createObjectBuilder().add("testEmpty", JsonValue.EMPTY_JSON_ARRAY).build();
        final  JsonPatch diff = Json.createDiff(from, to);
        assertEquals("[{\"op\":\"remove\",\"path\":\"/testEmpty/0\"}]", diff.toString());
    }

    @Test
    public void testAddDiff() {
        // {"a":"xa"}
        String jsonA = "{\"a\":\"xa\"}";

        // {"a":"xa","b":"xb"}
        String jsonB = "{\"a\":\"xa\",\"b\":\"xb\"}";

        // this results in 1 diff operations:
        // adding "b"
        JsonPatch jsonPatch = Json.createDiff(Json.createReader(new StringReader(jsonA)).readObject(),
                                              Json.createReader(new StringReader(jsonB)).readObject());
        assertNotNull(jsonPatch);
        JsonArray patchOperations = jsonPatch.toJsonArray();
        assertNotNull(patchOperations);
        assertEquals(1, patchOperations.size());
        containsOperation(patchOperations, JsonPatch.Operation.ADD, "/b", Json.createValue("xb"));
    }

    @Test
    public void testAddDiffNewObject() {

        JsonObject target = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "value")
                                              .add("ab", "another"))
                                .build();

        JsonPatch patch = Json.createDiff(JsonValue.EMPTY_JSON_OBJECT, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.ADD, "/a", target.get("a"));

        // now try to apply that patch.
        JsonObject patched = patch.apply(JsonValue.EMPTY_JSON_OBJECT);
        Assert.assertEquals(target, patched);
    }

    @Test
    public void testAddDiffNewObjectWithEscaping() {

        JsonObject target = Json.createObjectBuilder()
                                .add("a~/", Json.createObjectBuilder()
                                                .add("esc/aped", "value")
                                                .add("tilde", "another"))
                                .build();

        JsonPatch patch = Json.createDiff(JsonValue.EMPTY_JSON_OBJECT, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.ADD, "/a~0~1", target.get("a"));

        // now try to apply that patch.
        JsonObject patched = patch.apply(JsonValue.EMPTY_JSON_OBJECT);
        Assert.assertEquals(target, patched);
    }

    @Test
    public void testAddDiffInNestedObject() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "value"))
                                .build();

        JsonObject target = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "value")
                                              .add("bb", "another value"))
                                .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.ADD, "/a/bb", Json.createValue("another value"));
    }

    @Test
    public void testRemoveDiffObject() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", "value")
                                .build();

        JsonPatch patch = Json.createDiff(source, JsonValue.EMPTY_JSON_OBJECT);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.REMOVE, "/a");
    }

    @Test
    public void testRemoveDiffNestedObject() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", "value")
                                .add("nested", Json.createObjectBuilder()
                                                   .add("1", 1)
                                                   .add("2", 2))
                                .build();

        {
            JsonPatch patch = Json.createDiff(source, JsonValue.EMPTY_JSON_OBJECT);
            assertNotNull(patch);

            JsonArray operations = patch.toJsonArray();
            assertEquals(2, operations.size());

            containsOperation(operations, JsonPatch.Operation.REMOVE, "/a");
            containsOperation(operations, JsonPatch.Operation.REMOVE, "/nested");
        }

        {
            JsonObject target = Json.createObjectBuilder()
                                    .add("a", "value")
                                    .add("nested", Json.createObjectBuilder()
                                                       .add("1", 1))
                                    .build();

            JsonPatch patch = Json.createDiff(source, target);
            assertNotNull(patch);

            JsonArray operations = patch.toJsonArray();
            assertEquals(1, operations.size());

            containsOperation(operations, JsonPatch.Operation.REMOVE, "/nested/2");
        }
    }

    @Test
    public void testDiffEqualObjects() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", "value")
                                .build();

        JsonObject target = Json.createObjectBuilder()
                                .add("a", "value")
                                .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);
        assertEquals(0, patch.toJsonArray().size());
    }

    @Test
    public void testDiffReplaceObject() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", "value")
                                .build();

        JsonObject target = Json.createObjectBuilder()
                                .add("a", "replaced")
                                .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.REPLACE, "/a", Json.createValue("replaced"));
    }

    @Test
    public void testDiffReplaceFromNestedObject() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "value"))
                                .build();

        JsonObject target = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "replaced"))
                                .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.REPLACE, "/a/aa", Json.createValue("replaced"));
    }

    //X TODO add move support -> see https://issues.apache.org/jira/browse/JOHNZON-105
    @Test
    @Ignore
    public void testDiffMoveValue() {

        JsonObject source = Json.createObjectBuilder()
                                .add("a", Json.createObjectBuilder()
                                              .add("aa", "valueToMove"))
                                .add("b", JsonValue.EMPTY_JSON_OBJECT)
                                .build();

        JsonObject target = Json.createObjectBuilder()
                                .add("a", JsonValue.EMPTY_JSON_OBJECT)
                                .add("b", Json.createObjectBuilder()
                                              .add("aa", "valueToMove"))
                                .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        JsonObject operation = operations.getJsonObject(0);
        assertEquals(JsonPatch.Operation.MOVE.operationName(), operation.getString("op"));
        assertEquals("/a/aa", operation.getString("from"));
        assertEquals("/b/aa", operation.getString("to"));
    }


    @Test
    public void testAddValueToArray() {

        JsonArray source = Json.createArrayBuilder()
                               .add("first")
                               .build();

        JsonArray target = Json.createArrayBuilder()
                               .add("first")
                               .add("second")
                               .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.ADD, "/1", Json.createValue("second"));
    }

    @Test
    public void testAddObjectToArray() {

        JsonArray source = Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                        .add("a", "a"))
                .build();

        JsonArray target = Json.createArrayBuilder()
                               .add(Json.createObjectBuilder()
                                        .add("a", "a"))
                               .add(Json.createObjectBuilder()
                                        .add("a", "b"))
                               .build();


        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.ADD, "/1", Json.createObjectBuilder()
                                                                         .add("a", "b")
                                                                         .build());
    }

    @Test
    public void testRemoveValueFromArray() {

        JsonArray source = Json.createArrayBuilder()
                               .add("a")
                               .add("b")
                               .build();

        JsonArray target = Json.createArrayBuilder()
                               .add("b")
                               .build();

        JsonPatch patch = Json.createDiff(source, target);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(2, operations.size());

        containsOperation(operations, JsonPatch.Operation.REPLACE, "/0", Json.createValue("b"));
        containsOperation(operations, JsonPatch.Operation.REMOVE, "/1");
    }

    @Test
    public void testRemoveObjectFromArray() {

        JsonArray source = Json.createArrayBuilder()
                               .add(Json.createObjectBuilder()
                                        .add("a", "a")
                                        .add("b", "b"))
                               .build();

        JsonPatch patch = Json.createDiff(source, JsonValue.EMPTY_JSON_ARRAY);
        assertNotNull(patch);

        JsonArray operations = patch.toJsonArray();
        assertEquals(1, operations.size());

        containsOperation(operations, JsonPatch.Operation.REMOVE, "/0");
    }

    //X TODO replaceValueInArray
    //X TODO replaceObjectInArray
    //X TODO Object in Array
    //X TODO Array in Object


    @Test
    public void testComplexDiff() {
        // {"a":"xa","b":2,"c":{"d":"xd"},"e":[1,2,3]}
        String jsonA = "{\"a\":\"xa\",\"b\":2,\"c\":{\"d\":\"xd\"},\"e\":[1,2,3]}";

        // {"a":"xa","c":{"d":"xd", "d2":"xd2"},"e":[1,3],"f":"xe"}
        String jsonB = "{\"a\":\"xa\",\"c\":{\"d\":\"xd\", \"d2\":\"xd2\"},\"e\":[1,3],\"f\":\"xe\"}";

        // this results in 5 diff operations:
        // removing b, adding d2, replacing 2 with 3 from e, removing 3 from 3, adding f
        JsonPatch jsonPatch = Json.createDiff(Json.createReader(new StringReader(jsonA)).readObject(),
                Json.createReader(new StringReader(jsonB)).readObject());
        assertNotNull(jsonPatch);
        JsonArray patchOperations = jsonPatch.toJsonArray();
        assertNotNull(patchOperations);
        assertEquals(5, patchOperations.size());
        containsOperation(patchOperations, JsonPatch.Operation.REMOVE, "/b");
        containsOperation(patchOperations, JsonPatch.Operation.ADD, "/c/d2", Json.createValue("xd2"));
        containsOperation(patchOperations, JsonPatch.Operation.REPLACE, "/e/1", Json.createValue(3));
        containsOperation(patchOperations, JsonPatch.Operation.REMOVE, "/e/2");
        containsOperation(patchOperations, JsonPatch.Operation.ADD, "/f", Json.createValue("xe"));
    }

    private void containsOperation(JsonArray patchOperations,
                                   JsonPatch.Operation patchOperation,
                                   String jsonPointer) {

        containsOperation(patchOperations, patchOperation, jsonPointer, null);
    }

    private void containsOperation(JsonArray patchOperations,
                                   JsonPatch.Operation patchOperation,
                                   String jsonPointer,
                                   JsonValue value) {

        for (JsonValue operation : patchOperations) {
            if (operation instanceof JsonObject &&
                    patchOperation.operationName().equalsIgnoreCase(((JsonObject) operation).getString("op")) &&
                    ((JsonObject) operation).getString("path").equals(jsonPointer)) {

                if (value != null) {
                    assertEquals(value, ((JsonObject) operation).get("value"));
                }

                return;
            }
        }
        Assert.fail("patchOperations does not contain " + patchOperation + " " + jsonPointer);
    }


}
