/*******************************************************************************
 * JBoss, Home of Professional Open Source
 * Copyright 2009, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 *******************************************************************************/

package org.jboss.test.selenium;

import static com.thoughtworks.selenium.SeleneseTestCase.seleniumEquals;

import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Properties;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.jboss.test.selenium.waiting.Condition;
import org.jboss.test.selenium.waiting.Retrieve;
import org.jboss.test.selenium.waiting.Wait;
import org.testng.Assert;

import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.SeleniumException;

/**
 * Class that serves as basis for all Selenium-based tests.
 * 
 * @author <a href="mailto:lfryc@redhat.com">Lukas Fryc</a>
 * @author <a href="mailto:ppitonak@redhat.com">Pavol Pitonak</a>
 * @version $Revision$
 */
public abstract class AbstractSeleniumTestCase {

    protected Properties locatorsProperties;
    protected Properties messagesProperties;
    protected DefaultSelenium selenium;
    /**
     * Generic timeout in miliseconds used in every selenium function
     * waitForPageToLoad()
     */
    public static String PAGE_LOAD = "180000";
    /** Generic timeout in miliseconds used for AJAX timeouts */
    public static long AJAX_LOAD = 3000;
    /** Element timeout in seconds used in waitForElement functions */
    public static int ELEM_TIMEOUT = 220;

    public AbstractSeleniumTestCase() {
        locatorsProperties = getLocatorsProperties();
        messagesProperties = getMessagesProperties();
    }

    /**
     * This method initializes properties with messages.
     * 
     * Should be overridden to properly initialize properties.
     * 
     * @return initialized properties
     */
    protected Properties getMessagesProperties() {
        return new Properties();
    }

    /**
     * This method initializes properties with locators.
     * 
     * Should be overridden to properly initialize properties.
     * 
     * @return initialized properties
     */
    protected Properties getLocatorsProperties() {
        return new Properties();
    }

    /**
     * Loads properties from specified resource file
     * 
     * @param resource
     *            where in classpath the file is located
     * @return loaded properties
     * @throws IOException
     *             if an error occurred when reading resource file
     */
    protected static Properties getProperties(String resource) throws IOException {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        InputStream is = cl.getResourceAsStream(resource);

        Properties props = new Properties();

        if (is == null) {
            is = AbstractSeleniumTestCase.class.getResourceAsStream(resource);
        }

        if (is != null) {
            props.load(is);
        }

        return props;
    }

    /**
     * Loads properties from specified resource file in context of specified
     * class' package.
     * 
     * @param tClass
     *            named resource will be searched in context of this class
     * @param name
     *            name of resource contained in current class' package
     * @return loaded properties
     * @throws IllegalStateException
     *             if an error occurred when reading resource file
     */
    protected <T> Properties getNamedPropertiesForClass(Class<T> tClass, String name) throws IllegalStateException {
        String propFile = tClass.getPackage().getName();
        propFile = propFile.replace('.', '/');
        propFile = String.format("%s/%s.properties", propFile, name);

        try {
            return getProperties(propFile);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * From given properties class gets property using "property" key or if
     * value with given key doesn't exist, returns substitution'
     * 
     * @param properties
     *            loaded properties
     * @param property
     *            key that will be found in properties
     * @param subst
     *            substitution which will be used in the case that property with
     *            given key doesn't exist
     * @throws IllegalStateException
     *             when property wasn't found and substitution isn't set
     * @return property value for given key or substitution if it doesn't exist
     */
    private String getProperty(Properties properties, String property, String subst) {
        if (properties == null || properties.getProperty(property) == null) {
            if (StringUtils.isEmpty(subst)) {
                throw new IllegalStateException("property '" + property + "' wasn't found and substitution isn't set");
            } else {
                return subst;
            }
        } else {
            return properties.getProperty(property);
        }
    }

    /**
     * Gets the property from messagesProperties. Use getMsg(String, String)
     * instead of this deprecated method.
     * 
     * @param prop
     *            the name of the property
     * @param subst
     *            the value which is returned in the case the property isn't set
     * @throws IllegalStateException
     *             when property wasn't found and substitution isn't set
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMsg(String,
     *      String) getMsg(String, String)
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMessagesProperties()
     *      getMessagesProperties()
     */
    @Deprecated
    public String getMess(String prop, String subst) {
        return getMsg(prop, subst);
    }

    /**
     * Gets the property from messagesProperties. Use getMess(String) instead of
     * this deprecated method.
     * 
     * @param prop
     *            the name of the property
     * @throws IllegalStateException
     *             when property wasn't found
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMsg(String)
     *      getMsg(String)
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMessagesProperties()
     *      getMessagesProperties()
     */
    @Deprecated
    public String getMess(String prop) {
        return getMsg(prop, null);
    }

    /**
     * Gets the property from messagesProperties.
     * 
     * @param prop
     *            the name of the property
     * @param subst
     *            the value which is returned in the case the property isn't set
     * @throws IllegalStateException
     *             when property wasn't found and substitution isn't set
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMessagesProperties()
     *      getMessagesProperties()
     */
    public String getMsg(String prop, String subst) {
        return getProperty(messagesProperties, prop, subst);
    }

    /**
     * Gets the property from messagesProperties.
     * 
     * @param prop
     *            the name of the property
     * @throws IllegalStateException
     *             when property wasn't found
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getMessagesProperties()
     *      getMessagesProperties()
     */
    public String getMsg(String prop) {
        return getMsg(prop, null);
    }

    /**
     * Gets the property from messagesProperties and use it to format Message
     * with given arguments
     * 
     * @param prop
     *            the name of the property with message format.
     * @param args
     *            an array of atributes to be formatted and substituted to prop
     * @throws IllegalStateException
     *             when property wasn't found
     * @return the property
     */
    public String formatMsg(String prop, Object... args) {
        return format(getMsg(prop, null), args);
    }

    /**
     * Gets the property from locatorsProperties
     * 
     * @param prop
     *            the name of the property
     * @param subst
     *            the value which is returned in the case the property isn't set
     * @throws IllegalStateException
     *             when property wasn't found and substitution isn't set
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getLocatorsProperties()
     *      getLocatorsProperties()
     */
    public String getLoc(String prop, String subst) {
        return getProperty(locatorsProperties, prop, subst);
    }

    /**
     * Gets the property from locatorsProperties
     * 
     * @param prop
     *            the name of the property
     * @throws IllegalStateException
     *             when property wasn't found
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getLocatorsProperties()
     *      getLocatorsProperties()
     */
    public String getLoc(String prop) {
        return getLoc(prop, null);
    }

    /**
     * Gets the property from messagesProperties and use it to format Message
     * with given arguments
     * 
     * @param prop
     *            the name of the property with message format.
     * @param args
     *            an array of atributes to be formatted and substituted to prop
     * @throws IllegalStateException
     *             when property wasn't found
     * @return the property
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#getLocatorsProperties()
     *      getLocatorsProperties()
     */
    public String formatLoc(String prop, Object... args) {
        return format(getLoc(prop, null), args);
    }

    /**
     * Uses a MessageFormat.format() to prepare given format string and use it
     * to format result with given arguments.
     * 
     * @param format
     *            string used in MessageFormat.format()
     * @param args
     *            used to formatting given format string
     * @return string formatted using given arguments
     */
    public String format(String format, Object... args) {
        String message = preformat(format);
        return MessageFormat.format(message, args);
    }

    /**
     * Prepares a message to use in Message.format()
     * 
     * @param message
     *            prepared to use in Message.format()
     * @return message prepared to use in Message.format()
     */
    private String preformat(String message) {
        return message.replace("'", "''").replace("\\''", "'");
    }

    /**
     * Click on an element only in the case element is present on the screen.
     * 
     * @param id
     *            element locator
     */
    public void clickIfVisible(String id) {

        if (selenium.isElementPresent(id)) {
            selenium.click(id);
            selenium.waitForPageToLoad(PAGE_LOAD);
        }
    }

    /**
     * Finding the correct row in a table due to input string and columns. The
     * function uses selenium javascript extension findTableRow. It tries to
     * look up the input string in the chosen column in selected table. After
     * the first occurrence of input string, it returns the number of row where
     * it was found.
     * 
     * @param tableLocation
     *            the table locator
     * @param searchName
     *            the string which the function searches for
     * @param searchCol
     *            the column in which the function searches for
     * @return the number of row with the first occurrence of searchName string.
     */
    public int findTableRow(String tableLocation, String searchName, int searchCol) {
        return Integer.valueOf(
                selenium.getEval("selenium.findTableRow(\"" + tableLocation + "\",'" + searchName + "'," + searchCol
                        + ")")).intValue();
    }

    /**
     * Counts table rows for selected table.
     * 
     * @param tableLocation
     *            the table locator
     * @return the number of rows of the table
     */
    public int countTableRows(String tableLocation) {
        return Integer.valueOf(selenium.getEval("selenium.countTableRows(\"" + tableLocation + "\")")).intValue();
    }

    // TODO refactor
    /**
     * Selects a value only if the value is present in the select. If the value
     * is not present, selection fails.
     * 
     * @param locator
     *            the locator of the select tag
     * @param value
     *            the value which should be selected
     */
    public void safeSelect(String locator, String value) {
        waitForElement(locator);

        for (int second = 0;; second++) {
            if (second >= ELEM_TIMEOUT) {
                Assert.fail("Element " + locator + " not found.");
            }
            try {
                String[] opts = selenium.getSelectOptions(locator);
                boolean isAvailable = false;

                for (String opt : opts) {
                    if (opt.equals(value)) {
                        isAvailable = true;
                    }
                }

                if (isAvailable) {
                    break;
                }
            } catch (Exception e) {
            }
            waitFor(1000);
        }

        selenium.select(locator, "label=" + value);
    }

    /**
     * Click and wait. Substitution for two selenium commands click and
     * waitForPageToLoad.
     * 
     * @param locator
     *            the locator of the element to be clicked on
     */
    public void clickAndWait(String locator) {
        waitForElement(locator);
        selenium.click(locator);
        selenium.waitForPageToLoad(PAGE_LOAD);
    }

    /**
     * Open and wait. Substitution for two selenium commands open and
     * waitForPageToLoad.
     * 
     * @param locator
     *            the address to be opened
     */
    public void openAndWait(String locator) {
        selenium.open(locator);
        selenium.waitForPageToLoad(PAGE_LOAD);
    }

    /**
     * Select if not selected. If the value is already selected, the method does
     * nothing.
     * 
     * @param selector
     *            the selector locator
     * @param label
     *            the label to be selected
     */
    public void selectIfNotSelected(String selector, String label) {
        waitForElement(selector);
        if (!selenium.getSelectedLabel(selector).equals(label)) {
            selenium.select(selector, "label=" + label);
            selenium.waitForPageToLoad(PAGE_LOAD);
        }
    }

    /**
     * Asserts the text order on the page. The messages which order is to be
     * determined are inputed in a comma separated way. i.e.
     * "string1;string2;string3". The method asserts that they appear on the
     * page in the same order as typed in the input string. It returns true if
     * the order is the same or false otherwise.
     * 
     * @param text
     *            input text in comma separated format which order is to be
     *            asserted
     * 
     * @return true if the order is the same or false otherwise
     */
    public boolean assertTextOrder(String text) {
        return Boolean.valueOf(selenium.getEval("selenium.assertTextOrder(\"" + text + "\")"));
    }

    /**
     * Waits for specified time in ms. Used mostly in AJAX based tests.
     * 
     * @param time
     *            the time (in ms) to be waited for.
     */
    public void waitFor(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Waits for element to appear on the screen. Used mostly in AJAX based
     * tests.
     * 
     * @param locator
     *            the locator of the element to be waited for
     */
    public void waitForElement(String locator) {
        waitForElement(locator, 1000);
    }
    
    /**
     * Waits for element to appear on the screen. Used mostly in AJAX based
     * tests.
     * 
     * @param locator
     *            the locator of the element to be waited for
     * @param step interval between two pollings
     */
    public void waitForElement(final String locator, int step) {
        Wait.failWith("Element \"" + locator + "\" not found.").interval(step).timeout(ELEM_TIMEOUT).until(new Condition() {
            public boolean isTrue() {
                return selenium.isElementPresent(locator);
            }
        });
    }

    /**
     * Waits for element to appear on the screen. Used mostly in AJAX based
     * tests.
     * 
     * @param locatorElem
     *            the element locator
     * @param timeToWait
     *            the time (in ms) to wait until timeout is reached
     */
    public void waitForElement(String locatorElem, long timeToWait) {
        for (int i = 0;; i++) {
            if (i * 500 >= timeToWait) {
                Assert.fail("Element " + locatorElem + " not found.");
            }
            try {
                if (selenium.isElementPresent(locatorElem)) {
                    break;
                }
            } catch (Exception e) {
            }
            waitFor(500);
        }
    }

    /**
     * Waits for element to appear on the screen. Used mostly in AJAX based
     * tests.
     * 
     * @param locatorElem
     *            the element locator
     * @param locatorLink
     *            the link locator - not used
     * @param timeToWait
     *            the time (in ms) to wait until timeout is reached
     * @param n
     *            the n
     * @see org.jboss.test.selenium.AbstractSeleniumTestCase#waitForElement(String,
     *      long) waitForElement(String, long)
     */
    @Deprecated
    public void waitForElement(String locatorElem, String locatorLink, long timeToWait, long n) {
        for (int i = 0;; i++) {
            if (i >= n) {
                Assert.fail("Element " + locatorElem + " not found.");
            }
            try {
                if (selenium.isElementPresent(locatorElem)) {
                    break;
                }
            } catch (Exception e) {
            }
            waitFor(1000);
        }
    }

    /**
     * Waits for text to appear on the screen. Used mostly in AJAX based tests.
     * 
     * @param text
     *            the text to be waited for
     */
    public void waitForText(String text) {
        for (int second = 0;; second++) {
            if (second >= ELEM_TIMEOUT) {
                Assert.fail("Text '" + text + "' not found.");
            }
            try {
                if (selenium.isTextPresent(text)) {
                    break;
                }
            } catch (Exception e) {
            }
            waitFor(100);
        }
    }

    /**
     * Waits for element specified by locator does contain text specified by
     * pattern
     * 
     * @param locator
     *            text of this locator's element will be tested for equivalence
     *            to pattern text
     * @param pattern
     *            pattern text, which will be tested to equivalence to element's
     *            text given by locator
     * 
     */
    public void waitForTextEquals(final String locator, final String pattern) {
        Wait.until(new Condition() {
            public boolean isTrue() {
                return seleniumEquals(selenium.getText(locator), pattern);
            }
        });
    }

    /**
     * Wait for text of element given by locator changes from lastText to
     * another text.
     * 
     * @param locator
     *            locator of element
     * @param lastText
     *            current text what we are waiting for change
     */
    public void waitForTextChanges(final String locator, final String lastText) {
        waitForTextChangesAndReturn(locator, lastText);
    }

    /**
     * Wait for text of element given by locator changes from lastText to
     * another text and returns this new text.
     * 
     * @param locator
     *            locator of element
     * @param lastText
     *            current text what we are waiting for change
     * @return new value of text
     */
    public String waitForTextChangesAndReturn(final String locator, final String lastText) {
        return Wait.waitForChangeAndReturn(lastText, new Retrieve<String>() {
            public String retrieve() {
                return selenium.getText(locator);
            }
        });
    }

    /**
     * Wait for attribute of element given by attributeLocator changes from
     * attributeValue to another value.
     * 
     * @param attributeLocator
     *            locator of attribute
     * @param attributeValue
     *            current value attribute what we are waiting for change
     */
    public void waitForAttributeChanges(final String attributeLocator, final String attributeValue) {
        waitForAttributeChangesAndReturn(attributeLocator, attributeValue);
    }

    /**
     * Wait for attribute of element given by attributeLocator changes from
     * attributeValue to another value and returns this new value.
     * 
     * @param attributeLocator
     *            locator of attribute
     * @param attributeValue
     *            current value attribute what we are waiting for change
     * @return new value of attribute
     */
    public String waitForAttributeChangesAndReturn(final String attributeLocator, final String attributeValue) {
        return Wait.waitForChangeAndReturn(attributeValue, new Retrieve<String>() {
            public String retrieve() {
                return selenium.getAttribute(attributeLocator);
            }
        });
    }

    /**
     * Get current style value of element given by locator.
     * 
     * Use CSS style notation instead of JavaScript's camel notation!
     * 
     * This methods of getting current style value haven't absolute browser
     * compatibility.
     * 
     * E.g.: use property "background-color" instead of "backgroundColor"
     * 
     * @param locator
     *            of element from what we want to get current style value
     * @param property
     *            CSS style property what we can to recognize
     * @return current value of property if its element exists and has this
     *         property value set; null value otherwise
     * @throws IllegalStateException
     *             if is caught unrecognized throwable
     */
    public String getStyle(String locator, String property) {
        String evaluate = MessageFormat.format("selenium.getStyle(\"{0}\", \"{1}\")", locator, property);
        String result = null;
        try {
            result = selenium.getEval(evaluate);
        } catch (Exception e) {
            if ("ERROR: Threw an exception: null property value".equals(e.getMessage())) {
                return null;
            }
            throw new SeleniumException(e);
        }
        return result;
    }

    /**
     * Aligns screen to top (resp. bottom) of element given by locator.
     * 
     * TODO should be reimplemented to use of JQuery.scrollTo
     * 
     * @param locator
     *            of element which should be screen aligned to
     * @param alignToTop
     *            should be top border of screen aligned to top border of
     *            element
     */
    public void scrollIntoView(String locator, boolean alignToTop) {
        String evaluate = MessageFormat.format("selenium.scrollIntoView(\"{0}\", {1})", locator, Boolean
                .toString(alignToTop));
        selenium.getEval(evaluate);
    }

    /**
     * Returns the count of elements for given jQuery locator. 
     * @param locator a jQuery locator that doesn't end with :eq(), :last, :first, :odd, etc.
     * @return count of elements matching given locator
     */
    public int getJQueryCount(String locator) {
        int count = 0;
        for (int i = 0;; i++) {
            if (selenium.isElementPresent(format(locator + ":eq({0})", i))) {
                count++;
            } else {
                break;
            }
        }
        return count;
    }

    /**
     * Checks if element given by locator is member of CSS class given by
     * className.
     * 
     * @param className
     *            name of CSS class
     * @param locator
     *            element's locator
     * @return true if element given by locator is member of CSS class given by
     *         className
     */
    public boolean belongsClass(String className, String locator) {
        Validate.notNull(className);
        Validate.notNull(locator);

        String classLocator = format("{0}@class", locator);
        String classNames = getAttributeOrNull(classLocator);

        if (classNames == null) {
            return false;
        }

        String regex = format("(?:^|.*\\s){0}(?:$|\\s.*)", className);
        return classNames.matches(regex);
    }

    /**
     * Returns whether the element is displayed on the page.
     * 
     * @param locator
     *            element locator
     * @return if style contains "display: none;" returns false, else returns
     *         true
     */
    public boolean isDisplayed(String locator) {
        try {
            return !getStyle(locator, "display").contains("none");
        } catch (Exception e) {
            // there is no attribute "style"
            return true;
        }
    }

    /**
     * Simulates a user hovering a mouse over the specified element at specific
     * coordinates relative to element.
     * 
     * @param locator
     *            element's locator
     * @param coordString
     *            specifies the x,y position (i.e. - 10,20) of the mouse event
     *            relative to the element returned by the locator.
     */
    public void mouseOverAt(String locator, String coordString) {
        selenium.getEval(format("selenium.doMouseOverAt(\"{0}\", \"{1}\")", locator, coordString));
    }

    /**
     * Gets the text of an element. This works for any element that contains
     * text. This command uses either the textContent (Mozilla-like browsers) or
     * the innerText (IE-like browsers) of the element, which is the rendered
     * text shown to the user.
     * 
     * If no element with given locator is found, returns null.
     * 
     * @param locator
     *            an element locator
     * @return the text of the element or null if element's wasn't found
     */
    public String getTextOrNull(String locator) {
        try {
            return selenium.getEval(format("selenium.getTextOrNull(\"{0}\")", locator));
        } catch (RuntimeException e) {
            if ("ERROR: Threw an exception: element is not found".equals(e.getMessage())) {
                return null;
            }
            throw e;
        }
    }

    /**
     * Gets the value of an element attribute. The value of the attribute may
     * differ across browsers (this is the case for the "style" attribute, for
     * example).
     * 
     * If no element with given attribute's locator is found, returns null.
     * 
     * @param attributeLocator
     *            an element's attribute locator
     * @return the attribute of the element or null if element's attribute
     *         wasn't found
     */
    public String getAttributeOrNull(String attributeLocator) {
        try {
            return selenium.getAttribute(attributeLocator);
        } catch (Exception e) {
            if (e.getMessage().startsWith("ERROR: Could not find element attribute:")
                    || e.getMessage().matches("^ERROR: Element .* not found$")) {
                return null;
            }
            throw new IllegalStateException("getAttributeOrNull unexpected state - " + e.getMessage(), e);
        }
	}

	/**
	 * Add required script to page once.
	 * 
	 * (This code will refuse adding script to the page duplicitly due to usage
	 * of hash, so you can add it thought you are not sure script is already
	 * added or not - this is usefull for adding libraries directly to the
	 * page).
	 * 
	 * This script will be appended to end of the body tag within the script
	 * tag.
	 * 
	 * @param script what should be added to the page
	 */
	public void addRequiredScript(String script) {
		final String escapedScript = StringEscapeUtils.escapeJavaScript(script);
		final String id = "selenium_script_" + Integer.toString(escapedScript.hashCode());
		if (selenium.isElementPresent(id)) {
			return;
		}
		selenium.getEval(format("selenium.addScriptLocally('{0}', '{1}')", id, escapedScript));
	}
}
