package com.atlassian.stash.internal.web.fragments;

import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.hostcontainer.HostContainer;
import com.atlassian.plugin.module.ModuleFactory;
import com.atlassian.plugin.web.ContextProvider;
import com.atlassian.plugin.web.WebFragmentHelper;
import com.atlassian.plugin.web.descriptors.ConditionElementParser;
import com.atlassian.stash.internal.plugin.web.fragments.ClientWebInterfaceManager;
import com.atlassian.stash.internal.plugin.web.fragments.ClientWebPanelModuleDescriptor;
import com.google.common.collect.Sets;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

@RunWith(MockitoJUnitRunner.class)
public class ClientWebPanelModuleDescriptorTest {

    @Mock
    private ClientWebInterfaceManager clientWebInterfaceManager;
    @Mock
    private ConditionElementParser.ConditionFactory conditionFactory;
    @Mock
    private Map<String, Object> contextInput;
    @Mock
    private ContextProvider contextProvider;
    @Mock
    private HostContainer hostContainer;
    @Mock
    private ModuleFactory moduleFactory;
    @Mock
    private Plugin plugin;
    @Mock
    private WebFragmentHelper webFragmentHelper;

    @InjectMocks
    private ClientWebPanelModuleDescriptor clientWebPanelModuleDescriptor;

    private static final float EPSILON = 0.1f;

    private static final String PLUGIN_KEY = "PLUGIN_KEY";

    private static final String KEY = "KEY";
    private static final String LOCATION = "LOCATION";
    private static final String VIEW_LOCATION_RESOURCE = "VIEW_LOCATION_RESOURCE";
    private static final String VIEW_LOCATION_TEMPLATE = "VIEW_LOCATION_TEMPLATE";
    private static final String CONTEXT_PROVIDER = "CONTEXT_PROVIDER";
    private static final String CONTEXT_PROVIDER_CLASS = "com.atlassian.stash.internal.web.fragments.PassThroughContextProvider";
    private static final String CONDITION = "CONDITION";
    private static final String ALWAYS_FALSE_CONDITION_CLASS = "com.atlassian.stash.internal.web.fragments.AlwaysFalseCondition";
    private static final String ALWAYS_TRUE_CONDITION_CLASS = "com.atlassian.stash.internal.web.fragments.AlwaysTrueCondition";

    private static final String DEPENDENCY = "DEPENDENCY";

    private static final float DEFAULT_WEIGHT = 1000;

    private static final String PARAM_A_NAME = "A";
    private static final String PARAM_A_TYPE = "JS";
    private static final String PARAM_A_VALUE = "PARAM_A";

    private static final String PARAM_B_NAME = "B";
    private static final String PARAM_B_TYPE = "TEXT";
    private static final String PARAM_B_VALUE = "PARAM_B";

    private static final String PARAM_C_NAME = "C";
    private static final String PARAM_C_VALUE = "PARAM_C";

    private static final String DEFAULT_PARAM_TYPE = "TEXT";

    private String getWebItemXml() {
        return getWebItemXml(ALWAYS_FALSE_CONDITION_CLASS, true);
    }
    private String getWebItemXml(String conditionClass, boolean useViewResource) {
        String view;
        if (useViewResource) {
            view = "   <resource name=\"view\" type=\"soy\" location=\"" + VIEW_LOCATION_RESOURCE + "/" + VIEW_LOCATION_TEMPLATE + "\"" + "/>";
        } else {
            view = "   <view>" + VIEW_LOCATION_TEMPLATE + "</view>" +
                   "   <dependency>" + VIEW_LOCATION_RESOURCE + "</dependency>";
        }

        return
        "<client-web-panel key=\"" + KEY + "\" location=\"" + LOCATION + "\">" +
            view +
        "   <context-provider class=\"" + CONTEXT_PROVIDER_CLASS + "\" />" +
        "   <condition class=\"" + conditionClass + "\" />" +
        "   <client-context-provider>" + CONTEXT_PROVIDER + "</client-context-provider>" +
        "   <client-condition>" + CONDITION + "</client-condition>" +
        "   <param name=\"" + PARAM_A_NAME + "\" type=\"" + PARAM_A_TYPE + "\" value=\"" + PARAM_A_VALUE + "\" />" +
        "   <param name=\"" + PARAM_B_NAME + "\" type=\"" + PARAM_B_TYPE + "\" value=\"" + PARAM_B_VALUE + "\" />" +
        "   <param name=\"" + PARAM_C_NAME + "\" value=\"" + PARAM_C_VALUE + "\" />" +
        "   <junk>junk value</junk>" +
        "   <dependency>" + DEPENDENCY + "</dependency>" +
        "</client-web-panel>";
    }

    @Before
    public void setupPlugin() throws Exception {
        when(plugin.getKey()).thenReturn(PLUGIN_KEY);
        when(webFragmentHelper.loadContextProvider(eq(CONTEXT_PROVIDER_CLASS), eq(plugin))).thenReturn(contextProvider);
        when(plugin.loadClass(eq(ALWAYS_FALSE_CONDITION_CLASS), any(Class.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return AlwaysFalseCondition.class;
            }
        });
        when(conditionFactory.create(AlwaysFalseCondition.class.getName(), plugin)).thenReturn(new AlwaysFalseCondition());
        when(plugin.loadClass(eq(ALWAYS_TRUE_CONDITION_CLASS), any(Class.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return AlwaysTrueCondition.class;
            }
        });
        when(conditionFactory.create(AlwaysTrueCondition.class.getName(), plugin)).thenReturn(new AlwaysTrueCondition());
    }

    private Element parseElement(String elementXml) throws Exception {
        return new SAXReader().read(new StringReader(elementXml)).getRootElement();
    }

    private Set<String> getDependencyKeys(Element container) {
        List dependencies = container.elements("dependency");
        Set<String> dependencyKeys = Sets.newHashSet();
        for (Object dependency : dependencies) {
            Element dep = (Element) dependency;
            dependencyKeys.add(dep.getText());
        }
        return dependencyKeys;
    }

    private Element getConfigTransformerWithAssertions(Element container) {
        Element transformation = container.element("transformation");
        assertNotNull("Generated element has a <transformation> child element", transformation);
        assertEquals("<transformation> element is for config extension", "config", transformation.attributeValue("extension"));

        Element transformer = transformation.element("transformer");
        assertNotNull("<transformation> element has a <transformer> child element", transformer);
        assertEquals("Transformer key should be for client web panel", "client-web-panel-transformer", transformer.attributeValue("key"));

        return transformer;
    }

    private void assertFieldElement(Element transformer, String name, String type, String content) {
        Element e = transformer.element(name);
        assertNotNull("Transformer should have an element named " + name, e);
        assertEquals("Transformer element <" + name + "> should have type " + type, type, e.attributeValue("type"));
        assertEquals("Transformer element <" + name + "> should have value " + content, content, e.getText());
    }

    private void assertFieldElement(Element transformer, String name, String type, float content) {
        Element e = transformer.element(name);
        assertNotNull("Transformer should have an element named " + name, e);
        assertEquals("Transformer element <" + name + "> should have type " + type, type, e.attributeValue("type"));
        assertEquals("Transformer element <" + name + "> should have value " + content, content, Float.parseFloat(e.getText()), EPSILON);
    }

    @Test
    public void testEnableCallsRefresh() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        clientWebPanelModuleDescriptor.enabled();
        verify(clientWebInterfaceManager, times(1)).refresh();
    }

    @Test
    public void testCanGetPageDataProvider() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml(ALWAYS_TRUE_CONDITION_CLASS, true)));
        clientWebPanelModuleDescriptor.enabled();
        ContextProvider pageDataProvider = clientWebPanelModuleDescriptor.getPageDataProvider();
        assertTrue("Returns ContextProvider", pageDataProvider != null);
        pageDataProvider.getContextMap(contextInput);
        verify(contextProvider).getContextMap(eq(contextInput));
    }

    @Test
    public void testNoDataAddedForFalseServerCondition() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        clientWebPanelModuleDescriptor.enabled();
        ContextProvider pageDataProvider = clientWebPanelModuleDescriptor.getPageDataProvider();
        assertTrue("Returns ContextProvider", pageDataProvider != null);
        Map<String, Object> outputContext = pageDataProvider.getContextMap(contextInput);
        assertEquals("Has serverCondition property", false, outputContext.get("serverCondition"));
        assertFalse("Has no serverContext property", outputContext.containsKey("serverContext"));
    }

    @Test
    public void testInitGeneratesTransformerElement() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        Element generated = clientWebPanelModuleDescriptor.getElement();

        Element transformer = getConfigTransformerWithAssertions(generated);

        assertFieldElement(transformer, "key", "STRING", KEY);
        assertFieldElement(transformer, "location", "STRING", LOCATION);
        assertFieldElement(transformer, "view", "JS", VIEW_LOCATION_TEMPLATE);
        assertFieldElement(transformer, "context-provider", "JS", CONTEXT_PROVIDER);
        assertFieldElement(transformer, "condition", "JS", CONDITION);
        assertFieldElement(transformer, "weight", "NUMBER", DEFAULT_WEIGHT);

        Element params = transformer.element("params");
        assertNotNull("Transformer should have a <params> element", params);

        assertFieldElement(params, PARAM_A_NAME, PARAM_A_TYPE, PARAM_A_VALUE);
        assertFieldElement(params, PARAM_B_NAME, PARAM_B_TYPE, PARAM_B_VALUE);
        assertFieldElement(params, PARAM_C_NAME, DEFAULT_PARAM_TYPE, PARAM_C_VALUE);

        assertNull("<junk> element should be ignored", transformer.element("junk"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testServerConditionsRemoved() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        Element generated = clientWebPanelModuleDescriptor.getElement();

        List<Element> conditions = generated.elements("condition");
        assertThat("No conditions should be left in the generated element.", conditions, hasSize(0));
    }

    @Test
    public void testInitAddsDependencies() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        Element generated = clientWebPanelModuleDescriptor.getElement();

        Set<String> dependencyKeys = getDependencyKeys(generated);

        assertTrue("Original dependency was copied over", dependencyKeys.contains(DEPENDENCY));
        assertTrue("A dependency was added on the web-fragment-manager web resource",
                dependencyKeys.contains("com.atlassian.bitbucket.server.bitbucket-client-web-fragments:web-fragment-manager"));
        assertTrue("A dependency was added for the Soy view's resource", dependencyKeys.contains(VIEW_LOCATION_RESOURCE));
    }

    @Test
    public void testPlaceholderResourceAdded() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml()));
        Element generated = clientWebPanelModuleDescriptor.getElement();

        List resources = generated.elements("resource");
        Element resource = null;
        for (Object o : resources) {
            Element e = (Element) o;
            if ("/web-fragment-placeholder.config".equals(e.attributeValue("location"))) {
                resource = e;
                break;
            }
        }

        assertNotNull("Placeholder resource for '/web-fragment-placeholder.config' was added", resource);
        assertEquals("Placeholder resource is type download", "download", resource.attributeValue("type"));

        List resourceParams = resource.elements("param");
        Element sourceParam = null;
        for (Object p : resourceParams) {
            Element param = (Element) p;
            if ("source".equals(param.attributeValue("name"))) {
                sourceParam = param;
                break;
            }
        }
        assertNotNull("Source param was provided for the resource", sourceParam);
        assertEquals("Source is webContextStatic", "webContextStatic", sourceParam.attributeValue("value"));
    }

    @Test
    public void testUsingViewElement() throws Exception {
        clientWebPanelModuleDescriptor.init(plugin, parseElement(getWebItemXml(ALWAYS_FALSE_CONDITION_CLASS, false)));
        Element generated = clientWebPanelModuleDescriptor.getElement();
        Set<String> dependencyKeys = getDependencyKeys(generated);
        assertTrue("A dependency was added for the Soy view's resource", dependencyKeys.contains(VIEW_LOCATION_RESOURCE));

        Element transformer = getConfigTransformerWithAssertions(generated);
        assertFieldElement(transformer, "view", "JS", VIEW_LOCATION_TEMPLATE);
    }
}
