package com.philliphsu.numberpadtimepicker;

import android.provider.Settings;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.ViewAssertion;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.action.ViewActions;
import android.support.test.espresso.assertion.ViewAssertions;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.text.format.DateFormat;
import android.view.View;
import android.view.ViewGroup;

import com.philliphsu.numberpadtimepickersample.MainActivity;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.List;

@RunWith(AndroidJUnit4.class)
public class NumberPadTimePickerDialogTest {
    private static final List<TestCase> MODE_12HR_TESTS_1_TO_9 = new ArrayList<>(9);
    private static final List<TestCase> MODE_12HR_TESTS_10_TO_95 = new ArrayList<>(54);
    private static final List<TestCase> MODE_12HR_TESTS_100_TO_959 = new ArrayList<>();
    private static final List<TestCase> MODE_12HR_TESTS_1000_TO_1259 = new ArrayList<>();

    private static final List<TestCase> MODE_24HR_TESTS_0_TO_9 = new ArrayList<>(10);
    private static final List<TestCase> MODE_24HR_TESTS_00_TO_95 = new ArrayList<>(65);
    private static final List<TestCase> MODE_24HR_TESTS_000_TO_959 = new ArrayList<>();
    private static final List<TestCase> MODE_24HR_TESTS_0000_TO_2359 = new ArrayList<>();

    static {
        build_Mode12Hr_Tests_1_to_9();
        build_Mode12Hr_Tests_10_to_95();
        build_Mode12Hr_Tests_100_to_959();
        build_Mode12Hr_Tests_1000_to_1259();

        build_Mode24Hr_Tests_0_to_9();
        build_Mode24Hr_Tests_00_to_95();
        build_Mode24Hr_Tests_000_to_959();
        build_Mode24Hr_Tests_0000_to_2359();
    }

    private static void build_Mode12Hr_Tests_1_to_9() {
        for (int i = 1; i <= 9; i++) {
            MODE_12HR_TESTS_1_TO_9.add(new TestCase.Builder(array(i), false)
                    .numberKeysEnabled(0, 6 /* 1[0-2]:... or i:[0-5]... */)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(true)
                    .okButtonEnabled(false)
                    .timeDisplay(text(i))
                    .build());
        }
    }

    private static void build_Mode24Hr_Tests_0_to_9() {
        for (int i = 0; i <= 9; i++) {
            TestCase.Builder builder = new TestCase.Builder(array(i), true)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(true)
                    .okButtonEnabled(false)
                    .timeDisplay(text(i));
            if (i <= 1) {
                builder.numberKeysEnabled(0, 10 /* i[0-9]:... or i:[0-5]... */);
            } else {
                builder.numberKeysEnabled(0, 6 /* 2[0-3]:... or i:[0-5]... */);
            }
            MODE_24HR_TESTS_0_TO_9.add(builder.build());
        }
    }

    private static void build_Mode12Hr_Tests_10_to_95() {
        for (int i = 10; i <= 95; i++) {
            if (i % 10 > 5) continue;
            TestCase test = new TestCase.Builder(array(i / 10, i % 10), false)
                    .numberKeysEnabled(0, 10)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(i >= 10 && i <= 12)
                    .okButtonEnabled(false)
                    .timeDisplay(String.format("%d", i) /* TODO: Pull formatting logic from
                    Presenter impl. into its own class. Then format the current sequence of
                    digits. */)
                    .build();
            MODE_12HR_TESTS_10_TO_95.add(test);
        }
    }

    private static void build_Mode24Hr_Tests_00_to_95() {
        for (int i = 0; i <= 95; i++) {
            if (i % 10 > 5 && i > 25) continue;
            TestCase test = new TestCase.Builder(array(i / 10, i % 10), true)
                    .numberKeysEnabled(0, (i % 10 > 5) ? 6 : 10 /* (0-1)(6-9):[0-5] or (i_1):(i_2)[0-9]*/)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(i >= 0 && i <= 23)
                    .okButtonEnabled(false)
                    .timeDisplay(String.format("%02d", i) /* TODO: Pull formatting logic from
                    Presenter impl. into its own class. Then format the current sequence of
                    digits. */)
                    .build();
            MODE_24HR_TESTS_00_TO_95.add(test);
        }
    }

    private static void build_Mode12Hr_Tests_100_to_959() {
        for (int i = 100; i <= 959; i++) {
            if (i % 100 > 59) continue;
            TestCase test = new TestCase.Builder(
                    array(i / 100, (i % 100) / 10, i % 10), false)
                    .numberKeysEnabled(0, (i > 125 || i % 10 > 5) ? 0 : 10)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(true)
                    .build();
            MODE_12HR_TESTS_100_TO_959.add(test);
        }
    }

    private static void build_Mode24Hr_Tests_000_to_959() {
        for (int i = 0; i <= 959; i++) {
            boolean skipEndingIn6Through9From60To100 = i % 10 > 5 && i > 60 && i < 100;
            boolean skipEndingIn6Through9From160To200 = i % 10 > 5 && i > 160 && i < 200;
            if (skipEndingIn6Through9From60To100
                    || skipEndingIn6Through9From160To200
                    || i > 259 && i % 100 > 59) {
                continue;
            }
            boolean canBeValidTimeNow = i < 60 || (i >= 100 && i < 160) || i >= 200;
            int cap;
            if ((i % 10 > 5 && (i < 160 || i > 200)) || i >= 236) {
                cap = 0;
            } else {
                cap = 10;
            }
            TestCase test = new TestCase.Builder(
                    array(i / 100, (i % 100) / 10, i % 10), true)
                    .numberKeysEnabled(0, cap)
                    .backspaceEnabled(true)
                    .okButtonEnabled(canBeValidTimeNow)
                    .headerDisplayFocused(cap != 0)
                    .altKeysEnabled(false)
                    .build();
            MODE_24HR_TESTS_000_TO_959.add(test);
        }
    }

    private static void build_Mode12Hr_Tests_1000_to_1259() {
        for (int i = 1000; i <= 1259; i++) {
            if (i % 100 > 59) continue;
            TestCase test = new TestCase.Builder(
                    array(i / 1000, (i % 1000) / 100, (i % 100) / 10, i % 10), false)
                    .numberKeysEnabled(0, 0)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(true)
                    .altKeysEnabled(true)
                    .build();
            MODE_12HR_TESTS_1000_TO_1259.add(test);
        }
    }

    private static void build_Mode24Hr_Tests_0000_to_2359() {
        for (int i = 0; i <= 2359; i++) {
            if (i % 100 > 59) continue;
            TestCase test = new TestCase.Builder(
                    array(i / 1000, (i % 1000) / 100, (i % 100) / 10, i % 10), true)
                    .numberKeysEnabled(0, 0)
                    .okButtonEnabled(true)
                    .backspaceEnabled(true)
                    .headerDisplayFocused(false)
                    .altKeysEnabled(false)
                    .build();
            MODE_24HR_TESTS_0000_TO_2359.add(test);
        }
    }

    private static int[] array(int... a) {
        return a == null ? new int[0] : a;
    }

    /**
     * {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test.
     *
     * <p>
     * Rules are interceptors which are executed for each test method and are important building
     * blocks of Junit tests.
     *
     * <p>
     * The annotated Activity will be launched before each annotated @Test and before any annotated
     * {@link Before @Before} methods. The Activity is automatically terminated after the test is
     * completed and all {@link After @After} methods are finished.
     */
    @Rule
    public ActivityTestRule<MainActivity> mActivityTestRule =
            new ActivityTestRule<>(MainActivity.class);

    private LocaleModel mLocaleModel;

    // Used to restore the device's time format at the end of testing.
    private boolean mInitiallyIn24HourMode;

    @Before
    public void setup() {
        mLocaleModel = new LocaleModel(mActivityTestRule.getActivity());
        mInitiallyIn24HourMode = DateFormat.is24HourFormat(mActivityTestRule.getActivity());
    }

    @Test
    public void verifyInitialViewEnabledStates() {
        openTimePicker();
        Espresso.onView(ViewMatchers.withId(R.id.nptp_input_time)).check(
                ViewAssertions.matches(ViewMatchers.withText("")));
        // Check that the am/pm view is set to the correct visibility.
        //
        // Rather than use the isDisplayed() matcher, which, on top of matching the view to a
        // View.VISIBLE state, matches the view to being drawn with visible bounds, we use
        // the withEffectiveVisibility() matcher to match only the former criterion.
        Espresso.onView(ViewMatchers.withId(R.id.nptp_input_ampm)).check(
                ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(mInitiallyIn24HourMode ?
                        ViewMatchers.Visibility.GONE : ViewMatchers.Visibility.VISIBLE)));
        if (!mInitiallyIn24HourMode) {
            Espresso.onView(ViewMatchers.withId(R.id.nptp_input_ampm)).check(
                    ViewAssertions.matches(isNthChildOf(
                            ViewMatchers.withId(R.id.nptp_input_time_container),
                            mLocaleModel.isAmPmWrittenBeforeTime() ? 0 : 1)));
        }
        Espresso.onView(ViewMatchers.withId(R.id.nptp_backspace)).check(
                matchesIsEnabled(false));
        // We can easily manually verify whether the divider is focused, so it's not worth the
        // trouble of writing a test.
        for (int i = 0; i < 10; i++) {
            Espresso.onView(withDigit(i)).check(matchesIsEnabled(mInitiallyIn24HourMode || i > 0));
        }
        Espresso.onView(ViewMatchers.withId(R.id.nptp_text9)).check(matchesIsEnabled(false));
        Espresso.onView(ViewMatchers.withId(R.id.nptp_text11)).check(matchesIsEnabled(false));
        Espresso.onView(ViewMatchers.withText(android.R.string.ok)).check(matchesIsEnabled(false));
    }

    @Test
    public void mode12Hr_verifyViewEnabledStates_Input_1_to_9() {
        initializeTimePicker(false);
        verifyViewEnabledStates(MODE_12HR_TESTS_1_TO_9);
    }

    @Test
    public void mode24Hr_verifyViewEnabledStates_Input_0_to_9() {
        initializeTimePicker(true);
        verifyViewEnabledStates(MODE_24HR_TESTS_0_TO_9);
    }

    @Test
    public void mode12Hr_verifyViewEnabledStates_Input_10_to_95() {
        initializeTimePicker(false);
        verifyViewEnabledStates(MODE_12HR_TESTS_10_TO_95);
    }

    @Test
    public void mode24Hr_verifyViewEnabledStates_Input_00_to_95() {
        initializeTimePicker(true);
        verifyViewEnabledStates(MODE_24HR_TESTS_00_TO_95);
    }

    @Test
    public void mode12Hr_verifyViewEnabledStates_Input_100_to_959() {
        initializeTimePicker(false);
        verifyViewEnabledStates(MODE_12HR_TESTS_100_TO_959);
    }

    @Test
    public void mode24Hr_verifyViewEnabledStates_Input_000_to_959() {
        initializeTimePicker(true);
        verifyViewEnabledStates(MODE_24HR_TESTS_000_TO_959);
    }

    @Test
    public void mode12Hr_verifyViewEnabledStates_Input_1000_to_1259() {
        initializeTimePicker(false);
        verifyViewEnabledStates(MODE_12HR_TESTS_1000_TO_1259);
    }

    @Test
    public void mode24Hr_verifyViewEnabledStates_Input_0000_to_2359() {
        initializeTimePicker(true);
        verifyViewEnabledStates(MODE_24HR_TESTS_0000_TO_2359);
    }

    @After
    public void resetDeviceTimeFormat() {
        setDeviceTo24HourMode(mInitiallyIn24HourMode);
    }

    private void setDeviceTo24HourMode(boolean use24HourMode) {
        Settings.System.putString(mActivityTestRule.getActivity().getContentResolver(),
                Settings.System.TIME_12_24, use24HourMode ? "24" : "12");
    }

    private void initializeTimePicker(boolean use24HourMode) {
        setDeviceTo24HourMode(use24HourMode);
        openTimePicker();
        if (!use24HourMode) {
            // Check that '0' button is disabled.
            Espresso.onView(ViewMatchers.withId(R.id.nptp_text10)).check(matchesIsEnabled(false));
        }
    }

    private static void openTimePicker() {
        Espresso.onView(ViewMatchers.withId(com.philliphsu.numberpadtimepickersample.R.id.button1))
                .perform(ViewActions.click());
    }

    /**
     * Helper method that wraps {@link ViewMatchers#withText(String) withText(String)}.
     *
     * @return A Matcher that matches a number key button by its text representation
     *         of {@code digit}.
     */
    private static Matcher<View> withDigit(int digit) {
        // TODO: When we're comfortable with the APIs, we can statically import them and
        // make direct calls to these methods and cut down on the verbosity, instead of
        // writing helper methods that wrap these APIs.
        return ViewMatchers.withText(text(digit));
    }

    // TODO: See if we can use ButtonTextModel#text() instead. Currently, it is package private.
    private static String text(int digit) {
        return String.format("%d", digit);
    }

    /**
     * @param enabled Whether the view should be matched to be enabled or not.
     * @return A {@link ViewAssertion} that asserts that a view should be matched
     *         to be enabled or disabled.
     */
    private static ViewAssertion matchesIsEnabled(boolean enabled) {
        // TODO: When we're comfortable with the APIs, we can statically import them and
        // make direct calls to these methods and cut down on the verbosity, instead of
        // writing helper methods that wrap these APIs.
        return ViewAssertions.matches(enabled ? ViewMatchers.isEnabled() : Matchers.not(ViewMatchers.isEnabled()));
    }

    /**
     * Returns a matcher that matches a {@link View} that is a child of the described parent
     * at the specified index.
     *
     * @param parentMatcher A matcher that describes the view's parent.
     * @param childIndex The index of the view at which it is a child of the described parent.
     */
    private static Matcher<View> isNthChildOf(final Matcher<View> parentMatcher, final int childIndex) {
        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("is child at index "+childIndex+" of view matched by parentMatcher: ");
                parentMatcher.describeTo(description);
            }

            @Override
            public boolean matchesSafely(View view) {
                ViewGroup parent = (ViewGroup) view.getParent();
                return parentMatcher.matches(parent) && view.equals(parent.getChildAt(childIndex));
            }
        };
    }

    private static ViewInteraction[] getButtonInteractions() {
        ViewInteraction[] buttonsInteractions = new ViewInteraction[10];
        // We cannot rely on the withDigit() matcher to retrieve these because,
        // after performing a click on a button, the time display will update to
        // take on that button's digit text, and so withDigit() will return a matcher
        // that matches multiple views with that digit text: the button
        // itself and the time display. This will prevent us from performing
        // validation on the same ViewInteractions later.
        buttonsInteractions[0] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text10));
        buttonsInteractions[1] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text0));
        buttonsInteractions[2] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text1));
        buttonsInteractions[3] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text2));
        buttonsInteractions[4] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text3));
        buttonsInteractions[5] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text4));
        buttonsInteractions[6] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text5));
        buttonsInteractions[7] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text6));
        buttonsInteractions[8] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text7));
        buttonsInteractions[9] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text8));
        return buttonsInteractions;
    }

    private static ViewInteraction[] getAltButtonInteractions() {
        ViewInteraction[] buttonsInteractions = new ViewInteraction[2];
        buttonsInteractions[0] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text9));
        buttonsInteractions[1] = Espresso.onView(ViewMatchers.withId(R.id.nptp_text11));
        return buttonsInteractions;
    }

    private static void verifyViewEnabledStates(List<TestCase> testSuite) {
        for (TestCase test : testSuite) {
            verifyViewEnabledStates(test);
        }
    }

    private static void verifyViewEnabledStates(TestCase test) {
        ViewInteraction[] buttonsInteractions = getButtonInteractions();
        ViewInteraction[] altButtonsInteractions = getAltButtonInteractions();
        for (int digit : test.sequence) {
            buttonsInteractions[digit]
                    .check(ViewAssertions.matches(ViewMatchers.isEnabled()))
                    .perform(ViewActions.click());
        }
        for (int i = 0; i < 10; i++) {
            buttonsInteractions[i].check(matchesIsEnabled(
                    i >= test.numberKeysEnabledStart && i < test.numberKeysEnabledEnd));
            altButtonsInteractions[0].check(matchesIsEnabled(test.leftAltKeyEnabled));
            altButtonsInteractions[1].check(matchesIsEnabled(test.rightAltKeyEnabled));
        }

        Espresso.onView(ViewMatchers.withText(android.R.string.ok))
                .check(matchesIsEnabled(test.okButtonEnabled));

        ViewInteraction backspaceInteraction = Espresso.onView(
                ViewMatchers.withId(R.id.nptp_backspace));
        // Reset after each iteration by backspacing on the button just clicked.
        backspaceInteraction.check(matchesIsEnabled(true))
                .perform(ViewActions.longClick())
                .check(matchesIsEnabled(false));
    }

    private static final class TestCase {
        final int[] sequence;
        final boolean ampmState;

        final int numberKeysEnabledStart;
        final int numberKeysEnabledEnd;
        final boolean backspaceEnabled;
        final boolean headerDisplayFocused;
        final boolean leftAltKeyEnabled;
        final boolean rightAltKeyEnabled;
        final boolean okButtonEnabled;
        final CharSequence timeDisplay;
        final CharSequence ampmDisplay;

        TestCase(int[] sequence, boolean ampmState, int numberKeysEnabledStart, int numberKeysEnabledEnd, boolean backspaceEnabled, boolean headerDisplayFocused, boolean leftAltKeyEnabled, boolean rightAltKeyEnabled, boolean okButtonEnabled, CharSequence timeDisplay, CharSequence ampmDisplay) {
            this.sequence = sequence;
            this.ampmState = ampmState;
            this.numberKeysEnabledStart = numberKeysEnabledStart;
            this.numberKeysEnabledEnd = numberKeysEnabledEnd;
            this.backspaceEnabled = backspaceEnabled;
            this.headerDisplayFocused = headerDisplayFocused;
            this.leftAltKeyEnabled = leftAltKeyEnabled;
            this.rightAltKeyEnabled = rightAltKeyEnabled;
            this.okButtonEnabled = okButtonEnabled;
            this.timeDisplay = timeDisplay;
            this.ampmDisplay = ampmDisplay;
        }

        static class Builder {
            private final int[] sequence;
            private final boolean ampmState;

            private int numberKeysEnabledStart;
            private int numberKeysEnabledEnd;
            private boolean backspaceEnabled;
            private boolean headerDisplayFocused;
            private boolean leftAltKeyEnabled;
            private boolean rightAltKeyEnabled;
            private boolean okButtonEnabled;
            private CharSequence timeDisplay;
            private CharSequence ampmDisplay;

            public Builder(int[] sequence, boolean ampmState) {
                this.sequence = sequence;
                this.ampmState = ampmState;
            }

            public Builder numberKeysEnabled(int numberKeysEnabledStart, int numberKeysEnabledEnd) {
                this.numberKeysEnabledStart = numberKeysEnabledStart;
                this.numberKeysEnabledEnd = numberKeysEnabledEnd;
                return this;
            }

            public Builder backspaceEnabled(boolean backspaceEnabled) {
                this.backspaceEnabled = backspaceEnabled;
                return this;
            }

            public Builder altKeysEnabled(boolean enabled) {
                leftAltKeyEnabled = rightAltKeyEnabled = enabled;
                return this;
            }

            public Builder headerDisplayFocused(boolean headerDisplayFocused) {
                this.headerDisplayFocused = headerDisplayFocused;
                return this;
            }

            public Builder timeDisplay(CharSequence timeDisplay) {
                this.timeDisplay = timeDisplay;
                return this;
            }

            public Builder ampmDisplay(CharSequence ampmDisplay) {
                this.ampmDisplay = ampmDisplay;
                return this;
            }

            public Builder okButtonEnabled(boolean okButtonEnabled) {
                this.okButtonEnabled = okButtonEnabled;
                return this;
            }

            public TestCase build() {
                return new TestCase(sequence, ampmState, numberKeysEnabledStart, numberKeysEnabledEnd,
                        backspaceEnabled, headerDisplayFocused, leftAltKeyEnabled,
                        rightAltKeyEnabled, okButtonEnabled, timeDisplay, ampmDisplay);
            }
        }
    }
}