package com.tomtom.espresso.test;

import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.not;

import com.tomtom.espresso.test.actions.SpoonScreenshotAction;

import cucumber.api.Scenario;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;

import android.support.test.espresso.EspressoException;
import android.support.test.espresso.NoMatchingViewException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Class containing generic Cucumber test step definitions using Espresso test Instrumentation
 */
public class CucumberHelperTestSteps {

    public static final int RETRY_WAIT = 500;
    private static Scenario scenario;

    @Before
    public static void before(final Scenario scenario) {
        CucumberHelperTestSteps.scenario = scenario;
    }

    public CucumberHelperTestSteps() {
    }

    public static class ScreenshotException extends RuntimeException implements EspressoException {
        private static final long serialVersionUID = -1247022787790657324L;

        ScreenshotException(final String message) {
            super(message);
        }
    }

    public static Scenario getScenario() {
        return CucumberHelperTestSteps.scenario;
    }

    /**
     * Take a screenshot when the test scenario has failed
     */
    public static void takeScreenshotOnFail() {
        if ((scenario != null) && (scenario.isFailed())) {
            takeScreenshot("failed");
        }
    }

    /**
     * Take a screenshot of the current activity and embed it in the HTML report
     * @param tag Name of the screenshot to include in the file name
     */
    public static void takeScreenshot(final String tag) {
        if (scenario == null) {
            throw new ScreenshotException("Error taking screenshot: I'm missing a valid test scenario to attach the screenshot to");
        }
        SpoonScreenshotAction.perform(tag);
        final File screenshot = SpoonScreenshotAction.getLastScreenshot();
        if (screenshot == null) {
            throw new ScreenshotException("Screenshot was not taken correctly, check for failures in screenshot library");
        }
        FileInputStream screenshotStream = null;
        try {
            screenshotStream = new FileInputStream(screenshot);
            final byte fileContent[] = new byte[(int)screenshot.length()];
            screenshotStream.read(fileContent); // Read data from input image file into an array of bytes
            scenario.embed(fileContent, "image/png"); // Embed the screenshot in the report under current test step
        }
        catch (final IOException ioe) {
            throw new ScreenshotException("Exception while reading file " + ioe);
        }
        finally {
            try { // close the streams using close method
                if (screenshotStream != null) {
                    screenshotStream.close();
                }
            }
            catch (final IOException ioe) {
                throw new ScreenshotException("Error while closing stream: " + ioe);
            }
        }
    }

    @Given("^I take a screenshot$")
    public void i_take_a_screenshot() {
        takeScreenshot("screenshot");
    }

    /**
     * Try to press a button with some text or load button if it's in adapter
     * This doesn't guarantee the button is visible to the user, for example:
     * user needs to scroll down to press button
     *
     * @param buttonText
     *            Text of the button to press
     */
    public static void pressButtonWithTextOnce(final String buttonText) {
        try {
            onView(withText(buttonText)).perform(click());
        } catch (final junit.framework.AssertionFailedError e) {
            // When item to click has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(buttonText))).perform(click());
        } catch (final NoMatchingViewException e) {
            // When item to click has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(buttonText))).perform(click());
        } catch (final RuntimeException e) {
            // When item to click has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(buttonText))).perform(click());
        }
    }

    /**
     * Try to press a button with some text and retry for 10 seconds in case it's
     * asynchronously loaded This doesn't guarantee the button is visible
     * to the user, for example: user needs to scroll down to press button
     *
     * @param buttonText
     *            Text of the button to press
     */
    public static void pressButtonWithText(final String buttonText) {
        int retries = 20;
        do {
            try {
                i_see_button_enabled(buttonText, "enabled"); // Only click on enabled buttons
                pressButtonWithTextOnce(buttonText);
                return;
            } catch (final junit.framework.AssertionFailedError e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            } catch (final NoMatchingViewException e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            } catch (final RuntimeException e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            }
        } while (--retries > 0);
        i_see_button_enabled(buttonText, "enabled"); // Only click on enabled buttons
        pressButtonWithTextOnce(buttonText);
    }

    /**
     * Test if some view is displayed and retry for 10 seconds in case it's asynchronously loaded
     *
     * @param id
     *            ID of the view item to test
     */
    public static void checkViewWithIdIsCompletelyDisplayed(final int id) {
        int retries = 20;
        do {
            try {
                onView(withId(id)).check(matches(isCompletelyDisplayed()));
                return;
            } catch (final junit.framework.AssertionFailedError e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            }
        } while (--retries > 0);
        onView(withId(id)).check(matches(isCompletelyDisplayed()));
    }

    /**
     * Test if a view with some text is displayed or try to load it from adapter
     *
     * @param text
     *            Text contained in the view to test
     */
    public static void checkViewWithTextIsCompletelyDisplayedOnce(final String text) {
        try {
            onView(withText(text)).check(matches(isCompletelyDisplayed()));
        } catch (final junit.framework.AssertionFailedError e) {
            // When item to check has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed()));
        } catch (final NoMatchingViewException e) {
            // When item to check has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed()));
        } catch (final RuntimeException e) {
            // When item to check has to be asynchronously loaded in UI from adapter
            onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed()));
        }
    }

    /**
     * Test if some view is displayed and retry for 10 seconds in case it's asynchronously loaded
     *
     * @param text
     *            Text contained in the view to test
     */
    public static void checkViewWithTextIsCompletelyDisplayed(final String text) {
        int retries = 20;
        do {
            try {
                checkViewWithTextIsCompletelyDisplayedOnce(text);
                return;
            } catch (final junit.framework.AssertionFailedError e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            } catch (final NoMatchingViewException e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            } catch (final RuntimeException e) {
                try { // Retry every half a second during 10 seconds
                    Thread.sleep(RETRY_WAIT);
                } catch (final InterruptedException ex) {
                }
            }
        } while (--retries > 0);
        checkViewWithTextIsCompletelyDisplayedOnce(text);
    }

    @Given("^I press \"(.+)\"$")
    public static void i_press_buttonText(final String buttonText) {
        pressButtonWithText(buttonText);
    }

    @Given("^I press \"(.+)\" ([0-9]+) times$")
    public static void i_press_buttonText(final String buttonText, final int repetitions) {
        for (int i = 0; i < repetitions; i++) {
            pressButtonWithText(buttonText);
        }
    }

    @Given("^I see text \"(.+)\"$")
    public static void i_see_text(final String text) {
        checkViewWithTextIsCompletelyDisplayed(text);
    }

    @Given("^I don't see text \"(.+)\"$")
    public static void i_do_not_see_text(final String text) {
        onView(withText(text)).check(matches(not(isDisplayed())));
    }

    @Given("^text \"(.+)\" does(?: not|n't) exist$")
    public static void text_does_not_exist(final String text) {
        onView(withText(text)).check(doesNotExist());
    }

    @Given("^I press the back button$")
    public static void i_press_back() {
        pressBack();
    }

    @Given("^I press the back button ([0-9]+) times$")
    public static void i_press_back(final int repetitions) {
        for (int i = 0; i < repetitions; i++) {
            i_press_back();
        }
    }

    @Given("^I see \"(.+)\" button (enabled|disabled)$")
    public static void i_see_button_enabled(final String buttonText, final String enabled) {
        if ("enabled".equalsIgnoreCase(enabled)) {
            onView(withText(buttonText)).check(matches(isEnabled()));
        } else {
            onView(withText(buttonText)).check(matches(not(isEnabled())));
        }
    }

    @Given("^I wait for ([0-9]+) seconds?$")
    public static void i_wait_for_seconds(final int seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (final InterruptedException e) {
        }
    }
}