/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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 com.android.deskclock.timer;

import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.IdRes;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

import com.android.deskclock.DeskClock;
import com.android.deskclock.R;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Timer;
import com.android.deskclock.widget.MockFabContainer;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Locale;

import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

/**
 * Exercise the user interface that collects new timer lengths.
 */
@RunWith(AndroidJUnit4ClassRunner.class)
public class TimerSetupViewTest {

    private MockFabContainer fabContainer;
    private TimerSetupView timerSetupView;

    private TextView timeView;
    private View deleteView;

    private Locale defaultLocale;

    @Rule
    public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);

    @Before
    public void setUp() {
        defaultLocale = Locale.getDefault();
        Locale.setDefault(new Locale("en", "US"));
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        final TimerFragment fragment = new TimerFragment();
        rule.getActivity().getSupportFragmentManager()
                .beginTransaction().add(fragment, null).commit();
        Runnable selectTabRunnable = () -> {
            fragment.selectTab();
            fabContainer = new MockFabContainer(fragment, context);
            fragment.setFabContainer(fabContainer);

            // Fetch the child views the tests will manipulate.
            final View view = fragment.getView();
            assertNotNull(view);

            timerSetupView = view.findViewById(R.id.timer_setup);
            assertNotNull(timerSetupView);
            assertEquals(VISIBLE, timerSetupView.getVisibility());

            timeView = timerSetupView.findViewById(R.id.timer_setup_time);
            timeView.setActivated(true);
            deleteView = timerSetupView.findViewById(R.id.timer_setup_delete);
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(selectTabRunnable);
    }

    @After
    public void tearDown() {
        fabContainer = null;
        timerSetupView = null;

        timeView = null;
        deleteView = null;

        Locale.setDefault(defaultLocale);
    }

    private void validateDefaultState() {
        assertIsReset();
    }

    @Test
    public void validateDefaultState_TimersExist() {
        Runnable addTimerRunnable = () -> {
            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
            validateDefaultState();
            DataModel.getDataModel().removeTimer(timer);
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
    }

    @Test
    public void validateDefaultState_NoTimersExist() {
        Runnable runnable = () -> {
            validateDefaultState();
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
    }

    private void type0InDefaultState() {
        performClick(R.id.timer_setup_digit_0);
        assertIsReset();
    }

    @Test
    public void type0InDefaultState_TimersExist() {
        Runnable addTimerRunnable = () -> {
            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
            type0InDefaultState();
            DataModel.getDataModel().removeTimer(timer);
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
    }

    @Test
    public void type0InDefaultState_NoTimersExist() {
        Runnable runnable = () -> {
            type0InDefaultState();
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
    }

    private void fillDisplayThenDeleteAll() {
        assertIsReset();
        // Fill the display.
        performClick(R.id.timer_setup_digit_1);
        assertHasValue(0, 0, 1);
        performClick(R.id.timer_setup_digit_2);
        assertHasValue(0, 0, 12);
        performClick(R.id.timer_setup_digit_3);
        assertHasValue(0, 1, 23);
        performClick(R.id.timer_setup_digit_4);
        assertHasValue(0, 12, 34);
        performClick(R.id.timer_setup_digit_5);
        assertHasValue(1, 23, 45);
        performClick(R.id.timer_setup_digit_6);
        assertHasValue(12, 34, 56);

        // Typing another character is ignored.
        performClick(R.id.timer_setup_digit_7);
        assertHasValue(12, 34, 56);
        performClick(R.id.timer_setup_digit_8);
        assertHasValue(12, 34, 56);

        // Delete everything in the display.
        performClick(R.id.timer_setup_delete);
        assertHasValue(1, 23, 45);
        performClick(R.id.timer_setup_delete);
        assertHasValue(0, 12, 34);
        performClick(R.id.timer_setup_delete);
        assertHasValue(0, 1, 23);
        performClick(R.id.timer_setup_delete);
        assertHasValue(0, 0, 12);
        performClick(R.id.timer_setup_delete);
        assertHasValue(0, 0, 1);
        performClick(R.id.timer_setup_delete);
        assertIsReset();
    }

    @Test
    public void fillDisplayThenDeleteAll_TimersExist() {
        Runnable addTimerRunnable = () -> {
            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
            fillDisplayThenDeleteAll();
            DataModel.getDataModel().removeTimer(timer);
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
    }

    @Test
    public void fillDisplayThenDeleteAll_NoTimersExist() {
        Runnable runnable = () -> {
            fillDisplayThenDeleteAll();
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
    }

    private void fillDisplayWith9s() {
        performClick(R.id.timer_setup_digit_9);
        performClick(R.id.timer_setup_digit_9);
        performClick(R.id.timer_setup_digit_9);
        performClick(R.id.timer_setup_digit_9);
        performClick(R.id.timer_setup_digit_9);
        performClick(R.id.timer_setup_digit_9);
        assertHasValue(99, 99, 99);
    }

    @Test
    public void fillDisplayWith9s_TimersExist() {
        Runnable addTimerRunnable = () -> {
            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
            fillDisplayWith9s();
            DataModel.getDataModel().removeTimer(timer);
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
    }

    @Test
    public void fillDisplayWith9s_NoTimersExist() {
        Runnable runnable = () -> {
            fillDisplayWith9s();
        };
        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
    }

    private void assertIsReset() {
        assertStateEquals(0, 0, 0);
        assertFalse(timerSetupView.hasValidInput());
        assertEquals(0, timerSetupView.getTimeInMillis());

        assertTrue(TextUtils.equals("00h 00m 00s", timeView.getText()));
        assertTrue(TextUtils.equals("0 hours, 0 minutes, 0 seconds",
                timeView.getContentDescription()));

        assertFalse(deleteView.isEnabled());
        assertTrue(TextUtils.equals("Delete", deleteView.getContentDescription()));

        final View fab = fabContainer.getFab();
        final TextView leftButton = fabContainer.getLeftButton();
        final TextView rightButton = fabContainer.getRightButton();

        if (DataModel.getDataModel().getTimers().isEmpty()) {
            assertEquals(INVISIBLE, leftButton.getVisibility());
        } else {
            assertEquals(VISIBLE, leftButton.getVisibility());
            assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
        }

        assertNull(fab.getContentDescription());
        assertEquals(INVISIBLE, fab.getVisibility());
        assertEquals(INVISIBLE, rightButton.getVisibility());
    }

    private void assertHasValue(int hours, int minutes, int seconds) {
        final long time =
                hours * HOUR_IN_MILLIS + minutes * MINUTE_IN_MILLIS + seconds * SECOND_IN_MILLIS;
        assertStateEquals(hours, minutes, seconds);
        assertTrue(timerSetupView.hasValidInput());
        assertEquals(time, timerSetupView.getTimeInMillis());

        final String timeString =
                String.format(Locale.US, "%02dh %02dm %02ds", hours, minutes, seconds);
        assertTrue(TextUtils.equals(timeString, timeView.getText()));

        assertTrue(deleteView.isEnabled());
        assertTrue(TextUtils.equals("Delete " + seconds % 10, deleteView.getContentDescription()));

        final View fab = fabContainer.getFab();
        final TextView leftButton = fabContainer.getLeftButton();
        final TextView rightButton = fabContainer.getRightButton();

        if (DataModel.getDataModel().getTimers().isEmpty()) {
            assertEquals(INVISIBLE, leftButton.getVisibility());
        } else {
            assertEquals(VISIBLE, leftButton.getVisibility());
            assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
        }

        assertEquals(VISIBLE, fab.getVisibility());
        assertTrue(TextUtils.equals("Start", fab.getContentDescription()));
        assertEquals(INVISIBLE, rightButton.getVisibility());
    }

    private void assertStateEquals(int hours, int minutes, int seconds) {
        final int[] expected = {
                seconds % 10, seconds / 10, minutes % 10, minutes / 10, hours % 10, hours / 10
        };
        final int[] actual = (int[]) timerSetupView.getState();
        assertArrayEquals(expected, actual);
    }

    private void performClick(@IdRes int id) {
        final View view = timerSetupView.findViewById(id);
        assertNotNull(view);
        assertTrue(view.performClick());
    }
}
