package com.mobify.astro.plugins;

import android.net.Uri;
import android.support.test.annotation.UiThreadTest;

import com.mobify.astro.ActivityTestBase;
import com.mobify.astro.PluginResolver;
import com.mobify.astro.TestPlugin;
import com.mobify.astro.messaging.EventManager;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.plugins.webviewplugin.AstroWebView;
import com.mobify.astro.plugins.webviewplugin.LoadingContainerView;
import com.mobify.astro.plugins.webviewplugin.WebViewPlugin;
import com.mobify.astro.utilities.AstroFileUtilities;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.net.URISyntaxException;
import java.util.Deque;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class NavigationPluginTest extends ActivityTestBase {

    PluginResolver mockPluginResolver;
    EventManager eventManager;
    MessageSender messageSender;
    NavigationPlugin navigationPlugin;
    WebViewPlugin webViewPlugin;
    Deque<NavigationStackFrame> navigationStack;
    TestPlugin testPlugin;

    @Before
    public void setUp() throws Exception {
        mockPluginResolver = mock(PluginResolver.class);
        eventManager = new EventManager();
        messageSender = new MessageSender(eventManager);
        navigationPlugin = new NavigationPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        navigationStack = navigationPlugin.navigationStack;

        testPlugin = new TestPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        when(mockPluginResolver.instanceForAddress(testPlugin.getInstanceAddress())).thenReturn(testPlugin);
    }

    @After
    public void tearDown() {
        validateMockitoUsage();
    }

    public NavigationPlugin makeNavigationPluginSpyWithMocksSize(int size) {
        NavigationPlugin navPlugin = new NavigationPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);

        // Populate the navigation stack with mock stack frames
        for (int i = 0; i < size; i++) {
            WebViewPlugin frame = mock(WebViewPlugin.class);
            doReturn(mock(AstroWebView.class)).when(frame).getWebView();
            when(frame.hasWebView()).thenReturn(true);
            navPlugin.navigationStack.push(frame);
        }
        return spy(navPlugin);
    }

    public NavigationPlugin makeNavigationPluginWithSize(int size) {
        NavigationPlugin navPlugin = new NavigationPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);

        // Populate the navigation stack with stack frames
        for (int i = 0; i < size; i++) {
            NavigationStackFrame frame = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
            navPlugin.navigationStack.push(frame);
            navPlugin.addressStack.push(String.format("Frame: %d", i));
        }
        return navPlugin;
    }

    @UiThreadTest
    @Test
    public void testInitialize() {
        assertNotNull(navigationPlugin.getView());
    }

    @UiThreadTest
    @Test
    public void testGetView() {
        assertEquals(LoadingContainerView.class, navigationPlugin.getView().getClass());
    }

    @UiThreadTest
    @Test
    public void testNavigateWithNewWebView() {
        String url = "file:///test.html";
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), null);
        } catch (Exception e) {
            fail();
        }

        NavigationStackFrame lastFrame = navigationStack.peekLast();

        assertEquals(1, navigationStack.size());

        Uri expectedUri = null;
        try {
            expectedUri = AstroFileUtilities.createAndroidAssetUri(Uri.parse(url));
        } catch (URISyntaxException e) {
            fail();
        }
        assertEquals(expectedUri.toString(), lastFrame.getUrl());
    }

    @UiThreadTest
    @Test
    public void testNavigateNoOptions() {
        String url = "file:///android_asset/test.html";
        navigationStack = navigationPlugin.navigationStack;
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        Uri uriObject = null;

        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), null);

            uriObject = Uri.parse(url);
            if (AstroFileUtilities.isFileUri(uriObject)) {
                uriObject = AstroFileUtilities.createAndroidAssetUri(uriObject);
            }
        } catch(Exception e) {
            fail();
        }

        NavigationStackFrame lastFrame = navigationStack.peekLast();

        assertEquals(1, navigationStack.size());
        assertEquals(uriObject.toString(), lastFrame.getUrl());
    }

    @UiThreadTest
    @Test
    public void testNavigateEmptyOptions() {
        String url = "file:///test.html";
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        Uri uriObject = null;
        JSONObject data = new JSONObject();

        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), data);

            uriObject = Uri.parse(url);
            if (AstroFileUtilities.isFileUri(uriObject)) {
                uriObject = AstroFileUtilities.createAndroidAssetUri(uriObject);
            }
        } catch(Exception e) {
            fail();
        }

        NavigationStackFrame lastFrame = navigationStack.peekLast();

        assertEquals(1, navigationStack.size());
        assertEquals(uriObject.toString(), lastFrame.getUrl());
    }

    @UiThreadTest
    @Test
    public void testNavigateStackEnabled() {
        String url = "file:///test.html";
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        JSONObject data = new JSONObject();
        Uri uriObject = null;

        try {
            data.put("stack", true);
        } catch (JSONException e) {
            fail();
        }

        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), data);

            uriObject = Uri.parse(url);
            if (AstroFileUtilities.isFileUri(uriObject)) {
                uriObject = AstroFileUtilities.createAndroidAssetUri(uriObject);
            }
        } catch(Exception e) {
            fail();
        }

        NavigationStackFrame lastFrame = navigationStack.peekLast();

        assertEquals(1, navigationStack.size());
        assertEquals(uriObject.toString(), lastFrame.getUrl());
    }

    @UiThreadTest
    @Test
    public void testNavigateToPlugin() throws Exception {
        navigationPlugin.navigateToPlugin(testPlugin.getInstanceAddress(), new JSONObject("{}"));

        assertEquals(1, navigationStack.size());
        assertEquals(testPlugin.getView(), navigationStack.peekLast().getView());
    }

    @UiThreadTest
    @Test
    public void testDestroy() {
        LoadingContainerView mockContainerView = mock(LoadingContainerView.class);

        // Pop a new web view on the stack to test that it gets removed when we destroy
        String url = "file:///test.html";
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), null);
        } catch (Exception e) {
            fail();
        }

        navigationPlugin.setContainerView(mockContainerView);
        navigationPlugin.destroy();

        verify(mockContainerView).clearChildView();
        assertEquals(0, navigationStack.size());
    }

    @UiThreadTest
    @Test
    public void testCurrentUrlWithWebView() {
        EventManager eventManager = new EventManager();
        MessageSender messageSender = new MessageSender(eventManager);
        navigationPlugin = makeNavigationPluginWithSize(1);
        String url = "file:///test.html";
        webViewPlugin = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        doReturn(webViewPlugin).when(mockPluginResolver).instanceForAddress(webViewPlugin.getInstanceAddress());
        try {
            webViewPlugin.navigate(url);
            navigationPlugin.navigateToPlugin(webViewPlugin.getInstanceAddress(), null);
        } catch (Exception e) {
            fail();
        }
        Uri expectedUri = null;
        try {
            expectedUri = AstroFileUtilities.createAndroidAssetUri(Uri.parse(url));
        } catch (URISyntaxException e) {
            fail();
        }
        assertEquals(expectedUri.toString(), navigationPlugin.currentUrl());
    }

    @UiThreadTest
    @Test
    public void testGetWebViewCount() {
        navigationPlugin = makeNavigationPluginWithSize(7);

        assertEquals(7, navigationPlugin.getActiveViewCount());
    }

    @UiThreadTest
    @Test
    public void testCanGoBackEmptyStack() {
        assertFalse(navigationPlugin.canGoBack());
    }

    @UiThreadTest
    @Test
    public void testCanGoBack() {
        navigationPlugin = makeNavigationPluginWithSize(3);
        assertTrue(navigationPlugin.canGoBack());
    }

    @UiThreadTest
    @Test
    public void testRestoreViews() {
        navigationPlugin = makeNavigationPluginWithSize(4);

        // Save the first frame so we can test restoring it
        NavigationStackFrame firstFrame = navigationPlugin.navigationStack.peekFirst();
        firstFrame.saveAndDestroyView();

        navigationPlugin.restoreViews();

        WebViewPlugin webViewPlugin = (WebViewPlugin) firstFrame;
        assertNotNull(webViewPlugin.getWebView());
    }

    @UiThreadTest
    @Test
    public void testRestoreWebViewsWithMoreThanMaxViews() {
        int numFrames = 7;
        navigationPlugin = makeNavigationPluginWithSize(numFrames);

        // Save all seven frames
        for (NavigationStackFrame frame: navigationPlugin.navigationStack) {
            frame.saveAndDestroyView();
        }

        navigationPlugin.restoreViews();

        int numHydratedFrames = 0;
        int numSavedFrames = 0;
        int frameNum = 0;
        int lastFourFramesStartIndex = numFrames - 4;
        for (NavigationStackFrame frame: navigationPlugin.navigationStack) {
            WebViewPlugin webViewPluginFrame = (WebViewPlugin) frame;
            if (webViewPluginFrame.getWebView() == null) {
                numSavedFrames++;
            } else {
                numHydratedFrames++;
            }

            // Root and last four should be restored
            if (frameNum == 0 || frameNum >= lastFourFramesStartIndex) {
                assertNotNull("Frame " + frameNum + " should be hydrated", webViewPluginFrame.getWebView());
            } else {
                assertNull("Frame " + frameNum + " should be null", webViewPluginFrame.getWebView());
            }
            frameNum++;
        }
        assertEquals("Incorrect number of hydrated frames", NavigationPlugin.MAX_VIEWS, numHydratedFrames);
        assertEquals("Incorrect number of saved frames", numFrames - NavigationPlugin.MAX_VIEWS, numSavedFrames);
    }

    @UiThreadTest
    @Test
    public void testReload() {
        navigationPlugin = makeNavigationPluginSpyWithMocksSize(1);

        NavigationStackFrame frame = navigationPlugin.navigationStack.peekFirst();

        navigationPlugin.reload();
        verify(frame).reload();
    }

    @UiThreadTest
    @Test
    public void testGoBack() {
        navigationPlugin = makeNavigationPluginWithSize(3);
        navigationPlugin.goBack();

        assertEquals(2, navigationPlugin.navigationStack.size());
    }

    @UiThreadTest
    @Test
    public void testWebviewDisposeWhenPopped() {
        navigationPlugin = makeNavigationPluginWithSize(2);
        WebViewPlugin noDispose = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender);
        WebViewPlugin noDisposeSpy = spy(noDispose);
        navigationPlugin.navigationStack.add(noDisposeSpy);
        navigationPlugin.goBack();
        verify(noDisposeSpy, never()).destroy();

        JSONObject options = new JSONObject();
        try {
            options.put("disposeWhenPopped", true);
        } catch (JSONException e) {
            fail();
        }
        WebViewPlugin dispose = new WebViewPlugin(getActivity(), mockPluginResolver, eventManager, messageSender, options);
        WebViewPlugin disposeSpy = spy(dispose);
        navigationPlugin.navigationStack.add(disposeSpy);
        navigationPlugin.goBack();
        verify(disposeSpy, times(1)).destroy();
    }

    @UiThreadTest
    @Test
    public void testGoBackMaintainsMaxViews() {
        navigationPlugin = makeNavigationPluginWithSize(6);

        NavigationStackFrame firstFrame = navigationPlugin.navigationStack.peekFirst();
        firstFrame.saveAndDestroyView();

        assertEquals(NavigationPlugin.MAX_VIEWS, navigationPlugin.getActiveViewCount());
        navigationPlugin.goBack();
        assertEquals(NavigationPlugin.MAX_VIEWS, navigationPlugin.getActiveViewCount());

        WebViewPlugin webViewPluginFrame = (WebViewPlugin) firstFrame;
        assertNotNull(webViewPluginFrame.getWebView());
    }

    @UiThreadTest
    @Test
    public void testPopToRoot() {
        navigationPlugin = makeNavigationPluginWithSize(7);
        navigationPlugin.popToRoot(null);
        assertEquals("Should be one active view remaining after popToRoot", 1, navigationPlugin.getActiveViewCount());

        WebViewPlugin webViewPluginFrame = (WebViewPlugin) navigationPlugin.navigationStack.getFirst();
        assertNotNull("Root should be hydrated", webViewPluginFrame.getWebView());
    }
}
