package com.reactnativenavigation.viewcontrollers.parent;

import android.app.Activity;
import android.content.res.Configuration;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.reactnativenavigation.BaseAndroidTest;
import com.reactnativenavigation.TestActivity;
import com.reactnativenavigation.TestUtils;
import com.reactnativenavigation.mocks.SimpleViewController;
import com.reactnativenavigation.options.Options;
import com.reactnativenavigation.options.params.Text;
import com.reactnativenavigation.viewcontrollers.viewcontroller.Presenter;
import com.reactnativenavigation.react.CommandListenerAdapter;
import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry;
import com.reactnativenavigation.viewcontrollers.stack.StackController;
import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import androidx.annotation.NonNull;

import static com.reactnativenavigation.utils.CollectionUtils.*;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
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.verify;
import static org.mockito.Mockito.when;

@RunWith(AndroidJUnit4.class)
public class ParentControllerTest extends BaseAndroidTest {
    private static final String INITIAL_TITLE = "initial title";
    private Activity activity;
    private ChildControllersRegistry childRegistry;
    private List<ViewController<?>> children;
    private ParentController<?> uut;
    private Presenter presenter;

    @Rule
    public ActivityScenarioRule<TestActivity> rule = new ActivityScenarioRule<>(TestActivity.class);

    @Before
    public void beforeEach() {
        rule.getScenario().onActivity(activity -> {
            this.activity = activity;
            childRegistry = new ChildControllersRegistry();
            children = new ArrayList<>();
            Options initialOptions = new Options();
            initialOptions.topBar.title.text = new Text(INITIAL_TITLE);
            presenter = spy(new Presenter(activity, new Options()));
            uut = spy(new ParentController<ViewGroup>(activity, childRegistry, "uut", presenter, initialOptions) {
                @Override
                public ViewController<?> getCurrentChild() {
                    return children.get(0);
                }

                @NonNull
                @Override
                public ViewGroup createView() {
                    FrameLayout layout = new FrameLayout(activity);
                    for (ViewController<?> child : children) {
                        child.setParentController(this);
                        layout.addView(child.getView());
                    }
                    return layout;
                }

                @Override
                public void sendOnNavigationButtonPressed(String buttonId) {
                }

                @NonNull
                @Override
                public Collection<ViewController<?>> getChildControllers() {
                    return children;
                }
            });
        });
    }

    @Test
    public void onConfigurationChange_shouldCallConfigurationChangeForPresenterAndVisibleChild() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            SimpleViewController child1 = spy(
                    new SimpleViewController(activity, childRegistry, "child1", new Options()));
            SimpleViewController child2 = spy(
                    new SimpleViewController(activity, childRegistry, "child2", new Options()));
            children.add(child1);
            children.add(child2);
            Mockito.when(child1.isViewShown()).thenReturn(true);
            Mockito.when(child2.isViewShown()).thenReturn(false);
            uut.onConfigurationChanged(mock(Configuration.class));
            verify(presenter).onConfigurationChanged(any(), any());
            verify(child1).onConfigurationChanged(any());
            verify(child2, never()).onConfigurationChanged(any());
        });
    }

    @Test
    public void onViewDidAppearShouldCallCurrentChildDidAppear() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            SimpleViewController child1 = spy(
                    new SimpleViewController(activity, childRegistry, "child1", new Options()));
            SimpleViewController child2 = spy(
                    new SimpleViewController(activity, childRegistry, "child2", new Options()));
            children.add(child1);
            children.add(child2);

            uut.onViewDidAppear();

            verify(child1).onViewDidAppear();
            verify(child2, never()).onViewDidAppear();
        });
    }

    @Test
    public void onViewDisappearShouldCallCurrentChildDisAppear() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            SimpleViewController child1 = spy(
                    new SimpleViewController(activity, childRegistry, "child1", new Options()));
            SimpleViewController child2 = spy(
                    new SimpleViewController(activity, childRegistry, "child2", new Options()));
            children.add(child1);
            children.add(child2);

            uut.onViewDisappear();

            verify(child1).onViewDisappear();
            verify(child2, never()).onViewDisappear();
        });
    }

    @Test
    public void holdsViewGroup() {
        assertThat(uut.getView()).isInstanceOf(ViewGroup.class);
    }

    @Test
    public void mustHaveChildControllers() {
        assertThat(uut.getChildControllers()).isNotNull();
    }

    @Test
    public void findControllerById_ChildById() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            SimpleViewController child1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
            SimpleViewController child2 = new SimpleViewController(activity, childRegistry, "child2", new Options());
            children.add(child1);
            children.add(child2);

            assertThat(uut.findController("uut")).isEqualTo(uut);
            assertThat(uut.findController("child1")).isEqualTo(child1);
        });
    }

    @Test
    public void findControllerById_Recursive() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            StackController stackController = TestUtils.newStackController(activity).build();
            stackController.ensureViewIsCreated();
            SimpleViewController child1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
            SimpleViewController child2 = new SimpleViewController(activity, childRegistry, "child2", new Options());
            stackController.push(child1, new CommandListenerAdapter());
            stackController.push(child2, new CommandListenerAdapter());
            children.add(stackController);

            assertThat(uut.findController("child2")).isEqualTo(child2);
        });
    }

    @Test
    public void destroy_DestroysChildren() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            ViewController<?> child1 = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
            children.add(child1);

            verify(child1, times(0)).destroy();
            uut.destroy();
            verify(child1, times(1)).destroy();
        });
    }

    @Test
    public void optionsAreClearedWhenChildIsAppeared() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            StackController stackController = spy(TestUtils.newStackController(activity).build());
            stackController.ensureViewIsCreated();
            SimpleViewController child1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
            stackController.push(child1, new CommandListenerAdapter());

            child1.onViewWillAppear();
            verify(stackController, times(1)).clearOptions();
        });
    }

    @Test
    public void mergeOptions_optionsAreMergedWhenChildAppears() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            Options options = new Options();
            options.topBar.title.text = new Text("new title");
            ViewController<?> child1 = spy(new SimpleViewController(activity, childRegistry, "child1", options));
            children.add(child1);
            uut.ensureViewIsCreated();

            child1.ensureViewIsCreated();
            child1.onViewWillAppear();
            ArgumentCaptor<Options> optionsCaptor = ArgumentCaptor.forClass(Options.class);
            verify(uut, times(1)).clearOptions();
            verify(uut, times(1)).applyChildOptions(optionsCaptor.capture(), eq(child1));
            assertThat(optionsCaptor.getValue().topBar.title.text.get()).isEqualTo("new title");
        });
    }

    @Test
    public void mergeOptions_initialParentOptionsAreNotMutatedWhenChildAppears() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            Options options = new Options();
            options.topBar.title.text = new Text("new title");
            ViewController<?> child1 = spy(new SimpleViewController(activity, childRegistry, "child1", options));
            children.add(child1);

            uut.ensureViewIsCreated();

            child1.ensureViewIsCreated();
            child1.onViewWillAppear();
            assertThat(uut.initialOptions.topBar.title.text.get()).isEqualTo(INITIAL_TITLE);
        });
    }

    @Test
    public void resolveCurrentOptions_returnOptionsIfNoChildren() {
        assertThat(uut.getChildControllers().size()).isZero();
        assertThat(uut.resolveCurrentOptions()).isEqualTo(uut.initialOptions);
    }

    @Test
    public void resolveCurrentOptions_mergesWithCurrentChild() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            ViewController child1 = Mockito.mock(ViewController.class);
            when(child1.getView()).thenReturn(new FrameLayout(activity));
            Options copiedChildOptions = spy(new Options());
            Options childOptions = spy(new Options() {
                @Override
                public Options copy() {
                    return copiedChildOptions;
                }
            });
            when(child1.resolveCurrentOptions()).thenReturn(childOptions);

            children.add(child1);

            uut.ensureViewIsCreated();
            assertThat(uut.getCurrentChild()).isEqualTo(child1);
            uut.resolveCurrentOptions();
            verify(child1).resolveCurrentOptions();
            verify(copiedChildOptions).withDefaultOptions(uut.initialOptions);
        });
    }

    @Test
    public void resolveCurrentOptions_withDefaultOptions() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            SimpleViewController child1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
            children.add(child1);
            uut.ensureViewIsCreated();

            Options defaultOptions = new Options();
            Options currentOptions = spy(new Options());
            ParentController<?> spy = spy(uut);
            Mockito.when(spy.resolveCurrentOptions()).thenReturn(currentOptions);
            spy.resolveCurrentOptions(defaultOptions);
            verify(currentOptions).withDefaultOptions(defaultOptions);
        });
    }

    @Test
    public void applyTopInset() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            children.addAll(createChildren());
            uut.applyTopInset();
            forEach(children, c -> verify(c).applyTopInset());
        });
    }

    @Test
    public void getTopInset() {
        assertThat(uut.getTopInset()).isZero();
    }

    @Test
    public void getTopInsetForChild() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            ParentController<?> parent = Mockito.mock(ParentController.class);
            Mockito.when(parent.resolveChildOptions(uut)).thenReturn(Options.EMPTY);

            when(parent.getTopInset(any())).thenReturn(123);
            uut.setParentController(parent);

            assertThat(uut.getTopInset(Mockito.mock(ViewController.class))).isEqualTo(123);
        });
    }

    @Test
    public void applyBottomInset() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            children.addAll(createChildren());
            uut.applyBottomInset();
            forEach(children, c -> verify(c).applyBottomInset());
        });
    }

    @Test
    public void getBottomInsetForChild() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            ParentController<?> parent = Mockito.mock(ParentController.class);
            Mockito.when(parent.resolveChildOptions(uut)).thenReturn(Options.EMPTY);

            when(parent.getBottomInset(any())).thenReturn(123);
            uut.setParentController(parent);

            assertThat(uut.getBottomInset(Mockito.mock(ViewController.class))).isEqualTo(123);
        });
    }

    private List<ViewController<?>> createChildren() {
        return Arrays.asList(
                spy(new SimpleViewController(activity, childRegistry, "child1", new Options())),
                spy(new SimpleViewController(activity, childRegistry, "child2", new Options())));
    }
}