package com.atlassian.stash.internal.notification.usersettings;

import com.atlassian.bitbucket.event.ApplicationEvent;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.sal.api.user.UserKey;
import com.atlassian.sal.api.usersettings.UserSettings;
import com.atlassian.sal.api.usersettings.UserSettingsService;
import com.atlassian.sal.core.usersettings.DefaultUserSettings;
import com.atlassian.stash.internal.notification.event.NotificationSendModePreferenceChangeEvent;
import com.google.common.base.Function;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.DirectFieldAccessor;

import static com.atlassian.stash.internal.notification.NotificationConstants.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("unchecked")
public class DefaultUserNotificationSettingsServiceTest {

    private static final int USER_ID = 1;
    private static final int OTHER_USER_ID = 2;

    private static final String MODECHANGED_BATCHED_EVENT_NAME = "stash.notification.modechanged.batched";

    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private ApplicationUser otherUser;
    @Mock
    private ApplicationPropertiesService propertiesService;
    private DefaultUserNotificationSettingsService service;
    @Mock
    private ApplicationUser user;
    private UserSettings userSettings;
    @Mock
    private UserSettingsService userSettingsService;

    @Before
    public void setup() {
        userSettings = DefaultUserSettings.builder()
                .put(KEY_SEND_MODE, SendMode.IMMEDIATE.name())
                .put(KEY_DISCOVERY_COUNT, 5L)
                .build();

        when(user.getId()).thenReturn(USER_ID);
        when(user.getName()).thenReturn("user");
        when(otherUser.getId()).thenReturn(OTHER_USER_ID);
        when(otherUser.getName()).thenReturn("otheruser");

        when(propertiesService.getPluginProperty(eq(PROP_DEFAULT_SEND_MODE), anyString()))
                .thenReturn(SendMode.BATCHED.name());

        when(userSettingsService.getUserSettings(UserKey.fromLong(USER_ID))).thenReturn(userSettings);
        when(userSettingsService.getUserSettings(UserKey.fromLong(OTHER_USER_ID))).thenReturn(userSettings);

        service = new DefaultUserNotificationSettingsService(eventPublisher, propertiesService, userSettingsService);
    }

    @Test
    public void testGetSettingForUser() {
        UserNotificationSettings settings = service.getSettingsForUser(user);
        assertEquals(SendMode.IMMEDIATE, settings.getSendMode());
        assertEquals(5, settings.getBatchFeatureDiscoveryShowCount());
    }

    @Test
    public void testGetSettingsForOtherUser() {
        // we will check default settings as part of this test too
        when(userSettingsService.getUserSettings(UserKey.fromLong(USER_ID))).thenReturn(null);
        UserSettings userSettings = DefaultUserSettings.builder().build();
        when(userSettingsService.getUserSettings(UserKey.fromLong(OTHER_USER_ID))).thenReturn(userSettings);

        UserNotificationSettings settings = service.getSettingsForUser(otherUser);
        assertEquals(SendMode.BATCHED, settings.getSendMode());

        // check the setting via property was requested when user didn't have a setting
        verify(propertiesService, times(1)).getPluginProperty(eq("plugin.bitbucket-notification.sendmode.default"), anyString());
    }

    @Test
    public void testGetSettingsForUserChecksLegacyKeys() {
        UserSettings userSettings = DefaultUserSettings.builder()
                .put(KEY_LEGACY_DISCOVERY_COUNT, 42L)
                .put(KEY_LEGACY_SEND_MODE, SendMode.BATCHED.name())
                .build();

        ApplicationUser legacyUser = mock(ApplicationUser.class);
        when(legacyUser.getId()).thenReturn(100);

        when(userSettingsService.getUserSettings(UserKey.fromLong(100L))).thenReturn(userSettings);

        UserNotificationSettings settings = service.getSettingsForUser(legacyUser);
        assertEquals(SendMode.BATCHED, settings.getSendMode());
        assertEquals(42, settings.getBatchFeatureDiscoveryShowCount());
    }

    @Test
    public void testGetSettingsForUserPrefersNewKeysOverLegacy() {
        UserSettings userSettings = DefaultUserSettings.builder(this.userSettings) //Apply the new keys, set the legacy
                .put(KEY_LEGACY_DISCOVERY_COUNT, 42L)
                .put(KEY_LEGACY_SEND_MODE, SendMode.BATCHED.name())
                .build();

        ApplicationUser mixedUser = mock(ApplicationUser.class);
        when(mixedUser.getId()).thenReturn(100);

        when(userSettingsService.getUserSettings(UserKey.fromLong(100L))).thenReturn(userSettings);

        UserNotificationSettings settings = service.getSettingsForUser(mixedUser);
        assertEquals(SendMode.IMMEDIATE, settings.getSendMode());
        assertEquals(5, settings.getBatchFeatureDiscoveryShowCount());
    }

    @SuppressWarnings("ConstantConditions")
    @Test(expected = NullPointerException.class)
    public void testGetSettingsForUserWithNull() {
        service.getSettingsForUser(null);
    }

    @Test
    public void testUpdateSettingsForCurrentUser() {
        UserNotificationSettings settings = new UserNotificationSettings.Builder()
                .sendMode(SendMode.BATCHED)
                .batchFeatureDiscoveryShowCount(5)
                .build();

        UserSettings newSettings = updateSettingsForUser(user, settings);

        assertEquals(SendMode.BATCHED.name(), newSettings.getString(KEY_SEND_MODE).getOrNull());
        assertEquals(5L, newSettings.getLong(KEY_DISCOVERY_COUNT).getOrElse(0L).longValue());
    }

    @Test
    public void testUpdateSettingsForOtherUserAsAdmin() {
        UserNotificationSettings settings = new UserNotificationSettings.Builder()
                .sendMode(SendMode.BATCHED)
                .build();
        service.updateSettingsForUser(otherUser, settings);
        verify(userSettingsService).updateUserSettings(eq(UserKey.fromLong(OTHER_USER_ID)), any(Function.class));
    }

    @Test
    public void testPartialUpdate() {
        UserNotificationSettings settings = new UserNotificationSettings.Builder()
                .sendMode(SendMode.BATCHED)
                .build();

        // Original settings are IMMEDIATE and 5, so the partial update provided should change
        // the mode without changing the count
        UserSettings newSettings = updateSettingsForUser(user, settings);
        assertEquals(SendMode.BATCHED.name(), newSettings.getString(KEY_SEND_MODE).getOrNull());
        assertEquals(Long.valueOf(5), newSettings.getLong(KEY_DISCOVERY_COUNT).getOrNull());
    }

    @Test
    public void testAnalyticsEventWhenSettingChanged() {
        UserNotificationSettings settings = new UserNotificationSettings.Builder()
                .sendMode(SendMode.BATCHED)
                .build();

        updateSettingsForUser(user, settings);

        ArgumentCaptor<NotificationSendModePreferenceChangeEvent> eventCaptor = ArgumentCaptor.forClass(NotificationSendModePreferenceChangeEvent.class);
        verify(eventPublisher).publish(eventCaptor.capture());
        NotificationSendModePreferenceChangeEvent event = eventCaptor.getValue();

        assertEquals(MODECHANGED_BATCHED_EVENT_NAME, event.getAnalyticsEventName());
        assertEquals(user, event.getAffectedUser());
        assertEquals(SendMode.BATCHED, event.getDefaultSendMode());
        assertEquals(SendMode.BATCHED, event.getNewSendMode());

        // This isn't an ideal way of testing the getUserIsAffectedUser computed property on the event, but due
        // to the way the real EventPublisher sets the user property (via reflection) it will have to do.
        setEventUser(event, user);
        assertTrue(event.getUserIsAffectedUser());

        setEventUser(event, otherUser);
        assertFalse(event.getUserIsAffectedUser());
    }

    @Test
    public void testNoAnalyticsEventWhenSettingNotChanged() {
        UserNotificationSettings settings = new UserNotificationSettings.Builder()
                .sendMode(SendMode.IMMEDIATE)
                .build();
        updateSettingsForUser(user, settings);
        verifyZeroInteractions(eventPublisher);
    }

    private UserSettings updateSettingsForUser(ApplicationUser user, UserNotificationSettings settings) {
        service.updateSettingsForUser(user, settings);

        ArgumentCaptor<Function> captor = ArgumentCaptor.forClass(Function.class);
        verify(userSettingsService, times(1)).updateUserSettings(eq(UserKey.fromLong(USER_ID)), captor.capture());

        // Create a builder from the base userSettings created during setup() to allow the update code
        // to correctly get access to the "current" settings
        return (UserSettings) captor.getValue().apply(DefaultUserSettings.builder(userSettings));
    }

    private void setEventUser(ApplicationEvent event, ApplicationUser user) {
        new DirectFieldAccessor(event).setPropertyValue("user", user);
    }
}