/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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
 *
 *      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 com.android.car.vms;

import static org.junit.Assert.assertTrue;

import android.car.vms.VmsLayer;
import android.car.vms.VmsLayerDependency;
import android.car.vms.VmsLayersOffering;
import android.car.vms.VmsOperationRecorder;
import android.util.Log;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Arrays;
import java.util.HashSet;

@RunWith(AndroidJUnit4.class)
@MediumTest
public class VmsOperationRecorderTest {

    /**
     * Capture messages that VmsOperationRecorder.Writer would normally pass to Log.d(...).
     */
    private static final class TestWriter extends VmsOperationRecorder.Writer {
        public String mMsg;

        @Override
        public boolean isEnabled() {
            return true;
        }

        @Override
        public void write(String msg) {
            super.write(msg);
            mMsg = msg;
        }
    }

    private TestWriter mWriter;
    private VmsOperationRecorder mRecorder;
    private static final String TAG = "VmsOperationRecorderTest";

    private static final VmsLayer layer1 = new VmsLayer(1, 3, 2);
    private static final VmsLayer layer2 = new VmsLayer(2, 4, 3);
    private static final VmsLayer layer3 = new VmsLayer(3, 5, 4);

    private static final VmsLayerDependency layerDependency1 = new VmsLayerDependency(layer3);
    private static final VmsLayerDependency layerDependency2 = new VmsLayerDependency(layer1,
            new HashSet<VmsLayer>(Arrays.asList(layer2, layer3)));

    private static final VmsLayersOffering layersOffering0 = new VmsLayersOffering(
            new HashSet<VmsLayerDependency>(), 66);
    private static final VmsLayersOffering layersOffering1 = new VmsLayersOffering(
            new HashSet<>(Arrays.asList(layerDependency1)), 66);
    private static final VmsLayersOffering layersOffering2 = new VmsLayersOffering(
            new HashSet<>(Arrays.asList(layerDependency1, layerDependency2)), 66);

    @Before
    public void setUp() {
        mWriter = new TestWriter();
        mRecorder = new VmsOperationRecorder(mWriter);
    }

    @Test
    public void testSubscribe() throws Exception {
        mRecorder.subscribe(layer1);
        assertJsonMsgEquals("{'subscribe':{'layer':{'subtype':3,'type':1,'version':2}}}");
    }

    @Test
    public void testUnsubscribe() throws Exception {
        mRecorder.unsubscribe(layer1);
        assertJsonMsgEquals("{'unsubscribe':{'layer':{'type':1,'subtype':3,'version':2}}}");
    }

    @Test
    public void testStartMonitoring() throws Exception {
        mRecorder.startMonitoring();
        assertJsonMsgEquals("{'startMonitoring':{}}");
    }

    @Test
    public void testStopMonitoring() throws Exception {
        mRecorder.stopMonitoring();
        assertJsonMsgEquals("{'stopMonitoring':{}}");
    }

    @Test
    public void testSetLayersOffering0() throws Exception {
        mRecorder.setLayersOffering(layersOffering0);
        assertJsonMsgEquals("{'setLayersOffering':{'publisherId':66}}");
    }

    @Test
    public void testSetLayersOffering2() throws Exception {
        mRecorder.setLayersOffering(layersOffering2);
        assertJsonMsgEquals("{'setLayersOffering':{'publisherId':66,'layerDependency':["
                + "{'layer':{'type':3,'subtype':5,'version':4}},"
                + "{'layer':{'type':1,'subtype':3,'version':2},'dependency':["
                + "{'type':2,'subtype':4,'version':3},{'type':3,'subtype':5,'version':4}]}"
                + "]}}");
    }

    @Test
    public void testGetPublisherId() throws Exception {
        mRecorder.getPublisherId(9);
        assertJsonMsgEquals("{'getPublisherId':{'publisherId':9}}");
    }

    @Test
    public void testAddSubscription() throws Exception {
        mRecorder.addSubscription(42, layer1);
        assertJsonMsgEquals(
                "{'addSubscription':{'sequenceNumber':42,'layer':{'type':1,'subtype':3,'version':2}}}"
        );
    }

    @Test
    public void testRemoveSubscription() throws Exception {
        mRecorder.removeSubscription(42, layer1);
        assertJsonMsgEquals("{'removeSubscription':"
                + "{'sequenceNumber':42,'layer':{'type':1,'subtype':3,'version':2}}}");
    }

    @Test
    public void testAddPromiscuousSubscription() throws Exception {
        mRecorder.addPromiscuousSubscription(42);
        assertJsonMsgEquals("{'addPromiscuousSubscription':{'sequenceNumber':42}}");
    }

    @Test
    public void testRemovePromiscuousSubscription() throws Exception {
        mRecorder.removePromiscuousSubscription(42);
        assertJsonMsgEquals("{'removePromiscuousSubscription':{'sequenceNumber':42}}");
    }

    @Test
    public void testAddHalSubscription() throws Exception {
        mRecorder.addHalSubscription(42, layer1);
        assertJsonMsgEquals("{'addHalSubscription':"
                + "{'sequenceNumber':42,'layer':{'type':1,'subtype':3,'version':2}}}");
    }

    @Test
    public void testRemoveHalSubscription() throws Exception {
        mRecorder.removeHalSubscription(42, layer1);
        assertJsonMsgEquals("{'removeHalSubscription':"
                + "{'sequenceNumber':42,'layer':{'type':1,'subtype':3,'version':2}}}");
    }

    @Test
    public void testSetPublisherLayersOffering() throws Exception {
        mRecorder.setPublisherLayersOffering(layersOffering1);
        assertJsonMsgEquals("{'setPublisherLayersOffering':{'publisherId':66,'layerDependency':["
                + "{'layer':{'type':3,'subtype':5,'version':4}}]}}");
    }

    @Test public void testSetHalPublisherLayersOffering() throws Exception {
        mRecorder.setHalPublisherLayersOffering(layersOffering1);
        assertJsonMsgEquals("{'setHalPublisherLayersOffering':{'publisherId':66,'layerDependency':["
                + "{'layer':{'type':3,'subtype':5,'version':4}}]}}");
    }

    @Test
    public void testSubscribeToPublisher() throws Exception {
        mRecorder.subscribe(layer1, 99);
        assertJsonMsgEquals(
                "{'subscribe':{'publisherId':99, 'layer':{'type':1,'subtype':3,'version':2}}}");
    }

    @Test
    public void testUnsubscribeToPublisher() throws Exception {
        mRecorder.unsubscribe(layer1, 99);
        assertJsonMsgEquals(
                "{'unsubscribe':{'publisherId':99, 'layer':{'type':1,'subtype':3,'version':2}}}}");
    }

    private void assertJsonMsgEquals(String expectJson) throws Exception {
        // Escaping double quotes in a JSON string is really noisy. The test data uses single
        // quotes instead, which gets replaced here.
        JSONObject expect = new JSONObject(expectJson.replace("'", "\""));
        JSONObject got = new JSONObject(mWriter.mMsg);
        assertTrue(similar(expect, got));
    }

    /*
     * Determine if two JSONObjects are similar.
     * They must contain the same set of names which must be associated with
     * similar values.
     */
    private boolean similar(JSONObject expect, JSONObject got) {
        try {
            if (!expect.keySet().equals(got.keySet())) {
                return false;
            }

            for (String key : expect.keySet()) {
                Object valueExpect = expect.get(key);
                Object valueGot = got.get(key);

                if (valueExpect == valueGot) {
                    continue;
                }

                if (valueExpect == null) {
                    return false;
                }

                if (valueExpect instanceof JSONObject) {
                    return similar((JSONObject) valueExpect, (JSONObject) valueGot);
                } else if (valueExpect instanceof JSONArray) {
                    // Equal JSONArray have the same length and one contains the other.
                    JSONArray expectArray = (JSONArray) valueExpect;
                    JSONArray gotArray = (JSONArray) valueGot;

                    if (expectArray.length() != gotArray.length()) {
                        return false;
                    }

                    for (int i = 0; i < expectArray.length(); i++) {
                        boolean gotContainsSimilar = false;
                        for (int j = 0; j < gotArray.length(); j++) {
                            if (similar((JSONObject) expectArray.get(i),
                                    (JSONObject) gotArray.get(j))) {
                                gotContainsSimilar = true;
                                break;
                            }
                        }
                        if (!gotContainsSimilar) {
                            return false;
                        }
                    }
                } else if (!valueExpect.equals(valueGot)) {
                    return false;
                }
            }

        } catch (JSONException e) {
            Log.d(TAG, "Could not compare JSONObjects: " + e);
            return false;
        }
        return true;
    }
}
