package de.renew.propertymanagementgui.gui;

import java.awt.Dimension;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JPanel;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;

import de.renew.propertymanagement.prop.ConfigurableProperty;
import de.renew.propertymanagementgui.reactivecomponents.State;

import static org.mockito.Mockito.mock;

/**
 * Unit test class for ContentComponent
 *
 * @author 7kraft
 */
class ContentComponentTest {

    // helpers

    private ConfigurableProperty mockProperty(String pluginName) {
        ConfigurableProperty property = Mockito.mock(ConfigurableProperty.class);
        Mockito.when(property.getPluginName()).thenReturn(pluginName);
        return property;
    }

    private Map<String, ConfigurableProperty> sampleProperties() {
        Map<String, ConfigurableProperty> map = new LinkedHashMap<>();
        map.put("p1", mockProperty("Plugin A"));
        map.put("p2", mockProperty("Plugin B"));
        map.put("p3", mockProperty("Plugin A"));
        return map;
    }

    private static Field field(
        @SuppressWarnings("SameParameterValue") Class<?> theClass,
        @SuppressWarnings("SameParameterValue") String name) throws Exception
    {
        Field field = theClass.getDeclaredField(name);
        field.setAccessible(true);
        return field;
    }

    private static Method method(
        @SuppressWarnings("SameParameterValue") Class<?> theClass, String name, Class<?>... params)
        throws Exception
    {
        Method method = theClass.getDeclaredMethod(name, params);
        method.setAccessible(true);
        return method;
    }

    // test methods

    @Test
    public void testContentComponent() throws Exception {
        //given/when
        ContentComponent cc = new ContentComponent(sampleProperties());

        //then
        Object obj = field(ContentComponent.class, "_propertiesByPlugins").get(cc);
        Assertions.assertThat(obj).isNotNull();
        Assertions.assertThat(obj).isInstanceOf(Map.class);

        Map<?, ?> grouped = (Map<?, ?>) obj;

        Assertions.assertThat(grouped.keySet().stream().allMatch(k -> k instanceof String))
            .isTrue();
        Set<String> keys =
            grouped.keySet().stream().map(k -> (String) k).collect(Collectors.toSet());
        Assertions.assertThat(keys).isEqualTo(Set.of("Plugin A", "Plugin B"));

        Object objA = grouped.get("Plugin A");
        Object objB = grouped.get("Plugin B");
        Assertions.assertThat(objA).isInstanceOf(List.class);
        Assertions.assertThat(objB).isInstanceOf(List.class);
        Assertions.assertThat(((List<?>) objA).size()).isEqualTo(2);
        Assertions.assertThat(((List<?>) objB).size()).isEqualTo(1);
    }

    @Test
    public void testRender() {
        //given
        ContentComponent cc = new ContentComponent(sampleProperties());

        JPanel sidePanel = new JPanel();
        JPanel pagePanel = new JPanel();

        try (
            MockedConstruction<SideNavComponent> mockedSide = Mockito.mockConstruction(
                SideNavComponent.class,
                (mock, ctx) -> Mockito.when(mock.get()).thenReturn(sidePanel));
            MockedConstruction<PageComponent> mockedPage = Mockito.mockConstruction(
                PageComponent.class,
                (mock, ctx) -> Mockito.when(mock.get()).thenReturn(pagePanel))) {

            //when
            JComponent content = cc.render();

            //then
            Assertions.assertThat(content).isNotNull();
            Assertions.assertThat(content.getLayout()).isInstanceOf(BoxLayout.class);
            Dimension max = content.getMaximumSize();
            Assertions.assertThat(max.width).isEqualTo(Integer.MAX_VALUE);
            Assertions.assertThat(max.height).isEqualTo(Integer.MAX_VALUE);

            Assertions.assertThat(content.getComponentCount()).isEqualTo(2);
            Assertions.assertThat(content.getComponent(0)).isSameAs(sidePanel);
            Assertions.assertThat(content.getComponent(1)).isSameAs(pagePanel);

            Assertions.assertThat(mockedSide.constructed().size()).isEqualTo(1);
            Assertions.assertThat(mockedPage.constructed().size()).isEqualTo(1);
        }
    }

    @Test
    public void testSetLayoutFor() throws Exception {
        //given
        ContentComponent cc = new ContentComponent(sampleProperties());
        JComponent component = new JPanel();

        //when
        method(ContentComponent.class, "setLayoutFor", JComponent.class).invoke(cc, component);

        //then
        Assertions.assertThat(component.getLayout()).isInstanceOf(BoxLayout.class);
        Dimension max = component.getMaximumSize();
        Assertions.assertThat(max.width).isEqualTo(Integer.MAX_VALUE);
        Assertions.assertThat(max.height).isEqualTo(Integer.MAX_VALUE);
    }

    @Test
    public void testRenderSideNavIn() throws Exception {
        //given
        ContentComponent cc = new ContentComponent(sampleProperties());
        JComponent parent = new JPanel();

        @SuppressWarnings("unchecked")
        State<String> active = (State<String>) Mockito.mock(State.class);

        AtomicReference<Map<?, ?>> capturedEntries = new AtomicReference<>();
        JPanel sidePanel = new JPanel();

        try (@SuppressWarnings("unused")
        MockedConstruction<SideNavComponent> mockedSide =
            Mockito.mockConstruction(SideNavComponent.class, (mock, ctx) -> {
                Mockito.when(mock.get()).thenReturn(sidePanel);
                Object arg0 = ctx.arguments().get(0);
                Assertions.assertThat(arg0).isInstanceOf(Map.class);
                capturedEntries.set((Map<?, ?>) arg0);
            })) {

            //when
            method(ContentComponent.class, "renderSideNavIn", JComponent.class, State.class)
                .invoke(cc, parent, active);

            //then
            Map<?, ?> entries = capturedEntries.get();
            Assertions.assertThat(entries).isNotNull();

            Assertions.assertThat(entries.keySet().stream().allMatch(k -> k instanceof String))
                .isTrue();
            Set<String> keys =
                entries.keySet().stream().map(k -> (String) k).collect(Collectors.toSet());
            Assertions.assertThat(keys).isEqualTo(Set.of("Plugin A", "Plugin B"));

            for (Map.Entry<?, ?> entry : entries.entrySet()) {
                Assertions.assertThat(entry.getValue()).isInstanceOf(Runnable.class);
                ((Runnable) entry.getValue()).run();
            }
            Mockito.verify(active).setValue("Plugin A");
            Mockito.verify(active).setValue("Plugin B");

            Assertions.assertThat(parent.getComponentCount()).isEqualTo(1);
            Assertions.assertThat(parent.getComponent(0)).isSameAs(sidePanel);
        }
    }

    @Test
    public void testRenderPageIn() throws Exception {
        //given
        ContentComponent cc = new ContentComponent(sampleProperties());
        JComponent content = new JPanel();

        @SuppressWarnings("unchecked")
        State<String> active = (State<String>) Mockito.mock(State.class);
        Mockito.when(active.getValue()).thenReturn("Plugin A");

        AtomicReference<List<?>> capturedProps = new AtomicReference<>();
        JPanel pagePanel = new JPanel();

        try (@SuppressWarnings("unused")
        MockedConstruction<PageComponent> mockedPage =
            Mockito.mockConstruction(PageComponent.class, (mock, ctx) -> {
                Object arg0 = ctx.arguments().get(0);
                Assertions.assertThat(arg0).isInstanceOf(List.class);
                capturedProps.set((List<?>) arg0);
                Mockito.when(mock.get()).thenReturn(pagePanel);
            })) {

            //when
            method(ContentComponent.class, "renderPageIn", JComponent.class, State.class)
                .invoke(cc, content, active);

            //then
            List<?> props = capturedProps.get();
            Assertions.assertThat(props).isNotNull();
            Assertions.assertThat(props.size()).isEqualTo(2);

            Assertions.assertThat(content.getComponentCount()).isEqualTo(1);
            Assertions.assertThat(content.getComponent(0)).isSameAs(pagePanel);
        }
    }
}