/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.sdk.client.subscriptions;


import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedSubscription.ChangeListener;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.DataChangeTrigger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.DeadbandType;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.ContentFilter;
import org.eclipse.milo.opcua.stack.core.types.structured.DataChangeFilter;
import org.eclipse.milo.opcua.stack.core.types.structured.EventFilter;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.SimpleAttributeOperand;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;


@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ManagedSubscriptionTest extends AbstractSubscriptionTest {

    @Test
    public void createAndDeleteDataItem() throws UaException {
        ManagedDataItem dataItem = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );

        assertTrue(dataItem.getStatusCode().isGood());

        subscription.deleteDataItem(dataItem);
        assertFalse(subscription.getDataItems().contains(dataItem));
    }

    @Test
    public void createAndDeleteEventItem() throws UaException {
        EventFilter eventFilter = new EventFilter(
            new SimpleAttributeOperand[]{
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "EventId")},
                    AttributeId.Value.uid(),
                    null),
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "Time")},
                    AttributeId.Value.uid(),
                    null),
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "Message")},
                    AttributeId.Value.uid(),
                    null)
            },
            new ContentFilter(null)
        );

        ManagedEventItem eventItem = subscription.createEventItem(Identifiers.Server, eventFilter);

        assertTrue(eventItem.getStatusCode().isGood());

        subscription.deleteEventItem(eventItem);
        assertFalse(subscription.getEventItems().contains(eventItem));
    }

    @Test
    public void createWithSamplingInterval() throws UaException {
        ReadValueId readValueId = new ReadValueId(
            Identifiers.Server_ServerStatus_State,
            AttributeId.Value.uid(),
            null,
            QualifiedName.NULL_VALUE
        );

        ManagedDataItem dataItem = subscription.createDataItem(5000.0, readValueId);

        assertEquals(5000.0, dataItem.getSamplingInterval());
        assertEquals(5000.0, dataItem.getMonitoredItem().getRequestedSamplingInterval());
        assertEquals(5000.0, dataItem.getMonitoredItem().getRevisedSamplingInterval());
    }

    @Test
    public void setPublishingInterval() throws UaException {
        double original = subscription.getPublishingInterval();

        double revised1 = subscription.setPublishingInterval(100.0);
        assertEquals(100.0, revised1);

        double revised2 = subscription.setPublishingInterval(original);
        assertEquals(original, revised2);
    }

    @Test
    public void setPublishingMode() throws UaException {
        subscription.setPublishingEnabled(false);
        assertFalse(subscription.isPublishingEnabled());

        subscription.setPublishingEnabled(true);
        assertTrue(subscription.isPublishingEnabled());
    }

    @Test
    public void defaultMonitoringMode() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(MonitoringMode.Reporting, dataItem1.getMonitoringMode());
        assertEquals(MonitoringMode.Reporting, dataItem1.getMonitoredItem().getMonitoringMode());

        subscription.setDefaultMonitoringMode(MonitoringMode.Sampling);

        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(MonitoringMode.Sampling, dataItem2.getMonitoringMode());
        assertEquals(MonitoringMode.Sampling, dataItem2.getMonitoredItem().getMonitoringMode());

        subscription.setDefaultMonitoringMode(MonitoringMode.Disabled);

        ManagedDataItem dataItem3 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(MonitoringMode.Disabled, dataItem3.getMonitoringMode());
        assertEquals(MonitoringMode.Disabled, dataItem3.getMonitoredItem().getMonitoringMode());
    }

    @Test
    public void defaultSamplingInterval() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem1.getMonitoredItem().getRequestedSamplingInterval());
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem1.getMonitoredItem().getRevisedSamplingInterval());

        subscription.setDefaultSamplingInterval(100.0);
        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem2.getMonitoredItem().getRequestedSamplingInterval());
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem2.getMonitoredItem().getRevisedSamplingInterval());

        subscription.setDefaultSamplingInterval(5000.0);
        ManagedDataItem dataItem3 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem3.getMonitoredItem().getRequestedSamplingInterval());
        assertEquals(subscription.getDefaultSamplingInterval(), dataItem3.getMonitoredItem().getRevisedSamplingInterval());
    }

    @Test
    public void defaultQueueSize() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultQueueSize(), dataItem1.getMonitoredItem().getRequestedQueueSize());
        assertEquals(subscription.getDefaultQueueSize(), dataItem1.getMonitoredItem().getRevisedQueueSize());

        subscription.setDefaultQueueSize(uint(subscription.getDefaultQueueSize().intValue() + 1));
        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultQueueSize(), dataItem2.getMonitoredItem().getRequestedQueueSize());
        assertEquals(subscription.getDefaultQueueSize(), dataItem2.getMonitoredItem().getRevisedQueueSize());
    }

    @Test
    public void defaultDataFilter() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultDataFilter(), dataItem1.getMonitoredItem().getMonitoringFilter());

        subscription.setDefaultDataFilter(
            new DataChangeFilter(
                DataChangeTrigger.StatusValueTimestamp,
                uint(DeadbandType.None.getValue()),
                0.0
            )
        );

        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );

        assertEquals(subscription.getDefaultDataFilter(), dataItem2.getMonitoredItem().getMonitoringFilter());
    }

    @Test
    public void defaultTimestamp() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultTimestamps(), dataItem1.getMonitoredItem().getTimestamps());

        subscription.setDefaultTimestamps(TimestampsToReturn.Neither);
        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertEquals(subscription.getDefaultTimestamps(), dataItem2.getMonitoredItem().getTimestamps());
    }

    @Test
    public void defaultDiscardOldest() throws UaException {
        ManagedDataItem dataItem1 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertTrue(subscription.getDefaultDiscardOldest());
        assertTrue(dataItem1.getDiscardOldest());
        assertTrue(dataItem1.getMonitoredItem().getDiscardOldest());

        subscription.setDefaultDiscardOldest(false);
        assertFalse(subscription.getDefaultDiscardOldest());

        ManagedDataItem dataItem2 = subscription.createDataItem(
            Identifiers.Server_ServerStatus_CurrentTime
        );
        assertFalse(dataItem2.getDiscardOldest());
        assertFalse(dataItem2.getMonitoredItem().getDiscardOldest());
    }

    @Test
    public void dataChangeListener() throws UaException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(2);

        subscription.addChangeListener(new ChangeListener() {
            @Override
            public void onDataReceived(List<ManagedDataItem> dataItems, List<DataValue> dataValues) {
                if (dataItems.get(0).getNodeId().equals(Identifiers.Server_ServerStatus_State)) {
                    latch.countDown();
                }
            }
        });

        subscription.addDataChangeListener(
            (dataItems, dataValues) -> {
                if (dataItems.get(0).getNodeId().equals(Identifiers.Server_ServerStatus_State)) {
                    latch.countDown();
                }
            }
        );

        ManagedDataItem dataItem = subscription.createDataItem(Identifiers.Server_ServerStatus_State);
        assertTrue(dataItem.getStatusCode().isGood());

        assertTrue(latch.await(5, TimeUnit.SECONDS));
    }

    @Test
    public void eventChangeListener() throws UaException, InterruptedException {
        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(1);

        subscription.addChangeListener(new ChangeListener() {
            @Override
            public void onEventReceived(List<ManagedEventItem> eventItems, List<Variant[]> eventFields) {
                if (eventItems.get(0).getNodeId().equals(Identifiers.Server)) {
                    latch1.countDown();
                }
            }
        });

        subscription.addEventChangeListener(
            (eventItems, eventFields) -> {
                if (eventItems.get(0).getNodeId().equals(Identifiers.Server)) {
                    latch2.countDown();
                }
            }
        );

        EventFilter eventFilter = new EventFilter(
            new SimpleAttributeOperand[]{
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "EventId")},
                    AttributeId.Value.uid(),
                    null),
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "Time")},
                    AttributeId.Value.uid(),
                    null),
                new SimpleAttributeOperand(
                    Identifiers.BaseEventType,
                    new QualifiedName[]{new QualifiedName(0, "Message")},
                    AttributeId.Value.uid(),
                    null)
            },
            new ContentFilter(null)
        );

        ManagedEventItem eventItem = subscription.createEventItem(Identifiers.Server, eventFilter);
        assertTrue(eventItem.getStatusCode().isGood());

        assertTrue(latch1.await(5, TimeUnit.SECONDS));
        assertTrue(latch2.await(5, TimeUnit.SECONDS));
    }

    @Test
    public void addRemoveChangeListener() {
        {
            ChangeListener changeListener = new ChangeListener() {
                @Override
                public void onDataReceived(List<ManagedDataItem> dataItems, List<DataValue> dataValues) {}

                @Override
                public void onEventReceived(List<ManagedEventItem> eventItems, List<Variant[]> eventFields) {}

                @Override
                public void onKeepAliveReceived() {}
            };

            subscription.addChangeListener(changeListener);

            assertTrue(subscription.removeChangeListener(changeListener));
        }

        {
            ChangeListener changeListener = subscription.addDataChangeListener((items, values) -> {});

            assertTrue(subscription.removeChangeListener(changeListener));
        }

        {
            ChangeListener changeListener = subscription.addEventChangeListener((items, events) -> {});

            assertTrue(subscription.removeChangeListener(changeListener));
        }
    }

    @Test
    public void addRemoveStatusListener() {
        ManagedSubscription.StatusListener statusListener = new ManagedSubscription.StatusListener() {
            @Override
            public void onNotificationDataLost(ManagedSubscription subscription) {}

            @Override
            public void onSubscriptionStatusChanged(ManagedSubscription subscription, StatusCode statusCode) {}

            @Override
            public void onSubscriptionTransferFailed(ManagedSubscription subscription, StatusCode statusCode) {}
        };

        subscription.addStatusListener(statusListener);

        assertTrue(subscription.removeStatusListener(statusListener));
    }

}
