/*
 * Copyright 2016-present Open Networking Foundation
 *
 * 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 org.onosproject.incubator.net.virtual.impl;

import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.onlab.junit.TestUtils;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.osgi.TestServiceDirectory;
import org.onosproject.TestApplicationId;
import org.onosproject.common.event.impl.TestEventDispatcher;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.core.CoreServiceAdapter;
import org.onosproject.core.IdGenerator;
import org.onosproject.net.TenantId;
import org.onosproject.incubator.net.virtual.VirtualDevice;
import org.onosproject.incubator.net.virtual.VirtualLink;
import org.onosproject.incubator.net.virtual.VirtualNetwork;
import org.onosproject.incubator.net.virtual.VirtualNetworkIntent;
import org.onosproject.incubator.net.virtual.VirtualNetworkIntentStore;
import org.onosproject.incubator.net.virtual.VirtualNetworkStore;
import org.onosproject.incubator.net.virtual.store.impl.DistributedVirtualNetworkStore;
import org.onosproject.incubator.net.virtual.store.impl.SimpleVirtualIntentStore;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultPort;
import org.onosproject.net.EncapsulationType;
import org.onosproject.net.Link;
import org.onosproject.net.NetTestTools;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.TestDeviceParams;
import org.onosproject.net.intent.Constraint;
import org.onosproject.net.intent.FakeIntentManager;
import org.onosproject.net.intent.FlowRuleIntent;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.IntentCompiler;
import org.onosproject.net.intent.IntentEvent;
import org.onosproject.net.intent.IntentExtensionService;
import org.onosproject.net.intent.IntentListener;
import org.onosproject.net.intent.IntentService;
import org.onosproject.net.intent.IntentState;
import org.onosproject.net.intent.IntentTestsMocks;
import org.onosproject.net.intent.Key;
import org.onosproject.net.intent.MockIdGenerator;
import org.onosproject.net.intent.PathIntent;
import org.onosproject.net.intent.TestableIntentService;
import org.onosproject.net.intent.WorkPartitionService;
import org.onosproject.net.intent.WorkPartitionServiceAdapter;
import org.onosproject.net.intent.constraint.EncapsulationConstraint;
import org.onosproject.store.service.TestStorageService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Junit tests for VirtualNetworkIntentService.
 */
@Ignore("deprecated prototype implementation")
public class VirtualNetworkIntentManagerTest extends TestDeviceParams {

    private final String tenantIdValue1 = "TENANT_ID1";
    private static final ApplicationId APP_ID =
            new TestApplicationId("MyAppId");

    private ConnectPoint cp1;
    private ConnectPoint cp2;
    private ConnectPoint cp3;
    private ConnectPoint cp4;
    private ConnectPoint cp5;
    private ConnectPoint cp6;
    private VirtualLink link1;
    private VirtualLink link2;
    private VirtualLink link3;
    private VirtualLink link4;
    private VirtualLink link5;
    private VirtualLink link6;

    private VirtualNetworkManager manager;
    private static DistributedVirtualNetworkStore virtualNetworkManagerStore;
    private VirtualNetworkIntentStore intentStore;
    private CoreService coreService;
    private TestableIntentService intentService = new FakeIntentManager();
    private VirtualNetworkIntentManager vnetIntentService;
    private TestIntentCompiler compiler = new TestIntentCompiler();
    private IntentExtensionService intentExtensionService;
    private WorkPartitionService workPartitionService;
    private ServiceDirectory testDirectory;
    private TestListener listener = new TestListener();
    private static final int MAX_WAIT_TIME = 5;
    private static final int MAX_PERMITS = 1;
    private static Semaphore created;
    private static Semaphore withdrawn;
    private static Semaphore purged;

    @Before
    public void setUp() throws Exception {
        virtualNetworkManagerStore = new DistributedVirtualNetworkStore();
        intentStore = new SimpleVirtualIntentStore();

        coreService = new VirtualNetworkIntentManagerTest.TestCoreService();

        MockIdGenerator.cleanBind();

        TestUtils.setField(virtualNetworkManagerStore, "coreService", coreService);
        TestUtils.setField(virtualNetworkManagerStore, "storageService", new TestStorageService());
        virtualNetworkManagerStore.activate();

        manager = new VirtualNetworkManager();
        manager.store = virtualNetworkManagerStore;
        NetTestTools.injectEventDispatcher(manager, new TestEventDispatcher());
        intentService.addListener(listener);

        // Register a compiler and an installer both setup for success.
        intentExtensionService = intentService;
        intentExtensionService.registerCompiler(VirtualNetworkIntent.class, compiler);

        created = new Semaphore(0, true);
        withdrawn = new Semaphore(0, true);
        purged = new Semaphore(0, true);

        workPartitionService = new WorkPartitionServiceAdapter();
        testDirectory = new TestServiceDirectory()
                .add(VirtualNetworkStore.class, virtualNetworkManagerStore)
                .add(IntentService.class, intentService)
                .add(WorkPartitionService.class, workPartitionService);
        TestUtils.setField(manager, "serviceDirectory", testDirectory);

        manager.activate();
    }

    @After
    public void tearDown() {
        virtualNetworkManagerStore.deactivate();
        manager.deactivate();
        NetTestTools.injectEventDispatcher(manager, null);
        MockIdGenerator.unbind();
        intentService.removeListener(listener);
        created = null;
        withdrawn = null;
        purged = null;
    }

    /**
     * Method to create the virtual network for further testing.
     *
     * @return virtual network
     */
    private VirtualNetwork setupVirtualNetworkTopology() {
        manager.registerTenantId(TenantId.tenantId(tenantIdValue1));
        VirtualNetwork virtualNetwork = manager.createVirtualNetwork(TenantId.tenantId(tenantIdValue1));
        VirtualDevice virtualDevice1 =
                manager.createVirtualDevice(virtualNetwork.id(), DID1);
        VirtualDevice virtualDevice2 =
                manager.createVirtualDevice(virtualNetwork.id(), DID2);
        VirtualDevice virtualDevice3 =
                manager.createVirtualDevice(virtualNetwork.id(), DID3);
        VirtualDevice virtualDevice4 =
                manager.createVirtualDevice(virtualNetwork.id(), DID4);

        Port port1 = new DefaultPort(virtualDevice1, PortNumber.portNumber(1), true);
        cp1 = new ConnectPoint(virtualDevice1.id(), port1.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice1.id(), port1.number(), cp1);

        Port port2 = new DefaultPort(virtualDevice1, PortNumber.portNumber(2), true);
        cp2 = new ConnectPoint(virtualDevice1.id(), port2.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice1.id(), port2.number(), cp2);

        Port port3 = new DefaultPort(virtualDevice2, PortNumber.portNumber(3), true);
        cp3 = new ConnectPoint(virtualDevice2.id(), port3.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice2.id(), port3.number(), cp3);

        Port port4 = new DefaultPort(virtualDevice2, PortNumber.portNumber(4), true);
        cp4 = new ConnectPoint(virtualDevice2.id(), port4.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice2.id(), port4.number(), cp4);

        Port port5 = new DefaultPort(virtualDevice3, PortNumber.portNumber(5), true);
        cp5 = new ConnectPoint(virtualDevice3.id(), port5.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice3.id(), port5.number(), cp5);

        Port port6 = new DefaultPort(virtualDevice3, PortNumber.portNumber(6), true);
        cp6 = new ConnectPoint(virtualDevice3.id(), port6.number());
        manager.createVirtualPort(virtualNetwork.id(), virtualDevice3.id(), port6.number(), cp6);

        link1 = manager.createVirtualLink(virtualNetwork.id(), cp1, cp3);
        virtualNetworkManagerStore.updateLink(link1, link1.tunnelId(), Link.State.ACTIVE);
        link2 = manager.createVirtualLink(virtualNetwork.id(), cp3, cp1);
        virtualNetworkManagerStore.updateLink(link2, link2.tunnelId(), Link.State.ACTIVE);
        link3 = manager.createVirtualLink(virtualNetwork.id(), cp4, cp5);
        virtualNetworkManagerStore.updateLink(link3, link3.tunnelId(), Link.State.ACTIVE);
        link4 = manager.createVirtualLink(virtualNetwork.id(), cp5, cp4);
        virtualNetworkManagerStore.updateLink(link4, link4.tunnelId(), Link.State.ACTIVE);

        vnetIntentService = new VirtualNetworkIntentManager(manager, virtualNetwork.id());
        vnetIntentService.intentStore = intentStore;
        return virtualNetwork;
    }

    /**
     * Tests the submit(), withdraw(), and purge() methods.
     */
    @Test
    public void testCreateAndRemoveIntent() {
        VirtualNetwork virtualNetwork = setupVirtualNetworkTopology();

        Key intentKey = Key.of("test", APP_ID);

        List<Constraint> constraints = new ArrayList<>();
        constraints.add(new EncapsulationConstraint(EncapsulationType.VLAN));

        VirtualNetworkIntent virtualIntent = VirtualNetworkIntent.builder()
                .networkId(virtualNetwork.id())
                .key(intentKey)
                .appId(APP_ID)
                .ingressPoint(cp1)
                .egressPoint(cp5)
                .constraints(constraints)
                .build();
        // Test the submit() method.
        vnetIntentService.submit(virtualIntent);

        // Wait for the both intents to go into an INSTALLED state.
        try {
            if (!created.tryAcquire(MAX_PERMITS, MAX_WAIT_TIME, TimeUnit.SECONDS)) {
                fail("Failed to wait for intent to get installed.");
            }
        } catch (InterruptedException e) {
            fail("Semaphore exception during intent installation." + e.getMessage());
        }

        // Test the getIntentState() method
        assertEquals("The intent state did not match as expected.", IntentState.INSTALLED,
                     vnetIntentService.getIntentState(virtualIntent.key()));

        // Test the withdraw() method.
        vnetIntentService.withdraw(virtualIntent);
        // Wait for the both intents to go into a WITHDRAWN state.
        try {
            if (!withdrawn.tryAcquire(MAX_PERMITS, MAX_WAIT_TIME, TimeUnit.SECONDS)) {
                fail("Failed to wait for intent to get withdrawn.");
            }
        } catch (InterruptedException e) {
            fail("Semaphore exception during intent withdrawal." + e.getMessage());
        }

        // Test the getIntentState() method
        assertEquals("The intent state did not match as expected.", IntentState.WITHDRAWN,
                     vnetIntentService.getIntentState(virtualIntent.key()));

        // Test the purge() method.
        vnetIntentService.purge(virtualIntent);
        // Wait for the both intents to be removed/purged.
        try {
            if (!purged.tryAcquire(MAX_PERMITS, MAX_WAIT_TIME, TimeUnit.SECONDS)) {
                fail("Failed to wait for intent to get purged.");
            }
        } catch (InterruptedException e) {
            fail("Semaphore exception during intent purging." + e.getMessage());
        }

    }

    /**
     * Tests the getIntents, getIntent(), getIntentData(), getIntentCount(),
     * isLocal() methods.
     */
    @Test
    public void testGetIntents() {
        VirtualNetwork virtualNetwork = setupVirtualNetworkTopology();

        Key intentKey = Key.of("test", APP_ID);

        List<Constraint> constraints = new ArrayList<>();
        constraints.add(new EncapsulationConstraint(EncapsulationType.VLAN));

        VirtualNetworkIntent virtualIntent = VirtualNetworkIntent.builder()
                .networkId(virtualNetwork.id())
                .key(intentKey)
                .appId(APP_ID)
                .ingressPoint(cp1)
                .egressPoint(cp5)
                .constraints(constraints)
                .build();
        // Test the submit() method.
        vnetIntentService.submit(virtualIntent);

        // Wait for the both intents to go into an INSTALLED state.
        try {
            if (!created.tryAcquire(MAX_PERMITS, MAX_WAIT_TIME, TimeUnit.SECONDS)) {
                fail("Failed to wait for intent to get installed.");
            }
        } catch (InterruptedException e) {
            fail("Semaphore exception during intent installation." + e.getMessage());
        }

        // Test the getIntents() method
        assertEquals("The intents size did not match as expected.", 1,
                     Iterators.size(vnetIntentService.getIntents().iterator()));

        // Test the getIntent() method
        assertNotNull("The intent should have been found.", vnetIntentService.getIntent(virtualIntent.key()));

        // Test the getIntentData() method
        assertEquals("The intent data size did not match as expected.", 1,
                     Iterators.size(vnetIntentService.getIntentData().iterator()));

        // Test the getIntentCount() method
        assertEquals("The intent count did not match as expected.", 1,
                     vnetIntentService.getIntentCount());

        // Test the isLocal() method
        assertTrue("The intent should be local.", vnetIntentService.isLocal(virtualIntent.key()));

    }

    /**
     * Test listener to listen for intent events.
     */
    private static class TestListener implements IntentListener {

        @Override
        public void event(IntentEvent event) {
            switch (event.type()) {
                case INSTALLED:
                    // Release one permit on the created semaphore since the Intent event was received.
//                    virtualNetworkManagerStore.addOrUpdateIntent(event.subject(), IntentState.INSTALLED);
                    created.release();
                    break;
                case WITHDRAWN:
                    // Release one permit on the removed semaphore since the Intent event was received.
//                    virtualNetworkManagerStore.addOrUpdateIntent(event.subject(), IntentState.WITHDRAWN);
                    withdrawn.release();
                    break;
                case PURGED:
                    // Release one permit on the purged semaphore since the Intent event was received.
                    purged.release();
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * Core service test class.
     */
    private class TestCoreService extends CoreServiceAdapter {

        @Override
        public IdGenerator getIdGenerator(String topic) {
            return new IdGenerator() {
                private AtomicLong counter = new AtomicLong(0);

                @Override
                public long getNewId() {
                    return counter.getAndIncrement();
                }
            };
        }
    }

    private static class TestIntentCompiler implements IntentCompiler<VirtualNetworkIntent> {
        @Override
        public List<Intent> compile(VirtualNetworkIntent intent, List<Intent> installable) {
            return Lists.newArrayList(new MockInstallableIntent());
        }
    }

    private static class MockInstallableIntent extends FlowRuleIntent {

        public MockInstallableIntent() {
            super(APP_ID, null, Collections.singletonList(new IntentTestsMocks.MockFlowRule(100)),
                    Collections.emptyList(), PathIntent.ProtectionType.PRIMARY, null);
        }
    }

//    private void addOrUpdateIntent(Intent intent, IntentState state) {
//        checkNotNull(intent, "Intent cannot be null");
//        IntentData intentData = intentStore.(intent.key());
//        if (intentData == null) {
//            intentData = new IntentData(intent, state, new WallClockTimestamp(System.currentTimeMillis()));
//        } else {
//            intentData = new IntentData(intent, state, intentData.version());
//        }
//        intentKeyIntentDataMap.put(intent.key(), intentData);
//    }
}
