package com.thinkbiganalytics.alerts.api.core;

/*-
 * #%L
 * thinkbig-alerts-core
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import com.google.common.base.Function;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.MoreExecutors;
import com.thinkbiganalytics.alerts.api.Alert;
import com.thinkbiganalytics.alerts.api.Alert.State;
import com.thinkbiganalytics.alerts.api.AlertChangeEvent;
import com.thinkbiganalytics.alerts.api.AlertCriteria;
import com.thinkbiganalytics.alerts.api.AlertListener;
import com.thinkbiganalytics.alerts.api.AlertResponder;
import com.thinkbiganalytics.alerts.api.AlertResponse;
import com.thinkbiganalytics.alerts.api.core.AggregatingAlertProvider.AlertInvocationHandler;
import com.thinkbiganalytics.alerts.api.core.AggregatingAlertProvider.SourceAlertID;
import com.thinkbiganalytics.alerts.spi.AlertManager;
import com.thinkbiganalytics.alerts.spi.AlertSource;
import com.thinkbiganalytics.metadata.event.reactor.ReactorContiguration;
import com.thinkbiganalytics.security.UsernamePrincipal;

import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.Serializable;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {ReactorContiguration.class, AggregatingAlertProviderTest.TestConfig.class})
public class AggregatingAlertProviderTest {

    @Inject
    private AggregatingAlertProvider provider;

    @Mock
    private AlertSource source;

    @Mock
    private AlertManager manager;

    @Mock
    private AlertResponse response;

    @Mock
    private AlertListener listener;

    @Mock
    private AlertResponder responder;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        provider.addListener(this.listener);
        provider.addResponder(this.responder);

        when(this.source.criteria()).thenReturn(new BaseAlertCriteria());
        when(this.manager.criteria()).thenReturn(new BaseAlertCriteria());
    }

    @Test
    public void testResolve() {
        TestAlert mgrAlert = new TestAlert(this.manager);
        TestAlert srcAlert = new TestAlert(this.source);
        SourceAlertID mgrId = new SourceAlertID(mgrAlert.getId(), this.manager);
        SourceAlertID srcId = new SourceAlertID(srcAlert.getId(), this.source);
        String mgrIdStr = mgrId.toString();
        String srcIdStr = srcId.toString();

        this.provider.addAlertSource(this.source);
        this.provider.addAlertManager(this.manager);

        when(this.source.resolve(any(TestID.class))).thenReturn(srcAlert.getId());
        when(this.manager.resolve(any(TestID.class))).thenReturn(mgrAlert.getId());
        when(this.source.resolve(any(String.class))).thenReturn(srcAlert.getId());
        when(this.manager.resolve(any(String.class))).thenReturn(mgrAlert.getId());

        Alert.ID mgrObjResolved = this.provider.resolve(mgrId);
        Alert.ID srcObjResolved = this.provider.resolve(srcId);
        Alert.ID mgrStrResolved = this.provider.resolve(mgrIdStr);
        Alert.ID srcStrResolved = this.provider.resolve(srcIdStr);

        assertThat(mgrObjResolved).isInstanceOf(SourceAlertID.class).isEqualTo(mgrId).isEqualTo(mgrStrResolved);
        assertThat(srcObjResolved).isInstanceOf(SourceAlertID.class).isEqualTo(srcId).isEqualTo(srcStrResolved);
    }

    @Test
    public void testGetAlert() {
        TestAlert mgrAlert = new TestAlert(this.manager);
        TestAlert srcAlert = new TestAlert(this.source);
        SourceAlertID mgrId = new SourceAlertID(mgrAlert.getId(), this.manager);
        SourceAlertID srcId = new SourceAlertID(srcAlert.getId(), this.source);

        this.provider.addAlertSource(this.source);
        this.provider.addAlertManager(this.manager);

        when(this.source.getAlert(any(Alert.ID.class))).thenReturn(Optional.of(srcAlert));
        when(this.manager.getAlert(any(Alert.ID.class))).thenReturn(Optional.of(mgrAlert));

        Alert getMgrAlert = providerToSourceAlertFunction().apply(this.provider.getAlert(mgrId).get());
        Alert getSrcAlert = providerToSourceAlertFunction().apply(this.provider.getAlert(srcId).get());

        assertThat(getMgrAlert).isEqualTo(mgrAlert);
        assertThat(getSrcAlert).isEqualTo(srcAlert);
    }

    @Test
    public void testGetAlertsSinceTimeSourceOnly() {
        DateTime since = DateTime.now().minusSeconds(1);
        TestAlert srcAlert = new TestAlert(this.source);

        this.provider.addAlertSource(this.source);

        when(this.source.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(srcAlert));

        Iterator<? extends Alert> results = this.provider.getAlertsAfter(since);
        Iterator<? extends Alert> itr = Iterators.transform(results, providerToSourceAlertFunction());
        Alert alert = itr.next();

        assertThat(alert).isEqualTo(srcAlert);
    }

    @Test
    public void testGetAlertsSinceTimeManagerOnly() {
        DateTime since = DateTime.now().minusSeconds(1);
        TestAlert mgrAlert = new TestAlert(this.manager);

        this.provider.addAlertManager(this.manager);

        when(this.manager.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(mgrAlert));

        Iterator<? extends Alert> results = this.provider.getAlertsAfter(since);
        Iterator<? extends Alert> itr = Iterators.transform(results, providerToSourceAlertFunction());
        Alert alert = itr.next();

        assertThat(alert).isEqualTo(mgrAlert);
    }

    @Test
    public void testGetAlertsSinceTimeAllSources() {
        DateTime since = DateTime.now().minusSeconds(1);
        TestAlert mgrAlert = new TestAlert(this.manager);
        TestAlert srcAlert = new TestAlert(this.source);

        this.provider.addAlertSource(this.source);
        this.provider.addAlertManager(this.manager);

        when(this.source.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(srcAlert));
        when(this.manager.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(mgrAlert));

        Iterator<? extends Alert> results = this.provider.getAlertsAfter(since);
        Iterator<? extends Alert> itr = Iterators.transform(results, providerToSourceAlertFunction());
        List<Alert> alerts = Lists.newArrayList(itr);

        assertThat(alerts).hasSize(2).contains(srcAlert, mgrAlert);
    }

    @Test
    public void testRespondToActionable() {
        TestAlert mgrAlert = new TestAlert(this.manager, true);

        this.provider.addAlertManager(this.manager);

        when(this.manager.getAlert(any(Alert.ID.class))).thenReturn(Optional.of(mgrAlert));

        this.provider.respondTo(new SourceAlertID(mgrAlert.getId(), this.manager), this.responder);

        verify(this.responder).alertChange(any(Alert.class), any(AlertResponse.class));
    }

    @Test
    public void testRespondToNonActionable() {
        TestAlert mgrAlert = new TestAlert(this.manager, false);

        this.provider.addAlertManager(this.manager);

        when(this.manager.getAlert(any(Alert.ID.class))).thenReturn(Optional.of(mgrAlert));

        this.provider.respondTo(new SourceAlertID(mgrAlert.getId(), this.manager), this.responder);

        verify(this.responder, never()).alertChange(any(Alert.class), any(AlertResponse.class));
    }

    @Test
    public void testRespondToChange() throws InterruptedException {
        TestAlert initMgrAlert = new TestAlert(this.manager, true);
        CountDownLatch latch = new CountDownLatch(1);

        this.provider.addAlertManager(this.manager);

        when(this.manager.getAlert(any(Alert.ID.class))).thenReturn(Optional.of(initMgrAlert));
        when(this.manager.getResponse(any(Alert.class))).thenReturn(this.response);
        when(this.response.handle(any(String.class), any(Serializable.class))).thenReturn(initMgrAlert);
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch.countDown();
                return null;
            }
        }).when(this.listener).alertChange(any(Alert.class));

        this.provider.respondTo(new SourceAlertID(initMgrAlert.getId(), this.manager), new AlertResponder() {
            @Override
            public void alertChange(Alert alert, AlertResponse response) {
                response.handle(null, "handled");
            }
        });

        latch.await(10, TimeUnit.SECONDS);

        verify(this.response).handle(eq(null), eq("handled"));
        verify(this.listener, atLeastOnce()).alertChange(any(Alert.class));
    }

    @Test
    public void testAlertsAvailable() throws InterruptedException {
        TestAlert mgrAlert = new TestAlert(this.manager, true);
        TestAlert srcAlert = new TestAlert(this.source);
        CountDownLatch latch = new CountDownLatch(3);

        this.provider.addAlertSource(this.source);
        this.provider.addAlertManager(this.manager);

        when(this.source.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(srcAlert));
        when(this.manager.getAlerts(any(AlertCriteria.class))).thenAnswer(iteratorAnswer(mgrAlert));
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch.countDown();
                return null;
            }
        }).when(this.listener).alertChange(any(Alert.class));
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch.countDown();
                return null;
            }
        }).when(this.responder).alertChange(any(Alert.class), any(AlertResponse.class));

        this.provider.alertsAvailable(2);

        latch.await(10, TimeUnit.SECONDS);

        verify(this.listener, times(2)).alertChange(any(Alert.class));
        verify(this.responder, times(1)).alertChange(any(Alert.class), any(AlertResponse.class));
    }


    private Answer<Iterator<? extends Alert>> iteratorAnswer(final Alert... alerts) {
        return new Answer<Iterator<? extends Alert>>() {
            @Override
            public Iterator<? extends Alert> answer(InvocationOnMock invocation) throws Throwable {
                return interator(alerts);
            }
        };
    }

    private Iterator<? extends Alert> interator(Alert... alerts) {
        return Arrays.asList(alerts).iterator();
    }

    private Function<Alert, Alert> providerToSourceAlertFunction() {
        return new Function<Alert, Alert>() {
            @Override
            public Alert apply(Alert input) {
                if (Proxy.isProxyClass(input.getClass())) {
                    AlertInvocationHandler handler = (AlertInvocationHandler) Proxy.getInvocationHandler(input);
                    return handler.getWrappedAlert();
                } else {
                    return input;
                }
            }
        };
    }


    private static class TestID implements Alert.ID {

        @Override
        public String toString() {
            return "TestID:" + hashCode();
        }
    }

    @Configuration
    public static class TestConfig {

        @Bean(name = "alertProvider")
        @Primary
        @Scope("prototype")
        public AggregatingAlertProvider alertProvider() {
            AggregatingAlertProvider provider = new AggregatingAlertProvider();
            provider.setAvailableAlertsExecutor(MoreExecutors.directExecutor());
            return provider;
        }
    }

    private class TestAlert implements Alert {

        private TestID id = new TestID();
        private AlertSource source;
        private boolean actionable;
        private DateTime createdTime;
        private boolean cleared = false;

        public TestAlert(AlertSource src) {
            this(src, src instanceof AlertManager);
        }

        public TestAlert(AlertSource src, boolean actionable) {
            this(src, actionable, DateTime.now());
        }

        public TestAlert(AlertSource src, DateTime created) {
            this(src, src instanceof AlertManager, created);
        }

        public TestAlert(AlertSource src, boolean actionable, DateTime created) {
            this.source = src;
            this.actionable = actionable;
            this.createdTime = created;
        }

        @Override
        @SuppressWarnings("serial")
        public ID getId() {
            return this.id;
        }

        @Override
        public URI getType() {
            return URI.create("http://com.example/alert/test");
        }

        @Override
        public String getDescription() {
            return "";
        }

        @Override
        public Level getLevel() {
            return Level.INFO;
        }

        @Override
        public DateTime getCreatedTime() {
            return this.createdTime;
        }

        @Override
        public AlertSource getSource() {
            return this.source;
        }

        @Override
        public boolean isActionable() {
            return this.actionable;
        }

        @Override
        public State getState() {
            return this.getEvents().get(0).getState();
        }

        @Override
        public boolean isCleared() {
            return this.cleared;
        }

        @Override
        public List<AlertChangeEvent> getEvents() {
            return Collections.singletonList(new TestChangeEvent(this));
        }

        @Override
        @SuppressWarnings("unchecked")
        public <C extends Serializable> C getContent() {
            return (C) new Boolean(this.actionable);
        }
    }

    private class TestChangeEvent implements AlertChangeEvent {

        private TestAlert alert;
        private DateTime time = DateTime.now();

        public TestChangeEvent(TestAlert alert) {
            this.alert = alert;
            this.time = alert.createdTime;
        }

        @Override
        public DateTime getChangeTime() {
            return this.time;
        }

        @Override
        public String getDescription() {
            return null;
        }

        @Override
        public State getState() {
            return alert.isActionable() ? State.CREATED : State.UNHANDLED;
        }

        @Override
        public Principal getUser() {
            return new UsernamePrincipal("test");
        }

        @Override
        public <C extends Serializable> C getContent() {
            return null;
        }
    }
}
