/*
 *    Copyright (C) 2016 Björn Quentin
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *      http://www.apache.org/licenses/LICENSE-2.0
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package de.mobilej.thinr;

import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;

import org.junit.After;
import org.junit.Before;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.reflect.Whitebox;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

import de.mobilej.ABridge;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.eq;
import static org.powermock.api.mockito.PowerMockito.doAnswer;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;

/**
 * Base Class for tests serving some utilities
 * <p>
 * Created by bjoern on 17.05.2016.
 */
public class TestBase {

    protected String field = "field";
    protected String result;

    protected Queue<AsyncTaskHolder> asyncTasks;
    protected Queue<Runnable> runnables;
    //static Queue<Runnable> runnables;
    public static Handler mockHandler = mock(Handler.class);

    @Before
    public void setup() throws Exception {
        Field sdkIntField = Build.VERSION.class.getDeclaredField("SDK_INT");
        sdkIntField.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(sdkIntField, sdkIntField.getModifiers() & ~Modifier.FINAL);

        sdkIntField.set(null, getSdkVersionForBuildVersion());

        mockStatic(ABridge.class);

        asyncTasks = new ArrayBlockingQueue<>(10);
        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                AsyncTaskHolder holder = new AsyncTaskHolder();
                holder.asyncTask = (AsyncTask) invocation.getArguments()[1];
                holder.params = (Object[]) invocation.getArguments()[2];
                asyncTasks.add(holder);
                return null;
            }
        }).when(ABridge.class, "callObject", eq("android.os.AsyncTask.executeOnExecutor(java.util.concurrent.Executor,java.lang.Object[])"), anyObject(), any(Object[].class));

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                AsyncTaskHolder holder = new AsyncTaskHolder();
                holder.asyncTask = (AsyncTask) invocation.getArguments()[1];
                holder.params = (Object[]) invocation.getArguments()[2];
                asyncTasks.add(holder);
                return null;
            }
        }).when(ABridge.class, "callObject", eq("android.os.AsyncTask.execute(java.lang.Object[])"), anyObject(), any(Object[].class));

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {

                for (AsyncTaskHolder holder : asyncTasks) {
                    if (holder.asyncTask == invocation.getArguments()[1]) {
                        holder.canceled = true;
                    }
                }

                return null;
            }
        }).when(ABridge.class, "callBoolean", eq("android.os.AsyncTask.cancel(boolean)"), anyObject(), any(Object[].class));

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {

                for (AsyncTaskHolder holder : asyncTasks) {
                    if (holder.asyncTask == invocation.getArguments()[1]) {
                        return holder.canceled;
                    }
                }

                return false;
            }
        }).when(ABridge.class, "callBoolean", eq("android.os.AsyncTask.isCancelled()"), anyObject(), any(Object[].class));


        Looper mockMainLooper = mock(Looper.class);
        when(mockMainLooper.getThread()).thenReturn(Thread.currentThread());
        mockStatic(Looper.class);
        when(Looper.getMainLooper()).thenReturn(mockMainLooper);

        mockNewHandler();
    }

    protected int getSdkVersionForBuildVersion() {
        return Build.VERSION_CODES.HONEYCOMB;
    }

    @After
    public void tearDown() throws Exception {
        Field sdkIntField = Build.VERSION.class.getDeclaredField("SDK_INT");
        sdkIntField.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(sdkIntField, sdkIntField.getModifiers() & ~Modifier.FINAL);

        sdkIntField.set(null, 0);
    }

    protected boolean doNextRunnable(Queue<Runnable> runnables) {
        if (runnables.size() > 0) {
            Runnable r = runnables.poll();
            r.run();
            return true;
        }

        return false;
    }

    protected void doNextAsyncTaskUncancelled(Queue<AsyncTaskHolder> asyncTasks) throws Exception {
        if (asyncTasks.size() > 0) {
            AsyncTaskHolder holder = asyncTasks.poll();
            AsyncTask asyncTask = holder.asyncTask;
            Object res = Whitebox.invokeMethod(asyncTask, "doInBackground", holder.params);
            Whitebox.invokeMethod(asyncTask, "onPostExecute", res);
        }
    }


    protected boolean doNextAsyncTask(Queue<AsyncTaskHolder> asyncTasks) throws Exception {
        if (asyncTasks.size() > 0) {
            AsyncTaskHolder holder = asyncTasks.poll();
            AsyncTask asyncTask = holder.asyncTask;

            Object res = null;
            if (!holder.canceled) {
                res = Whitebox.invokeMethod(asyncTask, "doInBackground", holder.params);
            }

            if (holder.canceled) {
                Whitebox.invokeMethod(asyncTask, "onCancelled");
                return false;
            } else {
                Whitebox.invokeMethod(asyncTask, "onPostExecute", res);
            }

            return true;
        }

        return false;
    }


    protected boolean doNextAsyncTaskRunBackgroundEvenIfCancelled(Queue<AsyncTaskHolder> asyncTasks) throws Exception {
        if (asyncTasks.size() > 0) {
            AsyncTaskHolder holder = asyncTasks.peek();
            AsyncTask asyncTask = holder.asyncTask;

            Object res = Whitebox.invokeMethod(asyncTask, "doInBackground", holder.params);
            asyncTasks.poll();

            if (holder.canceled) {
                Whitebox.invokeMethod(asyncTask, "onCancelled");
                return false;
            } else {
                Whitebox.invokeMethod(asyncTask, "onPostExecute", res);
            }

            return true;
        }

        return false;
    }

    private Queue<Runnable> mockNewHandler() throws Exception {
        runnables = new ArrayBlockingQueue<>(10);

        whenNew(Handler.class).withAnyArguments().thenReturn(mockHandler);

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                runnables.add((Runnable) invocation.getArguments()[0]);
                return null;
            }
        }).when(mockHandler).post(any(Runnable.class));

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                runnables.remove((Runnable) invocation.getArguments()[0]);
                return null;
            }
        }).when(mockHandler).removeCallbacks(any(Runnable.class));

        return runnables;
    }

    protected void runQueuesEmpty() throws Exception {
        while (doNextRunnable(runnables) || doNextAsyncTask(asyncTasks)) {
            // run till completion
        }
    }

    public static class AsyncTaskHolder {
        public AsyncTask asyncTask;
        public Object[] params;
        public boolean canceled;
    }
}