package net.bierbaumer.otp_authenticator;


import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.intent.Intents;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
import android.support.v7.widget.ActionBarContextView;
import android.test.ActivityInstrumentationTestCase2;
import android.test.ViewAsserts;
import android.view.View;
import android.widget.ListView;


import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.binary.Base32;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;

import java.util.ArrayList;

import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.longClick;
import static android.support.test.espresso.action.ViewActions.pressBack;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.Intents.intending;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.is;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private MainActivity mActivity;

    public MainActivityTest() {
        super(MainActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

    public void testStart(){
        ViewAsserts.assertOnScreen(mActivity.getWindow().getDecorView(), mActivity.findViewById(R.id.listView));
    }


    //TODO. switch to toolbar
    public void test000About() throws InterruptedException {
        openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getContext());

        onView(allOf(withText("About"), isDisplayed())).perform(click());

        Thread.sleep(1000);
        onView(withId(R.id.webViewAbout)).check(matches(isDisplayed()));
        onView(withId(R.id.webViewAbout)).perform(pressBack());

        onView(withId(R.id.webViewAbout)).check(doesNotExist());

    }

    public void test000EmptyStart() throws InterruptedException {

        onView(withText("No account has been added yet")).check(matches(isDisplayed()));
        onView(withText("Add")).check(matches(isDisplayed()));

        Intents.init();

        String qr = "XXX" ;

        // Build a result to return from the ZXING app
        Intent resultData = new Intent();
        resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr);
        Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);

        // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
        // with the ActivityResult we just created
        intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result);


        onView(withText("Add")).check(matches(isDisplayed()));

        onView(withText("Add")).perform(click());

        // We can also validate that an intent resolving to the "camera" activity has been sent out by our app
        intended(hasAction("com.google.zxing.client.android.SCAN"));


        Intents.release();
    }

    public void test001InvalidQRCode() throws InterruptedException {
        Intents.init();

        String qr ="invalid qr code";

        // Build a result to return from the ZXING app
        Intent resultData = new Intent();
        resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr);
        Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);

        // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
        // with the ActivityResult we just created
        intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result);

        // Now that we have the stub in place, click on the button in our app that launches into the Camera
        onView(withId(R.id.action_scan)).perform(click());

        // We can also validate that an intent resolving to the "camera" activity has been sent out by our app
        intended(hasAction("com.google.zxing.client.android.SCAN"));

        onView(withText("Invalid QR Code")).check(matches(isDisplayed()));


        Thread.sleep(5000);
        onView(withText("No account has been added yet")).check(matches(isDisplayed()));

        Intents.release();
    }

    public void test002NocodeScanned() throws InterruptedException {
        Intents.init();

        // Build a result to return from the ZXING app
        Intent resultData = new Intent();

        Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, resultData);

        // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
        // with the ActivityResult we just created
        intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result);

        // Now that we have the stub in place, click on the button in our app that launches into the Camera
        onView(withId(R.id.action_scan)).perform(click());

        // We can also validate that an intent resolving to the "camera" activity has been sent out by our app
        intended(hasAction("com.google.zxing.client.android.SCAN"));

        onView(withText("No account has been added yet")).check(matches(isDisplayed()));

        Intents.release();
    }


    String[][] codes = new String [][]{
            new String[]{"WOW", "Sicherheit00000"},
            new String[]{"SUCH", "Sicherheit00001"},
            new String[]{"APP", "Sicherheit00002"},
    };


    public  void test003AddCodes() throws InterruptedException {
        for(String[] code: codes){
            Intents.init();

            String qr = "otpauth://totp/"+code[0] +"?secret="+new String(new Base32().encode(code[1].getBytes())) ;

            // Build a result to return from the ZXING app
            Intent resultData = new Intent();
            resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr);
            Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);

            // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
            // with the ActivityResult we just created
            intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result);

            // Now that we have the stub in place, click on the button in our app that launches into the Camera
            onView(withId(R.id.action_scan)).perform(click());

            // We can also validate that an intent resolving to the "camera" activity has been sent out by our app
            intended(hasAction("com.google.zxing.client.android.SCAN"));

            onView(withText("Account added")).check(matches(isDisplayed()));

            Intents.release();
        }

        Thread.sleep(500);


        for(int i = 0; i < codes.length; i++){
            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewLabel))
                    .check(matches(withText(codes[i][0])));

            String otp = TOTPHelper.generate(codes[i][1].getBytes());

            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewOTP))
                    .check(matches(withText(otp)));
        }
    }

    public  void test003CodesChange() throws InterruptedException {
        ArrayList<String> oldCodes = new ArrayList<>();

        for(int i = 0; i < codes.length; i++){
            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewLabel))
                    .check(matches(withText(codes[i][0])));

            String otp = TOTPHelper.generate(codes[i][1].getBytes());
            oldCodes.add(otp);

            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewOTP))
                    .check(matches(withText(otp)));
        }

        Thread.sleep(30*1000);

        for(int i = 0; i < codes.length; i++){
            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewLabel))
                    .check(matches(withText(codes[i][0])));

            String otp = TOTPHelper.generate(codes[i][1].getBytes());
            assertTrue(!oldCodes.get(i).equals(otp));

            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewOTP))
                    .check(matches(withText(otp)));
        }


    }

    public void test004Rearrange() throws InterruptedException, UiObjectNotFoundException {
        UiDevice mDevice = UiDevice.getInstance(getInstrumentation());


        UiObject start =  mDevice.findObject(new UiSelector().textContains(codes[0][0]));
        UiObject end =  mDevice.findObject(new UiSelector().textContains(codes[1][0]));

        start.dragTo(start, 10);
        start.dragTo(end, 10);

        mDevice.pressBack();

        Thread.sleep(2000);

        String t = codes[0][0];
        codes[0][0] = codes[1][0];
        codes[1][0] = t;

        for(int i = 0; i < codes.length; i++){
            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewLabel))
                    .check(matches(withText(codes[i][0])));
        }

        start =  mDevice.findObject(new UiSelector().textContains(codes[0][0]));
        end =  mDevice.findObject(new UiSelector().textContains(codes[1][0]));

        start.dragTo(start, 10);
        start.dragTo(end, 10);

        mDevice.pressBack();

        Thread.sleep(2000);

        t = codes[0][0];
        codes[0][0] = codes[1][0];
        codes[1][0] = t;

        for(int i = 0; i < codes.length; i++){
            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(i)
                    .onChildView(withId(R.id.textViewLabel))
                    .check(matches(withText(codes[i][0])));
        }
    }

    public void test005EditMode() throws InterruptedException {

        onView(withId(R.id.action_edit)).check(doesNotExist());

        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(0)
                .perform(longClick());

        onView(withId(R.id.action_edit)).check(matches(isDisplayed()));
        onView(withId(R.id.action_delete)).check(matches(isDisplayed()));
        ActionBarContextView.class.getCanonicalName();
        onView(allOf(isDescendantOfA(withClassName(Matchers.containsString("ActionBarContextView"))), withText(codes[0][0]))).check(matches(isDisplayed()));


        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(1)
                .perform(longClick());



        onView(withId(R.id.action_edit)).check(matches(isDisplayed()));
        onView(withId(R.id.action_delete)).check(matches(isDisplayed()));
        onView(allOf(isDescendantOfA(withClassName(Matchers.containsString("ActionBarContextView"))), withText(codes[1][0]))).check(matches(isDisplayed()));


        onView(withId(R.id.listView)).perform(pressBack());

        onView(withId(R.id.action_edit)).check(doesNotExist());
    }

    public void test005RenameCancel(){

        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(1)
                .perform(longClick());

        onView(withId(R.id.action_edit)).check(matches(isDisplayed()));

        onView(withId(R.id.action_edit)).perform(click());

        onView(withText(codes[1][0])).perform(click()).perform(typeText(" VERY TEST"));

        onView(withText("Cancel")).perform(click());

        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(1)
                .onChildView(withId(R.id.textViewLabel))
                .check(matches(withText(codes[1][0])));

    }

    public void test006Rename(){

        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(1)
                .perform(longClick());

        onView(withId(R.id.action_edit)).check(matches(isDisplayed()));

        onView(withId(R.id.action_edit)).perform(click());

        onView(withText(codes[1][0])).perform(click()).perform(typeText(" VERY TEST"));

        onView(withText("Save")).perform(click());

        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(1)
                .onChildView(withId(R.id.textViewLabel))
                .check(matches(withText(codes[1][0] + " VERY TEST")));

    }


    public void test007DeleteCancel() throws InterruptedException, EncoderException {


        onData(anything()).inAdapterView(withId(R.id.listView))
                .atPosition(0)
                .perform(longClick());

        onView(withId(R.id.action_delete)).check(matches(isDisplayed()));
        onView(withId(R.id.action_delete)).perform(click());

        onView(withText("Remove")).check(matches(isDisplayed()));
        onView(withText("Cancel")).check(matches(isDisplayed()));

        onView(withText("Cancel")).perform(click());

        onView(withText("Remove")).check(doesNotExist());

        onView(withId(R.id.listView)).check(matches(withListSize(codes.length)));

    }

    public void test008Delete() throws InterruptedException, EncoderException {




        // remove test
        for(int i = codes.length; i > 0; i--){

            onData(anything()).inAdapterView(withId(R.id.listView))
                    .atPosition(0)
                    .perform(longClick());

            onView(withId(R.id.action_delete)).check(matches(isDisplayed()));
            onView(withId(R.id.action_delete)).perform(click());

            onView(withText("Remove")).check(matches(isDisplayed()));



            onView(withText("Remove")).perform(click());
            onView(withId(R.id.listView)).check(matches(withListSize(i - 1)));

            if(i > 1){
                onView(withText("Account removed")).check(matches(isDisplayed()));
            }
            else {
                onView(withText(R.string.no_accounts)).check(matches(isDisplayed()));
            }

        }


    }

    public static Matcher<View> withListSize (final int size) {
        return new TypeSafeMatcher<View> () {
            @Override public boolean matchesSafely (final View view) {
                return ((ListView) view).getChildCount () == size;
            }

            @Override public void describeTo (final Description description) {
                description.appendText ("ListView should have " + size + " items");
            }
        };
    }


    public static Matcher<View> withResourceName(String resourceName) {
        return withResourceName(is(resourceName));
    }

    public static Matcher<View> withResourceName(final Matcher<String> resourceNameMatcher) {
        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("with resource name: ");
                resourceNameMatcher.describeTo(description);
            }

            @Override
            public boolean matchesSafely(View view) {
                int id = view.getId();
                return id != View.NO_ID && id != 0 && view.getResources() != null
                        && resourceNameMatcher.matches(view.getResources().getResourceName(id));
            }
        };
    }



}
