package com.atlassian.stash.internal.watcher;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.event.watch.WatcherAddedEvent;
import com.atlassian.bitbucket.event.watch.WatcherRemovedEvent;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.watcher.Watchable;
import com.atlassian.bitbucket.watcher.Watcher;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Set;

import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultWatcherServiceTest {

    private static final int JOES_ID = 0;
    private static final int MARTYS_ID = 1;

    private final InternalNormalUser joe = new InternalNormalUser.Builder().id(JOES_ID).name("Joe Bloggs").build();
    private final InternalNormalUser marty = new InternalNormalUser.Builder().id(MARTYS_ID).name("Marty McFly").build();

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private EventPublisher eventPublisher;
    @InjectMocks
    private DefaultWatcherService service;
    @Captor
    private ArgumentCaptor<WatcherAddedEvent> watcherAddedEventCaptor;
    @Captor
    private ArgumentCaptor<InternalWatcher> watcherCaptor;
    @Mock
    private WatcherDao watcherDao;
    @Captor
    private ArgumentCaptor<WatcherRemovedEvent> watcherRemovedEventCaptor;

    @Before
    public void setUpAuthentication() {
        when(authenticationContext.getCurrentUser()).thenReturn(joe);
    }

    @Test
    public void testCountZeroWatchers() {
        Watchable watchable = mock(Watchable.class);
        when(watcherDao.countWatchers(watchable)).thenReturn(0L);
        assertEquals(0, service.countWatchers(watchable));
    }

    @Test
    public void testCountOneWatcher() {
        Watchable watchable = mock(Watchable.class);
        when(watcherDao.countWatchers(watchable)).thenReturn(1L);
        assertEquals(1, service.countWatchers(watchable));
    }

    @Test
    public void testFindZeroWatchers() {
        Watchable watchable = mock(Watchable.class);
        Set<InternalWatcher> watchers = Sets.newHashSet();
        when(watcherDao.findByWatchable(watchable)).thenReturn(watchers);
        assertEquals(watchers, service.findWatchers(watchable));
    }

    @Test
    public void testFindOneWatcher() {
        InternalWatcher watcher = mock(InternalWatcher.class);
        Watchable watchable = mock(Watchable.class);
        Set<InternalWatcher> watchers = Sets.newHashSet(watcher);
        when(watcherDao.findByWatchable(watchable)).thenReturn(watchers);
        assertEquals(watchers, service.findWatchers(watchable));
    }

    @Test
    public void testCreatingWatcherForCurrentUser() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        service.addCurrentUserAsWatcher(watchable);

        verify(watcherDao).create(watcherCaptor.capture());
        InternalWatcher watcher = watcherCaptor.getValue();

        // the id should be null because it has not yet been persisted
        assertNull(watcher.getId());

        // the user should be joe because he is the current user
        assertEquals(JOES_ID, watcher.getUser().getId());

        assertEquals(watchable, watcher.getWatchable());
    }

    @Test
    public void testCreatingWatcher() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        service.addUserAsWatcher(watchable, marty);

        verify(watcherDao).create(watcherCaptor.capture());
        InternalWatcher watcher = watcherCaptor.getValue();

        // the id should be null because it has not yet been persisted
        assertNull(watcher.getId());

        // the user should be joe because he is the current user
        assertEquals(MARTYS_ID, watcher.getUser().getId());

        assertEquals(watchable, watcher.getWatchable());
    }

    @Test
    public void testCannotWatchTwice() {
        // Set up the fixture so that joe is already a watcher
        InternalWatchable watchable = mock(InternalWatchable.class);
        InternalWatcher watcher = mock(InternalWatcher.class);
        when(watcherDao.findByWatchableAndUser(watchable, joe)).thenReturn(watcher);

        // attempt to add joe as a watcher (again)
        Watcher returnedWatcher = service.addCurrentUserAsWatcher(watchable);

        // did we detect that joe is already watching?
        verify(watcherDao, never()).create(any(InternalWatcher.class));

        // did we correctly return joe as a watcher?
        assertSame(watcher, returnedWatcher);
    }

    @Test
    public void testPublishingWatcherAdded() {
        InternalWatcher watcher = mock(InternalWatcher.class);
        when(watcherDao.create(any(InternalWatcher.class))).thenReturn(watcher);

        InternalWatchable watchable = mock(InternalWatchable.class);
        service.addCurrentUserAsWatcher(watchable);

        verify(eventPublisher).publish(watcherAddedEventCaptor.capture());
        WatcherAddedEvent event = watcherAddedEventCaptor.getValue();

        assertSame(service, event.getSource());
        assertSame(watcher, event.getWatcher());
    }

    @Test
    public void testReturnAddedWatcherForCurrentUser() {
        InternalWatcher watcher = mock(InternalWatcher.class);
        when(watcherDao.create(any(InternalWatcher.class))).thenReturn(watcher);

        InternalWatchable watchable = mock(InternalWatchable.class);
        assertSame(watcher, service.addCurrentUserAsWatcher(watchable));
    }

    @Test
    public void testReturnAddedWatcher() {
        InternalWatcher watcher = mock(InternalWatcher.class);
        when(watcherDao.create(any(InternalWatcher.class))).thenReturn(watcher);

        InternalWatchable watchable = mock(InternalWatchable.class);
        assertSame(watcher, service.addUserAsWatcher(watchable, marty));
    }

    @Test
    public void testRemovingNonWatcher() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        when(watcherDao.findByWatchableAndUser(watchable, joe)).thenReturn(null);
        assertFalse(service.removeCurrentUserAsWatcher(watchable));
        assertFalse(service.removeUserAsWatcher(watchable, marty));
    }

    @Test
    public void testRemoveCurrentUserAsWatcher() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        InternalWatcher watcher = mock(InternalWatcher.class);

        when(watcherDao.findByWatchableAndUser(watchable, joe)).thenReturn(watcher);
        when(watcher.getWatchable()).thenReturn(watchable);

        assertTrue(service.removeCurrentUserAsWatcher(watchable));
        verify(watcherDao).delete(watcher);
    }

    @Test
    public void testRemoveUserAsWatcher() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        InternalWatcher watcher = mock(InternalWatcher.class);

        when(watcherDao.findByWatchableAndUser(watchable, marty)).thenReturn(watcher);
        when(watcher.getWatchable()).thenReturn(watchable);

        assertTrue(service.removeUserAsWatcher(watchable, marty));
        verify(watcherDao).delete(watcher);
    }

    @Test
    public void testPublishingWatcherRemoved() {
        InternalWatchable watchable = mock(InternalWatchable.class);
        InternalWatcher watcher = mock(InternalWatcher.class);

        when(watcherDao.findByWatchableAndUser(watchable, joe)).thenReturn(watcher);
        when(watcher.getWatchable()).thenReturn(watchable);

        service.removeCurrentUserAsWatcher(watchable);
        verify(eventPublisher).publish(watcherRemovedEventCaptor.capture());
        WatcherRemovedEvent event = watcherRemovedEventCaptor.getValue();

        assertSame(service, event.getSource());
        assertSame(watcher, event.getWatcher());
    }

    @Test
    public void testIsCurrentUserWatching() {
        InternalWatchable watching = mock(InternalWatchable.class);
        InternalWatchable notWatching = mock(InternalWatchable.class);
        InternalWatcher watcher = mock(InternalWatcher.class);

        when(watcherDao.findByWatchableAndUser(watching, joe)).thenReturn(watcher);
        when(watcherDao.findByWatchableAndUser(notWatching, joe)).thenReturn(null);
        when(watcher.getWatchable()).thenReturn(watching);

        assertTrue(service.isCurrentUserWatching(watching));
        assertFalse(service.isCurrentUserWatching(notWatching));
    }
}
