/*
 * Copyright 2021-2023 Open Networking Foundation (ONF) and the ONF Contributors
 *
 * 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.opencord.olt.impl;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.onlab.packet.ChassisId;
import org.onlab.packet.EthType;
import org.onlab.packet.IPv4;
import org.onlab.packet.IPv6;
import org.onlab.packet.MacAddress;
import org.onlab.packet.TpPort;
import org.onlab.packet.VlanId;
import org.onosproject.cfg.ComponentConfigAdapter;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreServiceAdapter;
import org.onosproject.core.DefaultApplicationId;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.DefaultDevice;
import org.onosproject.net.DefaultHost;
import org.onosproject.net.DefaultPort;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.DefaultFlowRule;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.FlowRule;
import org.onosproject.net.flow.FlowRuleEvent;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.flow.criteria.Criteria;
import org.onosproject.net.flowobjective.DefaultFilteringObjective;
import org.onosproject.net.flowobjective.DefaultForwardingObjective;
import org.onosproject.net.flowobjective.FilteringObjective;
import org.onosproject.net.flowobjective.FlowObjectiveService;
import org.onosproject.net.flowobjective.ForwardingObjective;
import org.onosproject.net.host.HostService;
import org.onosproject.net.meter.MeterId;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.store.service.TestStorageService;
import org.opencord.olt.AccessDevicePort;
import org.opencord.olt.DiscoveredSubscriber;
import org.opencord.olt.OltPortStatus;
import org.opencord.olt.ServiceKey;
import org.opencord.olt.FlowOperation;
import org.opencord.olt.impl.fttb.FttbUtils;
import org.opencord.sadis.BaseInformationService;
import org.opencord.sadis.SadisService;
import org.opencord.sadis.SubscriberAndDeviceInformation;
import org.opencord.sadis.UniTagInformation;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.onosproject.net.AnnotationKeys.PORT_NAME;
import static org.opencord.olt.OltFlowsStatus.ERROR;
import static org.opencord.olt.OltFlowsStatus.NONE;
import static org.opencord.olt.OltFlowsStatus.ADDED;
import static org.opencord.olt.OltFlowsStatus.PENDING_ADD;
import static org.opencord.olt.OltFlowsStatus.PENDING_REMOVE;
import static org.opencord.olt.OltFlowsStatus.REMOVED;
import static org.opencord.olt.impl.OsgiPropertyConstants.DEFAULT_BP_ID_DEFAULT;
import static org.opencord.olt.impl.OsgiPropertyConstants.DEFAULT_MCAST_SERVICE_NAME;
import static org.opencord.olt.impl.OsgiPropertyConstants.DEFAULT_MCAST_SERVICE_NAME_DEFAULT;
import static org.opencord.olt.impl.OsgiPropertyConstants.DOWNSTREAM_OLT;
import static org.opencord.olt.impl.OsgiPropertyConstants.DOWNSTREAM_ONU;
import static org.opencord.olt.impl.OsgiPropertyConstants.UPSTREAM_OLT;
import static org.opencord.olt.impl.OsgiPropertyConstants.UPSTREAM_ONU;
import static org.opencord.olt.impl.fttb.FttbUtils.FTTB_FLOW_DIRECTION;
import static org.opencord.olt.impl.fttb.FttbUtils.FTTB_FLOW_DOWNSTREAM;
import static org.opencord.olt.impl.fttb.FttbUtils.FTTB_FLOW_UPSTREAM;
import static org.opencord.olt.impl.fttb.FttbUtils.FTTB_SERVICE_NAME;

public class OltFlowServiceTest extends OltTestHelpers {

    public static final String PORT_1 = "port-1";
    public static final String PORT_2 = "port-2";
    public static final String PORT_3 = "port-3";
    private OltFlowService component;
    private OltFlowService oltFlowService;

    private DeviceService deviceService;
    OltFlowService.InternalFlowListener internalFlowListener;
    private final ApplicationId testAppId = new DefaultApplicationId(1, "org.opencord.olt.test");
    private final short eapolDefaultVlan = 4091;

    private final DeviceId deviceId = DeviceId.deviceId("test-device");
    private final Device testDevice = new DefaultDevice(ProviderId.NONE, deviceId, Device.Type.OLT,
            "testManufacturer", "1.0", "1.0", "SN", new ChassisId(1));
    Port nniPort = new OltPort(testDevice, true, PortNumber.portNumber(1048576),
            DefaultAnnotations.builder().set(PORT_NAME, "nni-1").build());
    Port nniPortDisabled = new OltPort(testDevice, false, PortNumber.portNumber(1048576),
            DefaultAnnotations.builder().set(PORT_NAME, "nni-1").build());
    Port uniUpdateEnabled = new OltPort(testDevice, true, PortNumber.portNumber(16),
            DefaultAnnotations.builder().set(PORT_NAME, "uni-1").build());

    private final UniTagInformation dpuMgmtUti = new UniTagInformation.Builder()
            .setPonCTag(VlanId.vlanId("6"))
            .setPonSTag(VlanId.vlanId("60"))
            .setUsPonCTagPriority(1)
            .setUsPonSTagPriority(2)
            .setTechnologyProfileId(64)
            .setUpstreamBandwidthProfile("usBp")
            .setUpstreamOltBandwidthProfile("usOltBp")
            .setServiceName(FttbUtils.FTTB_SERVICE_DPU_MGMT_TRAFFIC)
            .setIsDhcpRequired(true)
            .setEnableMacLearning(true)
            .build();

    private final UniTagInformation ancpUti = new UniTagInformation.Builder()
            .setPonCTag(VlanId.vlanId("4"))
            .setPonSTag(VlanId.vlanId("40"))
            .setUsPonCTagPriority(3)
            .setUsPonSTagPriority(4)
            .setTechnologyProfileId(64)
            .setUpstreamBandwidthProfile("usBp")
            .setUpstreamOltBandwidthProfile("usOltBp")
            .setServiceName(FttbUtils.FTTB_SERVICE_DPU_ANCP_TRAFFIC)
            .setIsDhcpRequired(false)
            .build();

    private final UniTagInformation fttbSubscriberUti = new UniTagInformation.Builder()
            .setPonCTag(VlanId.vlanId("8"))
            .setPonSTag(VlanId.vlanId("80"))
            .setTechnologyProfileId(64)
            .setUpstreamBandwidthProfile("usBp")
            .setUpstreamOltBandwidthProfile("usOltBp")
            .setServiceName(FttbUtils.FTTB_SERVICE_SUBSCRIBER_TRAFFIC)
            .setIsDhcpRequired(false)
            .build();


    @Before
    public void setUp() {
        component = new OltFlowService();
        component.cfgService = new ComponentConfigAdapter();
        component.sadisService = Mockito.mock(SadisService.class);
        component.coreService = Mockito.spy(new CoreServiceAdapter());
        component.oltMeterService = Mockito.mock(OltMeterService.class);
        component.flowObjectiveService = Mockito.mock(FlowObjectiveService.class);
        component.hostService = Mockito.mock(HostService.class);
        component.flowRuleService = Mockito.mock(FlowRuleService.class);
        component.storageService = new TestStorageService();
        component.oltDeviceService = Mockito.mock(OltDeviceService.class);
        component.appId = testAppId;
        component.deviceService = Mockito.mock(DeviceService.class);

        doReturn(Mockito.mock(BaseInformationService.class))
                .when(component.sadisService).getSubscriberInfoService();
        doReturn(testAppId).when(component.coreService).registerApplication("org.opencord.olt");
        doReturn(testDevice).when(component.deviceService).getDevice(testDevice.id());
        when(component.sadisService.getSubscriberInfoService().get(testDevice.serialNumber())).
                thenReturn(Mockito.mock(SubscriberAndDeviceInformation.class));
        when(component.oltDeviceService.getNniPort(testDevice)).thenReturn(Optional.of(nniPort));

        component.activate(null);
        component.bindSadisService(component.sadisService);

        internalFlowListener = spy(component.internalFlowListener);

        oltFlowService = spy(component);
        deviceService = Mockito.mock(DeviceService.class);
    }

    @After
    public void tearDown() {
        component.deactivate(null);
    }

    @Test
    public void testUpdateConnectPointStatus() {

        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");
        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);
        Port port1 = new DefaultPort(device, PortNumber.portNumber(1), true,
                DefaultAnnotations.builder().set(PORT_NAME, PORT_1).build());
        Port port2 = new DefaultPort(device, PortNumber.portNumber(2), true,
                DefaultAnnotations.builder().set(PORT_NAME, PORT_2).build());
        Port port3 = new DefaultPort(device, PortNumber.portNumber(3), true,
                DefaultAnnotations.builder().set(PORT_NAME, PORT_3).build());

        ServiceKey sk1 = new ServiceKey(new AccessDevicePort(port1), new UniTagInformation());
        ServiceKey sk2 = new ServiceKey(new AccessDevicePort(port2), new UniTagInformation());
        ServiceKey sk3 = new ServiceKey(new AccessDevicePort(port3), new UniTagInformation());

        // cpStatus map for the test
        component.cpStatus = component.storageService.
                <ServiceKey, OltPortStatus>consistentMapBuilder().build().asJavaMap();
        OltPortStatus cp1Status = new OltPortStatus(PENDING_ADD, NONE, NONE, NONE, NONE);
        component.cpStatus.put(sk1, cp1Status);

        //check that we only update the provided value
        component.updateConnectPointStatus(sk1, ADDED, null, null, null, null);
        OltPortStatus updated = component.cpStatus.get(sk1);
        Assert.assertEquals(ADDED, updated.defaultEapolStatus);
        Assert.assertEquals(NONE, updated.subscriberFlowsStatus);
        Assert.assertEquals(NONE, updated.dhcpStatus);

        // check that it creates an entry if it does not exist
        component.updateConnectPointStatus(sk2, PENDING_ADD, NONE, NONE, NONE, NONE);
        Assert.assertNotNull(component.cpStatus.get(sk2));

        // check that if we create a new entry with null values they're converted to NONE
        component.updateConnectPointStatus(sk3, null, null, null, null, null);
        updated = component.cpStatus.get(sk3);
        Assert.assertEquals(NONE, updated.defaultEapolStatus);
        Assert.assertEquals(NONE, updated.subscriberFlowsStatus);
        Assert.assertEquals(NONE, updated.dhcpStatus);
    }

    /**
     * If the flow status is PENDING_REMOVE or ERROR and there is no
     * previous state in the map that don't update it.
     * In case of a device disconnection we immediately wipe out the status,
     * but then flows might update the cpStatus map. That result
     */
    @Test
    public void doNotUpdateConnectPointStatus() {
        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");
        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);
        Port port1 = new DefaultPort(device, PortNumber.portNumber(1), true,
                DefaultAnnotations.builder().set(PORT_NAME, PORT_1).build());

        ServiceKey sk1 = new ServiceKey(new AccessDevicePort(port1), new UniTagInformation());

        // cpStatus map for the test
        component.cpStatus = component.storageService.
                <ServiceKey, OltPortStatus>consistentMapBuilder().build().asJavaMap();

        // check that an entry is not created if the only status is pending remove
        component.updateConnectPointStatus(sk1, null, null, null, PENDING_REMOVE, null);
        OltPortStatus entry = component.cpStatus.get(sk1);
        Assert.assertNull(entry);

        // check that an entry is not created if the only status is ERROR
        component.updateConnectPointStatus(sk1, null, null, null, ERROR, null);
        entry = component.cpStatus.get(sk1);
        Assert.assertNull(entry);
    }

    @Test
    public void testHasDefaultEapol() {
        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");

        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);

        Port port = new DefaultPort(device, PortNumber.portNumber(16), true,
                                    DefaultAnnotations.builder().set(PORT_NAME, "name-1").build());
        ServiceKey skWithStatus = new ServiceKey(new AccessDevicePort(port),
                component.defaultEapolUniTag);

        Port port17 = new DefaultPort(device, PortNumber.portNumber(17), true,
                                      DefaultAnnotations.builder().set(PORT_NAME, "name-1").build());

        OltPortStatus portStatusAdded = new OltPortStatus(
                ADDED,
                NONE,
                null,
                null,
                null
        );

        OltPortStatus portStatusRemoved = new OltPortStatus(
                REMOVED,
                NONE,
                null,
                null,
                null
        );

        component.cpStatus.put(skWithStatus, portStatusAdded);
        Assert.assertTrue(component.hasDefaultEapol(port));

        component.cpStatus.put(skWithStatus, portStatusRemoved);

        Assert.assertFalse(component.hasDefaultEapol(port17));
    }

    @Test
    public void testHasSubscriberFlows() {
        // TODO test with multiple services
        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");

        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);

        Port port = new DefaultPort(device, PortNumber.portNumber(16), true,
                DefaultAnnotations.builder().set(PORT_NAME, "name-1").build());

        UniTagInformation uti = new UniTagInformation.Builder().setServiceName("test").build();
        ServiceKey skWithStatus = new ServiceKey(new AccessDevicePort(port),
                uti);

        OltPortStatus withDefaultEapol = new OltPortStatus(
                ADDED,
                NONE,
                NONE,
                NONE,
                NONE
        );

        OltPortStatus withDhcp = new OltPortStatus(
                REMOVED,
                NONE,
                NONE,
                ADDED,
                NONE
        );

        OltPortStatus withSubFlow = new OltPortStatus(
                REMOVED,
                ADDED,
                ADDED,
                ADDED,
                NONE
        );

        component.cpStatus.put(skWithStatus, withDefaultEapol);
        Assert.assertFalse(component.hasSubscriberFlows(port, uti));

        component.cpStatus.put(skWithStatus, withDhcp);
        Assert.assertTrue(component.hasDhcpFlows(port, uti));

        component.cpStatus.put(skWithStatus, withSubFlow);
        Assert.assertTrue(component.hasSubscriberFlows(port, uti));
    }

    @Test
    public void testHandleBasicPortFlowsNoEapol() throws Exception {
        component.enableEapol = false;
        // create empty service for testing
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation empty = new UniTagInformation.Builder().build();
        uniTagInformationList.add(empty);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                                         uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                                         false, si);
        component.handleBasicPortFlows(addedSub, DEFAULT_BP_ID_DEFAULT, DEFAULT_BP_ID_DEFAULT);
        // if eapol is not enabled there's nothing we need to do,
        // so make sure we don't even call sadis
        verify(component.subsService, never()).get(any());
    }

    @Test
    public void testHandleBasicPortFlowsWithEapolNoMeter() throws Exception {
        // create empty service for testing
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation empty = new UniTagInformation.Builder().build();
        uniTagInformationList.add(empty);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);
        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                                         uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                                         false, si);
        // whether the meter is pending or not is up to the createMeter method to handle
        // we just don't proceed with the subscriber till it's ready
        doReturn(false).when(component.oltMeterService)
                .createMeter(addedSub.device.id(), DEFAULT_BP_ID_DEFAULT);
        boolean res = component.handleBasicPortFlows(addedSub, DEFAULT_BP_ID_DEFAULT, DEFAULT_BP_ID_DEFAULT);

        Assert.assertFalse(res);

        // we do not create flows
        verify(component.flowObjectiveService, never())
                .filter(eq(addedSub.device.id()), any());
    }

    @Test
    public void testHandleBasicPortFlowsWithEapolAddedMeter() throws Exception {
        // create empty service for testing
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation empty = new UniTagInformation.Builder().build();
        uniTagInformationList.add(empty);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);
        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                                         uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                                         false, si);
        // this is the happy case, we have the meter so we check that the default EAPOL flow
        // is installed
        doReturn(true).when(component.oltMeterService)
                .createMeter(deviceId, DEFAULT_BP_ID_DEFAULT);
        doReturn(true).when(component.oltMeterService)
                .hasMeterByBandwidthProfile(deviceId, DEFAULT_BP_ID_DEFAULT);
        doReturn(MeterId.meterId(1)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(deviceId, DEFAULT_BP_ID_DEFAULT);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(uniUpdateEnabled.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.EAPOL.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(
                        DefaultTrafficTreatment.builder()
                                .meter(MeterId.meterId(1))
                                .writeMetadata(OltFlowServiceUtils.createTechProfValueForWriteMetadata(
                                        VlanId.NONE,
                                        component.defaultTechProfileId, MeterId.meterId(1)), 0)
                                .setOutput(PortNumber.CONTROLLER)
                                .pushVlan()
                                .setVlanId(VlanId.vlanId(eapolDefaultVlan)).build()
                )
                .add();


        component.handleBasicPortFlows(addedSub, DEFAULT_BP_ID_DEFAULT, DEFAULT_BP_ID_DEFAULT);

        // we check for an existing meter (present)
        // FIXME understand why the above test invokes this call and this one doesn't
//        verify(oltFlowService.oltMeterService, times(1))
//                .hasMeterByBandwidthProfile(eq(addedSub.device.id()), eq(DEFAULT_BP_ID_DEFAULT));

        // the meter exist, no need to check for PENDING or to create it
        verify(component.oltMeterService, never())
                .hasPendingMeterByBandwidthProfile(eq(deviceId), eq(DEFAULT_BP_ID_DEFAULT));
        verify(component.oltMeterService, never())
                .createMeterForBp(eq(deviceId), eq(DEFAULT_BP_ID_DEFAULT));

        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testHandleBasicPortFlowsRemovedSub() throws Exception {
        // create empty service for testing
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation empty = new UniTagInformation.Builder().build();
        uniTagInformationList.add(empty);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);
        final DiscoveredSubscriber removedSub =
                new DiscoveredSubscriber(testDevice,
                                         uniUpdateEnabled, DiscoveredSubscriber.Status.REMOVED,
                                         false, si);
        // we are testing that when a port goes down we remove the default EAPOL flow

        doReturn(MeterId.meterId(1)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(deviceId, DEFAULT_BP_ID_DEFAULT);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .deny()
                .withKey(Criteria.matchInPort(uniUpdateEnabled.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.EAPOL.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(
                        DefaultTrafficTreatment.builder()
                                .meter(MeterId.meterId(1))
                                .writeMetadata(OltFlowServiceUtils.createTechProfValueForWriteMetadata(
                                        VlanId.NONE,
                                        component.defaultTechProfileId, MeterId.meterId(1)), 0)
                                .setOutput(PortNumber.CONTROLLER)
                                .pushVlan()
                                .setVlanId(VlanId.vlanId(eapolDefaultVlan)).build()
                )
                .add();

        component.handleBasicPortFlows(removedSub, DEFAULT_BP_ID_DEFAULT, DEFAULT_BP_ID_DEFAULT);

        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testHandleNniFlowsOnlyLldp() {
        component.enableDhcpOnNni = false;
        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.LLDP.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testHandleNniFlowsDhcpV4() {
        component.enableDhcpOnNni = true;
        component.enableDhcpV4 = true;
        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(67)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(68)))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct DHCP filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testRemoveNniFlowsDhcpV4() {
        component.enableDhcpOnNni = true;
        component.enableDhcpV4 = true;
        component.handleNniFlows(testDevice, nniPortDisabled, FlowOperation.REMOVE);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .deny()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(67)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(68)))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct DHCP filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testHandleNniFlowsDhcpV6() {
        component.enableDhcpOnNni = true;
        component.enableDhcpV4 = false;
        component.enableDhcpV6 = true;
        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV6.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv6.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(546)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(547)))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct DHCP filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testHandleNniFlowsIgmp() {
        component.enableDhcpOnNni = false;
        component.enableIgmpOnNni = true;
        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_IGMP))
                .fromApp(testAppId)
                .withPriority(10000)
                .add();

        // invoked with the correct DHCP filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testHandleNniFlowsPppoe() {
        component.enableDhcpOnNni = false;
        component.enablePppoeOnNni = true;
        component.enablePppoe = true;
        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.PPPoED.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct Pppoe filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testRemoveNniFlowsPppoe() {
        component.enableDhcpOnNni = false;
        component.enablePppoeOnNni = true;
        component.enablePppoe = true;
        component.handleNniFlows(testDevice, nniPortDisabled, FlowOperation.REMOVE);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .deny()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.PPPoED.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct Pppoe filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testMacAddressNotRequired() {
        // create a single service that doesn't require mac address
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setEnableMacLearning(false)
                .build();
        uniTagInformationList.add(hsia);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        boolean isMacAvailable = component.isMacAddressAvailable(testDevice.id(), uniUpdateEnabled, si);
        // we return true as we don't care wether it's available or not
        Assert.assertTrue(isMacAvailable);
    }

    @Test
    public void testIsMacAddressAvailableViaMacLearning() {

        // create a single service that requires macLearning to be enabled
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        VlanId hsiaCtag = VlanId.vlanId((short) 11);
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setPonCTag(hsiaCtag)
                .setEnableMacLearning(true).build();
        uniTagInformationList.add(hsia);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        // with no hosts discovered, return false
        boolean isMacAvailable = component.isMacAddressAvailable(testDevice.id(), uniUpdateEnabled, si);
        Assert.assertFalse(isMacAvailable);

        // with a discovered host, return true
        Host fakeHost = new DefaultHost(ProviderId.NONE, HostId.hostId(MacAddress.NONE), MacAddress.ZERO,
                hsiaCtag, HostLocation.NONE, new HashSet<>(), DefaultAnnotations.builder().build());
        Set<Host> hosts = new HashSet<>(Arrays.asList(fakeHost));
        doReturn(hosts).when(component.hostService).getConnectedHosts((ConnectPoint) any());

        isMacAvailable = component.isMacAddressAvailable(testDevice.id(), uniUpdateEnabled, si);
        Assert.assertTrue(isMacAvailable);
    }

    @Test
    public void testIsMacAddressAvailableViaConfiguration() {
        // create a single service that has a macAddress configured
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setConfiguredMacAddress("2e:0a:00:01:00:00")
                .build();
        uniTagInformationList.add(hsia);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        boolean isMacAvailable = component.isMacAddressAvailable(testDevice.id(), uniUpdateEnabled, si);
        Assert.assertTrue(isMacAvailable);
    }

    @Test
    public void testHandleSubscriberDhcpFlowsAdd() {

        String usBp = "usBp";
        String usOltBp = "usOltBp";
        component.enableDhcpV4 = true;

        // create two services, one requires DHCP the other doesn't
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        VlanId hsiaCtag = VlanId.vlanId((short) 11);
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setPonCTag(hsiaCtag)
                .setTechnologyProfileId(64)
                .setUniTagMatch(VlanId.vlanId(VlanId.NO_VID))
                .setUpstreamBandwidthProfile(usBp)
                .setUpstreamOltBandwidthProfile(usOltBp)
                .setIsDhcpRequired(true).build();
        UniTagInformation mc = new UniTagInformation.Builder()
                .setIsDhcpRequired(false).build();
        uniTagInformationList.add(hsia);
        uniTagInformationList.add(mc);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                                         uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                                         false, si);

        // return meter IDs
        doReturn(MeterId.meterId(2)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), usBp);
        doReturn(MeterId.meterId(3)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), usOltBp);

        // TODO improve the matches on the filter
        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(addedSub.port.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(68)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(67)))
                .fromApp(testAppId)
                .withPriority(10000)
                .add();

        component.handleSubscriberDhcpFlows(addedSub.device.id(), addedSub.port,
                FlowOperation.ADD, si);
        verify(component.flowObjectiveService, times(1))
                .filter(eq(addedSub.device.id()), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testHandleSubscriberPppoeFlowsAdd() {

        String usBp = "usBp";
        String usOltBp = "usOltBp";
        component.enablePppoe = true;

        // create two services, one requires Pppoe the other doesn't
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        VlanId hsiaCtag = VlanId.vlanId((short) 11);
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setPonCTag(hsiaCtag)
                .setTechnologyProfileId(64)
                .setUniTagMatch(VlanId.vlanId(VlanId.NO_VID))
                .setUpstreamBandwidthProfile(usBp)
                .setUpstreamOltBandwidthProfile(usOltBp)
                .setIsPppoeRequired(true).build();
        UniTagInformation mc = new UniTagInformation.Builder()
                .setIsPppoeRequired(false).build();
        uniTagInformationList.add(hsia);
        uniTagInformationList.add(mc);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                        false, si);

        // return meter IDs
        doReturn(MeterId.meterId(2)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), usBp);
        doReturn(MeterId.meterId(3)).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), usOltBp);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(addedSub.port.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.PPPoED.ethType()))
                .fromApp(testAppId)
                .withPriority(10000)
                .add();

        component.handleSubscriberPppoeFlows(addedSub.device.id(), addedSub.port,
                FlowOperation.ADD, si);
        verify(component.flowObjectiveService, times(1))
                .filter(eq(addedSub.device.id()), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testInternalFlowListenerNotMaster() {
        doReturn(false).when(component.oltDeviceService).isLocalLeader(any());

        FlowRule flowRule = DefaultFlowRule.builder()
                .forDevice(DeviceId.deviceId("foo"))
                .fromApp(testAppId)
                .makePermanent()
                .withPriority(1000)
                .build();
        FlowRuleEvent event = new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED,
                flowRule);

        internalFlowListener.event(event);

        // if we're not master of the device, we should not update
        verify(internalFlowListener, never()).updateCpStatus(any(), any(), any());
    }

    @Test
    public void testInternalFlowListenerDifferentApp() {
        ApplicationId someAppId = new DefaultApplicationId(1, "org.opencord.olt.not-test");
        FlowRule flowRule = DefaultFlowRule.builder()
                .forDevice(DeviceId.deviceId("foo"))
                .fromApp(someAppId)
                .makePermanent()
                .withPriority(1000)
                .build();
        FlowRuleEvent event = new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED,
                flowRule);

        internalFlowListener.event(event);

        // if we're not master of the device, we should not update
        verify(internalFlowListener, never()).updateCpStatus(any(), any(), any());
    }

    @Test
    public void testRemoveSubscriberFlows() {
        // test that if we have EAPOL we wait till the tagged flow is removed
        // before installing the default one

        // setup
        component.enableEapol = true;

        // mock data
        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");
        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);
        Port port = new DefaultPort(device, PortNumber.portNumber(1), true,
                DefaultAnnotations.builder().set(PORT_NAME, PORT_1).build());

        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation hsia = new UniTagInformation.Builder()
                .setUpstreamBandwidthProfile("usbp")
                .setDownstreamBandwidthProfile("dsbp")
                .setPonCTag(VlanId.vlanId((short) 900)).build();
        uniTagInformationList.add(hsia);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        DiscoveredSubscriber sub = new DiscoveredSubscriber(
                device, port, DiscoveredSubscriber.Status.REMOVED, true, si);

        // first test that when we remove the EAPOL flow we return false so that the
        // subscriber is not removed from the queue
        doReturn(null).when(deviceService).getPort(any());
        doReturn(true).when(oltFlowService).areSubscriberFlowsPendingRemoval(any(), any(), eq(true));
        boolean res = oltFlowService.removeSubscriberFlows(sub, DEFAULT_BP_ID_DEFAULT, DEFAULT_MCAST_SERVICE_NAME);
        verify(oltFlowService, times(1))
                .handleSubscriberDhcpFlows(deviceId, port, FlowOperation.REMOVE, si);
        verify(oltFlowService, times(1))
                .handleSubscriberEapolFlows(sub, FlowOperation.REMOVE, si);
        verify(oltFlowService, times(1))
                .handleSubscriberDataFlows(device, port, FlowOperation.REMOVE,
                        si, DEFAULT_MCAST_SERVICE_NAME);
        verify(oltFlowService, times(1))
                .handleSubscriberIgmpFlows(sub, FlowOperation.REMOVE);
        verify(oltFlowService, never())
                .handleEapolFlow(any(), any(), any(),
                        eq(FlowOperation.ADD), eq(VlanId.vlanId(OltFlowService.EAPOL_DEFAULT_VLAN)));
        Assert.assertFalse(res);

        // then test that if the tagged EAPOL is not there we install the default EAPOL
        // and return true so we remove the subscriber from the queue
        doReturn(false).when(oltFlowService).areSubscriberFlowsPendingRemoval(any(), any(), eq(true));
        doReturn(port).when(oltFlowService.deviceService).getPort(deviceId, port.number());
        res = oltFlowService.removeSubscriberFlows(sub, DEFAULT_BP_ID_DEFAULT, DEFAULT_MCAST_SERVICE_NAME);
        verify(oltFlowService, times(1))
                .handleEapolFlow(any(), any(), any(),
                        eq(FlowOperation.ADD), eq(VlanId.vlanId(OltFlowService.EAPOL_DEFAULT_VLAN)));
        Assert.assertTrue(res);
    }

    @Test
    public void testRemovedFlowEvent() throws InterruptedException {
        // test that we update the status in case of REMOVED flow even with non
        // existing port in the onos device manager

        DeviceId deviceId = DeviceId.deviceId("test-device");
        ProviderId pid = new ProviderId("of", "foo");
        Device device =
                new DefaultDevice(pid, deviceId, Device.Type.OLT, "", "", "", "", null);

        Port port1 = new DefaultPort(device, PortNumber.portNumber(1), true,
                                     DefaultAnnotations.builder().set(PORT_NAME, PORT_1).build());
        // create empty service for testing
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        UniTagInformation vlanUniTag = new UniTagInformation.Builder().setPonCTag(VlanId.vlanId((short) 60))
                .build();
        uniTagInformationList.add(vlanUniTag);
        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);
        ServiceKey sk1 = new ServiceKey(new AccessDevicePort(port1), vlanUniTag);

        TrafficSelector selector = DefaultTrafficSelector.builder()
                .matchInPort(port1.number())
                .matchVlanId(VlanId.vlanId((short) 60))
                .build();

        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
                .immediate()
                .setOutput(port1.number())
                .build();

        FlowRule flowRule = DefaultFlowRule.builder()
                .makePermanent()
                .withPriority(1000)
                .forTable(0)
                .forDevice(deviceId)
                .fromApp(testAppId)
                .withSelector(selector)
                .withTreatment(treatment)
                .build();

        // cpStatus map for the test
        component.cpStatus = component.storageService.
                <ServiceKey, OltPortStatus>consistentMapBuilder().build().asJavaMap();
        OltPortStatus cp1Status = new OltPortStatus(NONE, NONE, PENDING_REMOVE, NONE, NONE);
        component.cpStatus.put(sk1, cp1Status);

        FlowRuleEvent event = new FlowRuleEvent(FlowRuleEvent.Type.RULE_REMOVED, flowRule);
        doReturn(true).when(component.oltDeviceService).isLocalLeader(any());
        doReturn(device).when(component.deviceService).getDevice(deviceId);
        doReturn(si).when(component.subsService).get(PORT_1);

        oltFlowService.internalFlowListener.event(event);

        //Some time to finish the operation
        TimeUnit.MILLISECONDS.sleep(200);

        OltPortStatus status = component.cpStatus.get(sk1);
        Assert.assertEquals(REMOVED, status.subscriberFlowsStatus);
    }

    @Test
    public void testHandleNniFlowsDhcpV4WithNniDhcpTrapVid() {
        component.enableDhcpOnNni = true;
        component.enableDhcpV4 = true;

        SubscriberAndDeviceInformation testOltFttbSadis = new SubscriberAndDeviceInformation();
        testOltFttbSadis.setNniDhcpTrapVid(VlanId.vlanId("60"));

        when(component.sadisService.getSubscriberInfoService().get(testDevice.serialNumber())).
                thenReturn(testOltFttbSadis);

        component.handleNniFlows(testDevice, nniPort, FlowOperation.ADD);

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(nniPort.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(67)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(68)))
                .addCondition(Criteria.matchVlanId(testOltFttbSadis.nniDhcpTrapVid()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(DefaultTrafficTreatment.builder().setOutput(PortNumber.CONTROLLER).build())
                .add();

        // invoked with the correct DHCP filtering objective
        verify(component.flowObjectiveService, times(1))
                .filter(eq(deviceId), argThat(new FilteringObjectiveMatcher(expectedFilter)));
        // invoked only twice, LLDP and DHCP
        verify(component.flowObjectiveService, times(2))
                .filter(eq(deviceId), any());
    }

    @Test
    public void testHandleFttbSubscriberDhcpFlowsAdd() {
        component.enableDhcpV4 = true;

        // add two services, one requires DHCP the other doesn't
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();

        UniTagInformation mc = new UniTagInformation.Builder()
                .setIsDhcpRequired(false).build();
        uniTagInformationList.add(dpuMgmtUti);
        uniTagInformationList.add(mc);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                        false, si);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(2);
        MeterId usOltBpMeterId = MeterId.meterId(3);

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        TrafficTreatment expectedTreatment = DefaultTrafficTreatment.builder()
                .setVlanId(dpuMgmtUti.getPonSTag())
                .setVlanPcp((byte) dpuMgmtUti.getUsPonSTagPriority())
                .setOutput(PortNumber.CONTROLLER)
                .meter(usBpMeterId)
                //TODO why tests use this one and not the other method ?
                .writeMetadata(OltFlowServiceUtils.createTechProfValueForWriteMetadata(VlanId.NONE,
                        dpuMgmtUti.getTechnologyProfileId(), usOltBpMeterId), 0L).build();

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .permit()
                .withKey(Criteria.matchInPort(addedSub.port.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(68)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(67)))
                .addCondition(Criteria.matchVlanId(dpuMgmtUti.getPonCTag()))
                .addCondition(Criteria.matchVlanPcp((byte) dpuMgmtUti.getUsPonCTagPriority()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(expectedTreatment)
                .add();

        component.handleSubscriberDhcpFlows(addedSub.device.id(), addedSub.port,
                FlowOperation.ADD, si);
        verify(component.flowObjectiveService, times(1))
                .filter(eq(addedSub.device.id()), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testRemoveFttbSubscriberDhcpFlows() {
        component.enableDhcpV4 = true;

        // Mocking the get call, to mark the SubscriberKey as already added.
        component.cpStatus = Mockito.mock(Map.class);
        doReturn(new OltPortStatus(null, null, null, ADDED, null))
                .when(component.cpStatus).get(Mockito.any());

        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        uniTagInformationList.add(dpuMgmtUti);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber removedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.REMOVED,
                        false, si);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(2);
        MeterId usOltBpMeterId = MeterId.meterId(3);

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        TrafficTreatment expectedTreatment = DefaultTrafficTreatment.builder()
                .setVlanId(dpuMgmtUti.getPonSTag())
                .setVlanPcp((byte) dpuMgmtUti.getUsPonSTagPriority())
                .setOutput(PortNumber.CONTROLLER)
                .meter(usBpMeterId)
                .writeMetadata(OltFlowServiceUtils.createTechProfValueForWriteMetadata(VlanId.NONE,
                        dpuMgmtUti.getTechnologyProfileId(), usOltBpMeterId), 0L).build();

        FilteringObjective expectedFilter = DefaultFilteringObjective.builder()
                .deny()
                .withKey(Criteria.matchInPort(removedSub.port.number()))
                .addCondition(Criteria.matchEthType(EthType.EtherType.IPV4.ethType()))
                .addCondition(Criteria.matchIPProtocol(IPv4.PROTOCOL_UDP))
                .addCondition(Criteria.matchUdpSrc(TpPort.tpPort(68)))
                .addCondition(Criteria.matchUdpDst(TpPort.tpPort(67)))
                .addCondition(Criteria.matchVlanId(dpuMgmtUti.getPonCTag()))
                .addCondition(Criteria.matchVlanPcp((byte) dpuMgmtUti.getUsPonCTagPriority()))
                .fromApp(testAppId)
                .withPriority(10000)
                .withMeta(expectedTreatment)
                .add();

        component.handleSubscriberDhcpFlows(removedSub.device.id(), removedSub.port,
                FlowOperation.REMOVE, si);
        verify(component.flowObjectiveService, times(1))
                .filter(eq(removedSub.device.id()), argThat(new FilteringObjectiveMatcher(expectedFilter)));
    }

    @Test
    public void testHandleFttbMacSwitchingFlowsAdd() {
        component.enableDhcpV4 = true;

        List<UniTagInformation> uniTagInformationList = new LinkedList<>();

        uniTagInformationList.add(dpuMgmtUti);
        uniTagInformationList.add(ancpUti);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                        false, si);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(1);
        MeterId usOltBpMeterId = MeterId.meterId(1);

        MeterId dsBpMeterId = MeterId.meterId(2);
        MeterId dsOltBpMeterId = MeterId.meterId(2);

        MacAddress mac = MacAddress.valueOf("0A:00:27:00:00:09");
        Host host = Mockito.mock(Host.class);
        doReturn(mac).when(host).mac();
        doReturn(dpuMgmtUti.getPonSTag()).when(host).vlan();

        doReturn(new HashSet<>(Arrays.asList(host))).when(component.hostService)
                .getConnectedHosts(new ConnectPoint(addedSub.device.id(), addedSub.port.number()));

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        doReturn(dsBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getDownstreamBandwidthProfile());
        doReturn(dsOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getDownstreamOltBandwidthProfile());

        String[] directions = {FTTB_FLOW_UPSTREAM, FTTB_FLOW_DOWNSTREAM};
        for (UniTagInformation uti : uniTagInformationList) {
            for (String direction : directions) {
                TrafficTreatment.Builder expectedTreatment = DefaultTrafficTreatment.builder();
                TrafficSelector.Builder expectedSelectorBuilder = DefaultTrafficSelector.builder();

                DefaultAnnotations.Builder annotationBuilder = DefaultAnnotations.builder();
                annotationBuilder.set(FTTB_FLOW_DIRECTION, direction);
                annotationBuilder.set(FTTB_SERVICE_NAME, uti.getServiceName());

                switch (direction) {
                    case FTTB_FLOW_UPSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(addedSub.port.number())
                                .matchVlanId(uti.getPonCTag())
                                .matchVlanPcp((byte) uti.getUsPonCTagPriority())
                                .matchEthSrc(mac);

                        expectedTreatment.setVlanId(uti.getPonSTag())
                                .setVlanPcp((byte) uti.getUsPonSTagPriority())
                                .setOutput(nniPort.number())
                                .meter(usBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.NONE,
                                        uti.getTechnologyProfileId(), nniPort.number()), 0L).build();

                        annotationBuilder.set(UPSTREAM_ONU, usBpMeterId.toString());
                        annotationBuilder.set(UPSTREAM_OLT, usOltBpMeterId.toString());
                        break;

                    case FTTB_FLOW_DOWNSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(nniPort.number())
                                .matchVlanId(uti.getPonSTag())
                                .matchEthDst(mac);

                        expectedTreatment.setVlanId(uti.getPonCTag())
                                .setOutput(addedSub.port.number())
                                .meter(dsBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.NONE,
                                        uti.getTechnologyProfileId(), addedSub.port.number()), 0L).build();

                        annotationBuilder.set(DOWNSTREAM_ONU, dsBpMeterId.toString());
                        annotationBuilder.set(DOWNSTREAM_OLT, dsOltBpMeterId.toString());
                        break;

                    default:
                        return;
                }

                ForwardingObjective expected = DefaultForwardingObjective.builder()
                        .withFlag(ForwardingObjective.Flag.VERSATILE)
                        .withPriority(1000)
                        .makePermanent()
                        .withSelector(expectedSelectorBuilder.build())
                        .withAnnotations(annotationBuilder.build())
                        .fromApp(testAppId)
                        .withTreatment(expectedTreatment.build())
                        .add();

                component.handleSubscriberDataFlows(addedSub.device, addedSub.port,
                        FlowOperation.ADD, si, DEFAULT_MCAST_SERVICE_NAME_DEFAULT);
                verify(component.flowObjectiveService, times(1))
                        .forward(eq(addedSub.device.id()), eq(expected));
            }
        }
    }

    @Test
    public void testRemoveFttbMacSwitchingFlows() {
        component.enableDhcpV4 = true;
        component.cpStatus = Mockito.mock(Map.class);
        List<UniTagInformation> uniTagInformationList = new LinkedList<>();

        uniTagInformationList.add(dpuMgmtUti);
        uniTagInformationList.add(ancpUti);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber removedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.REMOVED,
                        false, si);


        ServiceKey sk1 = new ServiceKey(new AccessDevicePort(removedSub.port), dpuMgmtUti);
        ServiceKey sk2 = new ServiceKey(new AccessDevicePort(removedSub.port), ancpUti);

        component.cpStatus = component.storageService.
                <ServiceKey, OltPortStatus>consistentMapBuilder().build().asJavaMap();
        OltPortStatus cp1Status = new OltPortStatus(NONE, NONE, PENDING_ADD, NONE, NONE);
        OltPortStatus cp2Status = new OltPortStatus(NONE, NONE, PENDING_ADD, NONE, NONE);
        component.cpStatus.put(sk1, cp1Status);
        component.cpStatus.put(sk2, cp2Status);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(1);
        MeterId usOltBpMeterId = MeterId.meterId(1);

        MeterId dsBpMeterId = MeterId.meterId(2);
        MeterId dsOltBpMeterId = MeterId.meterId(2);

        MacAddress mac = MacAddress.valueOf("0A:00:27:00:00:09");
        Host host = Mockito.mock(Host.class);
        doReturn(mac).when(host).mac();
        doReturn(dpuMgmtUti.getPonSTag()).when(host).vlan();

        doReturn(new HashSet<>(Arrays.asList(host))).when(component.hostService)
                .getConnectedHosts(new ConnectPoint(removedSub.device.id(), removedSub.port.number()));

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        doReturn(dsBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getDownstreamBandwidthProfile());
        doReturn(dsOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getDownstreamOltBandwidthProfile());

        String[] directions = {FTTB_FLOW_UPSTREAM, FTTB_FLOW_DOWNSTREAM};
        for (UniTagInformation uti : uniTagInformationList) {
            for (String direction : directions) {
                TrafficTreatment.Builder expectedTreatment = DefaultTrafficTreatment.builder();
                TrafficSelector.Builder expectedSelectorBuilder = DefaultTrafficSelector.builder();

                DefaultAnnotations.Builder annotationBuilder = DefaultAnnotations.builder();
                annotationBuilder.set(FTTB_FLOW_DIRECTION, direction);
                annotationBuilder.set(FTTB_SERVICE_NAME, uti.getServiceName());

                switch (direction) {
                    case FTTB_FLOW_UPSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(removedSub.port.number())
                                .matchVlanId(uti.getPonCTag())
                                .matchVlanPcp((byte) uti.getUsPonCTagPriority())
                                .matchEthSrc(mac);

                        expectedTreatment.setVlanId(uti.getPonSTag())
                                .setVlanPcp((byte) uti.getUsPonSTagPriority())
                                .setOutput(nniPort.number())
                                .meter(usBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.NONE,
                                        uti.getTechnologyProfileId(), nniPort.number()), 0L).build();

                        annotationBuilder.set(UPSTREAM_ONU, usBpMeterId.toString());
                        annotationBuilder.set(UPSTREAM_OLT, usOltBpMeterId.toString());
                        break;

                    case FTTB_FLOW_DOWNSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(nniPort.number())
                                .matchVlanId(uti.getPonSTag())
                                .matchEthDst(mac);

                        expectedTreatment.setVlanId(uti.getPonCTag())
                                .setOutput(removedSub.port.number())
                                .meter(dsBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.NONE,
                                        uti.getTechnologyProfileId(), removedSub.port.number()), 0L).build();

                        annotationBuilder.set(DOWNSTREAM_ONU, dsBpMeterId.toString());
                        annotationBuilder.set(DOWNSTREAM_OLT, dsOltBpMeterId.toString());
                        break;

                    default:
                        return;
                }

                ForwardingObjective expected = DefaultForwardingObjective.builder()
                        .withFlag(ForwardingObjective.Flag.VERSATILE)
                        .withPriority(1000)
                        .makePermanent()
                        .withSelector(expectedSelectorBuilder.build())
                        .withAnnotations(annotationBuilder.build())
                        .fromApp(testAppId)
                        .withTreatment(expectedTreatment.build())
                        .remove();

                component.handleSubscriberDataFlows(removedSub.device, removedSub.port,
                        FlowOperation.REMOVE, si, DEFAULT_MCAST_SERVICE_NAME_DEFAULT);
                verify(component.flowObjectiveService, times(1))
                        .forward(eq(removedSub.device.id()), eq(expected));
            }
        }
    }

    @Test
    public void testHandleFttbSubscriberFlowsAdd() {
        component.enableDhcpV4 = true;

        List<UniTagInformation> uniTagInformationList = new LinkedList<>();
        uniTagInformationList.add(fttbSubscriberUti);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber addedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.ADDED,
                        false, si);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(1);
        MeterId usOltBpMeterId = MeterId.meterId(1);

        MeterId dsBpMeterId = MeterId.meterId(2);
        MeterId dsOltBpMeterId = MeterId.meterId(2);

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        doReturn(dsBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getDownstreamBandwidthProfile());
        doReturn(dsOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(addedSub.device.id(), dpuMgmtUti.getDownstreamOltBandwidthProfile());

        String[] directions = {FTTB_FLOW_UPSTREAM, FTTB_FLOW_DOWNSTREAM};
        for (UniTagInformation uti : uniTagInformationList) {
            for (String direction : directions) {
                TrafficTreatment.Builder expectedTreatment = DefaultTrafficTreatment.builder();
                TrafficSelector.Builder expectedSelectorBuilder = DefaultTrafficSelector.builder();

                DefaultAnnotations.Builder annotationBuilder = DefaultAnnotations.builder();
                annotationBuilder.set(FTTB_FLOW_DIRECTION, direction);
                annotationBuilder.set(FTTB_SERVICE_NAME, uti.getServiceName());

                switch (direction) {
                    case FTTB_FLOW_UPSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(addedSub.port.number())
                                .matchVlanId(uti.getPonCTag());

                        expectedTreatment.setVlanId(uti.getPonSTag())
                                .setOutput(nniPort.number())
                                .meter(usBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.ANY,
                                        uti.getTechnologyProfileId(), nniPort.number()), 0L).build();

                        annotationBuilder.set(UPSTREAM_ONU, usBpMeterId.toString());
                        annotationBuilder.set(UPSTREAM_OLT, usOltBpMeterId.toString());
                        break;

                    case FTTB_FLOW_DOWNSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(nniPort.number())
                                .matchMetadata(uti.getPonSTag().toShort())
                                .matchVlanId(uti.getPonSTag());

                        expectedTreatment.setVlanId(uti.getPonCTag())
                                .setOutput(addedSub.port.number())
                                .meter(dsBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.ANY,
                                        uti.getTechnologyProfileId(), addedSub.port.number()), 0L).build();

                        annotationBuilder.set(DOWNSTREAM_ONU, dsBpMeterId.toString());
                        annotationBuilder.set(DOWNSTREAM_OLT, dsOltBpMeterId.toString());
                        break;

                    default:
                        return;
                }

                ForwardingObjective expected = DefaultForwardingObjective.builder()
                        .withFlag(ForwardingObjective.Flag.VERSATILE)
                        .withPriority(1000)
                        .makePermanent()
                        .withSelector(expectedSelectorBuilder.build())
                        .withAnnotations(annotationBuilder.build())
                        .fromApp(testAppId)
                        .withTreatment(expectedTreatment.build())
                        .add();

                component.handleSubscriberDataFlows(addedSub.device, addedSub.port,
                        FlowOperation.ADD, si, DEFAULT_MCAST_SERVICE_NAME_DEFAULT);
                verify(component.flowObjectiveService, times(1))
                        .forward(eq(addedSub.device.id()), eq(expected));
            }
        }
    }

    @Test
    public void testRemoveFttbSubscriberFlows() {
        component.enableDhcpV4 = true;

        OltPortStatus oltPortStatus1 = new OltPortStatus(null, null, ADDED,
                null, null);
        // Mocking the get call, to mark the SubscriberKey as already added.
        component.cpStatus = Mockito.mock(Map.class);
        when(component.cpStatus.get(Mockito.any())).thenReturn(oltPortStatus1);

        List<UniTagInformation> uniTagInformationList = new LinkedList<>();

        uniTagInformationList.add(fttbSubscriberUti);

        SubscriberAndDeviceInformation si = new SubscriberAndDeviceInformation();
        si.setUniTagList(uniTagInformationList);

        final DiscoveredSubscriber removedSub =
                new DiscoveredSubscriber(testDevice,
                        uniUpdateEnabled, DiscoveredSubscriber.Status.REMOVED,
                        false, si);

        // return meter IDs
        MeterId usBpMeterId = MeterId.meterId(1);
        MeterId usOltBpMeterId = MeterId.meterId(1);

        MeterId dsBpMeterId = MeterId.meterId(2);
        MeterId dsOltBpMeterId = MeterId.meterId(2);

        doReturn(usBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamBandwidthProfile());
        doReturn(usOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getUpstreamOltBandwidthProfile());

        doReturn(dsBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getDownstreamBandwidthProfile());
        doReturn(dsOltBpMeterId).when(component.oltMeterService)
                .getMeterIdForBandwidthProfile(removedSub.device.id(), dpuMgmtUti.getDownstreamOltBandwidthProfile());

        String[] directions = {FTTB_FLOW_UPSTREAM, FTTB_FLOW_DOWNSTREAM};
        for (UniTagInformation uti : uniTagInformationList) {
            for (String direction : directions) {
                TrafficTreatment.Builder expectedTreatment = DefaultTrafficTreatment.builder();
                TrafficSelector.Builder expectedSelectorBuilder = DefaultTrafficSelector.builder();

                DefaultAnnotations.Builder annotationBuilder = DefaultAnnotations.builder();
                annotationBuilder.set(FTTB_FLOW_DIRECTION, direction);
                annotationBuilder.set(FTTB_SERVICE_NAME, uti.getServiceName());

                switch (direction) {
                    case FTTB_FLOW_UPSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(removedSub.port.number())
                                .matchVlanId(uti.getPonCTag());

                        expectedTreatment.setVlanId(uti.getPonSTag())
                                .setOutput(nniPort.number())
                                .meter(usBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.ANY,
                                        uti.getTechnologyProfileId(), nniPort.number()), 0L).build();

                        annotationBuilder.set(UPSTREAM_ONU, usBpMeterId.toString());
                        annotationBuilder.set(UPSTREAM_OLT, usOltBpMeterId.toString());
                        break;

                    case FTTB_FLOW_DOWNSTREAM:
                        expectedSelectorBuilder
                                .matchInPort(nniPort.number())
                                .matchMetadata(uti.getPonSTag().toShort())
                                .matchVlanId(uti.getPonSTag());

                        expectedTreatment.setVlanId(uti.getPonCTag())
                                .setOutput(removedSub.port.number())
                                .meter(dsBpMeterId)
                                .writeMetadata(OltFlowServiceUtils.createMetadata(VlanId.ANY,
                                        uti.getTechnologyProfileId(), removedSub.port.number()), 0L).build();

                        annotationBuilder.set(DOWNSTREAM_ONU, dsBpMeterId.toString());
                        annotationBuilder.set(DOWNSTREAM_OLT, dsOltBpMeterId.toString());
                        break;

                    default:
                        return;
                }

                ForwardingObjective expected = DefaultForwardingObjective.builder()
                        .withFlag(ForwardingObjective.Flag.VERSATILE)
                        .withPriority(1000)
                        .makePermanent()
                        .withSelector(expectedSelectorBuilder.build())
                        .withAnnotations(annotationBuilder.build())
                        .fromApp(testAppId)
                        .withTreatment(expectedTreatment.build())
                        .remove();

                component.handleSubscriberDataFlows(removedSub.device, removedSub.port,
                        FlowOperation.REMOVE, si, DEFAULT_MCAST_SERVICE_NAME_DEFAULT);
                verify(component.flowObjectiveService, times(1))
                        .forward(eq(removedSub.device.id()), eq(expected));
            }
        }
    }
}
