# Copyright 2014 Konrad Podloucky
#
# 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.

import unittest

from mock import MagicMock, patch, PropertyMock, DEFAULT

from selenium.common.exceptions import NoSuchElementException

from pytractor.exceptions import AngularNotFoundException
from pytractor.mixins import (WebDriverMixin, angular_wait_required,
                              CLIENT_SCRIPTS_DIR)


try:  # FIXME: find another way
    import builtins
    super_str = 'builtins.super'
except ImportError:
    super_str = 'builtins.super'


class AngularWaitRequiredDecoratorTest(unittest.TestCase):
    @angular_wait_required
    def wrapped_function(self, *args, **kwargs):
        return self.check_function(*args, **kwargs)

    def check_function(self, *args, **kwargs):
        pass

    def test_angular_wait_required(self):
        with patch.multiple(self, wait_for_angular=DEFAULT,
                            check_function=DEFAULT, create=True) as patched:
            mock_wait_for_angular = patched['wait_for_angular']
            mock_check_function = patched['check_function']
            mock_arg = MagicMock()
            mock_kwarg = MagicMock()
            result = self.wrapped_function(mock_arg, kwarg=mock_kwarg)
        # result should be the result of the wrapped function
        self.assertIs(result, mock_check_function.return_value)
        # wait_for_angular() should have been called
        mock_wait_for_angular.assert_called_once_with()
        # the check function should have been called
        mock_check_function.assert_called_once_with(mock_arg,
                                                    kwarg=mock_kwarg)


class WebDriverMixinConstructorTest(unittest.TestCase):
    class ConstructorTester(object):
        """Class for checking calls to __init__"""
        def __init__(self, *args, **kwargs):
            self.constructor_called(*args, **kwargs)

        def constructor_called(self, *args, **kwargs):
            """
            This function will be called by __init__. We can hook onto this
            to check for __init__ calls.
            """
            pass

    class TestDriver(WebDriverMixin, ConstructorTester):
        pass

    def test_constructor(self):
        base_url = 'BASEURL'
        root_element = 'ROOTEL'
        test_timeout = 'TESTTIMEOUT'
        script_timeout = 'SCRIPTTIMEOUT'

        with patch.object(
            self.ConstructorTester, 'constructor_called'
        ) as mock_constructor_called, patch.object(
            self.TestDriver, 'set_script_timeout', create=True
        ) as mock_set_script_timeout:
            instance = self.TestDriver(base_url, root_element,
                                       test_timeout=test_timeout,
                                       script_timeout=script_timeout)

        # verify that properties have been set
        self.assertIs(instance._base_url, base_url)
        self.assertIs(instance._root_element, root_element)
        self.assertIs(instance._test_timeout, test_timeout)
        # verify that the super class' constructor has been called
        mock_constructor_called.assert_called_once_with()
        # verify that set_script_timeout has been called
        mock_set_script_timeout.assert_called_once_with(script_timeout)


class WebDriverMixinTest(unittest.TestCase):
    def setUp(self):
        set_script_timeout_patcher = patch.object(
            WebDriverMixin, 'set_script_timeout', create=True
        )
        self.mock_set_script_timeout = set_script_timeout_patcher.start()
        self.addCleanup(set_script_timeout_patcher.stop)
        self.mock_root_element = MagicMock()
        self.instance = WebDriverMixin('http://localhost',
                                       self.mock_root_element)

    @patch('pytractor.mixins.resource_string')
    def verify__execute_client_script_call(self, is_async, mock_resource_string):
        with patch.multiple(
            self.instance,
            execute_async_script=DEFAULT, execute_script=DEFAULT,
            create=True
        ) as mock_execute:
            (mock_execute_script,
             mock_execute_async_script) = [mock_execute.get(func_name)
                                           for func_name in
                                           ('execute_script',
                                            'execute_async_script')]
            mock_arg = MagicMock()
            result = self.instance._execute_client_script('SCRIPT', mock_arg,
                                                          is_async=is_async)
        # the script was read correctly with resource_string()
        mock_resource_string.assert_called_once_with(
            'pytractor.mixins',
            '{}/{}.js'.format(CLIENT_SCRIPTS_DIR, 'SCRIPT')
        )
        # execute_async_script or execute_script were called (but not both)
        script_content = mock_resource_string.return_value.decode()
        if is_async:
            mock_execute_async_script.assert_called_once_with(script_content,
                                                              mock_arg)
            self.assertEqual(len(mock_execute_script.mock_calls), 0)
            # the result is the one from execute_async_script()
            self.assertIs(result, mock_execute_async_script.return_value)
        else:
            mock_execute_script.assert_called_once_with(script_content,
                                                        mock_arg)
            self.assertEqual(len(mock_execute_async_script.mock_calls), 0)
            # the result is the one from execute_script()
            self.assertIs(result, mock_execute_script.return_value)

    def test__execute_client_script_async(self):
        self.verify__execute_client_script_call(True)

    def test__execute_client_script_sync(self):
        self.verify__execute_client_script_call(False)

    def verify_function_executes_script_with(self, func_to_call,
                                             script_name, *script_args,
                                             **script_kwargs):
        with patch.object(
            self.instance, '_execute_client_script'
        ) as mock_execute_client_script:
            result = func_to_call()
        mock_execute_client_script.assert_called_once_with(
            script_name,
            *script_args, **script_kwargs
        )
        self.assertIs(result, mock_execute_client_script.return_value)

    def test_wait_for_angular(self):
        self.verify_function_executes_script_with(
            self.instance.wait_for_angular,
            'waitForAngular', self.mock_root_element, is_async=True
        )

    def test_wait_for_angular_does_not_call_script_if_ignore_synchronization(
        self
    ):
        """wait_for_angular() must not call the waitForAngular script, if
        ignore_synchronization is set to True."""
        self.instance.ignore_synchronization = True

        with patch.object(
            self.instance, '_execute_client_script'
        ) as mock_execute_client_script:

            self.instance.wait_for_angular()

        self.assertEqual(mock_execute_client_script.call_count, 0)

    def test__test_for_angular(self):
        self.instance._test_timeout = 5000
        self.verify_function_executes_script_with(
            self.instance._test_for_angular,
            'testForAngular', self.instance._test_timeout / 1000
        )

    def test__location_equals(self):
        with patch.object(
            self.instance, 'execute_script', create=True
        ) as mock_execute_script:
            mock_location = MagicMock()
            mock_execute_script.return_value = MagicMock(__eq__=MagicMock())
            result = self.instance._location_equals(mock_location)
        mock_execute_script.assert_called_once_with(
            'return window.location.href'
        )
        script_result = mock_execute_script.return_value
        script_result.__eq__.assert_called_once_with(mock_location)
        self.assertIs(result, script_result.__eq__.return_value)

    # The following tests test some properties that use the wait_for_angular
    # decorator and fetch the property from the super class.
    def verify_super_property_called_with_wait(self, prop_name):
        """
        Verifies that accessing the given property calls the equally
        named property on the super class.
        """
        with patch(
                super_str
        ) as mock_super, patch.object(
            self.instance, 'wait_for_angular'
        ) as mock_wait_for_angular:
            # setup the mocked property
            mock_prop = PropertyMock(name='super.{}'.format(prop_name))
            setattr(type(mock_super.return_value), prop_name, mock_prop)

            result = getattr(self.instance, prop_name)

        mock_wait_for_angular.assert_called_once_with()
        mock_super.assert_called_once_with(WebDriverMixin, self.instance)
        mock_prop.assert_called_once_with()
        self.assertIs(result, mock_prop.return_value)

    def test_current_url(self):
        self.verify_super_property_called_with_wait('current_url')

    def test_page_source(self):
        self.verify_super_property_called_with_wait('page_source')

    def test_title(self):
        self.verify_super_property_called_with_wait('title')

    # Tests for methods that delegate to the super method
    def verify_super_method_called_with_wait(self, method_name):
        """
        Verifies that calling the given method calls the equally
        named method on the super class.
        """
        mock_args = [MagicMock(), MagicMock()]
        with patch(
            super_str
        ) as mock_super, patch.object(
            self.instance, 'wait_for_angular'
        ) as mock_wait_for_angular:
            mock_super_method = getattr(mock_super.return_value, method_name)
            method = getattr(self.instance, method_name)
            result = method(*mock_args)

        mock_wait_for_angular.assert_called_once_with()
        mock_super.assert_called_once_with(WebDriverMixin, self.instance)
        mock_super_method.assert_called_once_with(*mock_args)
        self.assertIs(result, mock_super_method.return_value)

    def test_find_element(self):
        self.verify_super_method_called_with_wait('find_element')

    def test_find_elements(self):
        self.verify_super_method_called_with_wait('find_elements')

    # tests for other methods
    def test_find_elements_by_binding(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        with patch.multiple(
            self.instance, wait_for_angular=DEFAULT,
            _execute_client_script=DEFAULT
        ) as mock_methods:
            result = self.instance.find_elements_by_binding(mock_descriptor,
                                                            mock_using)
        mock_methods['wait_for_angular'].assert_called_once_with()
        mock_methods['_execute_client_script'].assert_called_once_with(
            'findBindings', mock_descriptor, False, mock_using, is_async=False
        )
        self.assertIs(result,
                      mock_methods['_execute_client_script'].return_value)

    def test_find_element_by_binding_no_element(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_binding'
        ) as mock_find_elements_by_binding:
            mock_find_elements_by_binding.return_value = []
            with self.assertRaises(NoSuchElementException):
                self.instance.find_element_by_binding(mock_descriptor,
                                                      mock_using)
        mock_find_elements_by_binding.assert_called_once_with(
            mock_descriptor, using=mock_using
        )

    def test_find_element_by_binding_with_element(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        mock_element = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_binding'
        ) as mock_find_elements_by_binding:
            mock_find_elements_by_binding.return_value = [mock_element]
            result = self.instance.find_element_by_binding(mock_descriptor,
                                                           mock_using)
        mock_find_elements_by_binding.assert_called_once_with(
            mock_descriptor, using=mock_using
        )
        self.assertIs(result, mock_element)

    def test_get_with_angular(self):
        mock_url = MagicMock()
        with patch(
            super_str
        ) as mock_super, patch(
            'pytractor.mixins.WebDriverWait'
        ) as mock_webdriverwait_class, patch.multiple(
            self.instance, _test_for_angular=DEFAULT, execute_script=DEFAULT,
            create=True  # needed for execute_script
        ) as mock_methods:
            mock_test_for_angular = mock_methods['_test_for_angular']
            # return a truthy value to indicate that angular was found
            mock_test_for_angular.return_value = (True,)
            mock_execute_script = mock_methods['execute_script']

            self.instance.get(mock_url)
        mock_super.assert_called_once_with(WebDriverMixin, self.instance)
        mock_super.return_value.get.assert_called_once_with('about:blank')
        self.assertEqual(len(mock_execute_script.mock_calls), 2)
        mock_webdriverwait_class.assert_called_once_with(self.instance,
                                                         10)
        mock_webdriverwait_instance = mock_webdriverwait_class.return_value
        mock_webdriverwait_instance.until_not.assert_called_once_with(
            self.instance._location_equals, 'about:blank'
        )
        mock_test_for_angular.assert_called_once_with()

    def test_get_without_angular(self):
        mock_url = MagicMock()
        with patch(
            super_str
        ) as mock_super, patch(
            'pytractor.mixins.WebDriverWait'
        ) as mock_webdriverwait_class, patch.multiple(
            self.instance, _test_for_angular=DEFAULT, execute_script=DEFAULT,
            create=True  # needed for execute_script
        ) as mock_methods:
            mock_test_for_angular = mock_methods['_test_for_angular']
            # return a falsy value to indicate that angular was not found
            mock_test_for_angular.return_value = (False, 'ERROR')
            mock_execute_script = mock_methods['execute_script']

            with self.assertRaises(AngularNotFoundException):
                self.instance.get(mock_url)
        mock_super.assert_called_once_with(WebDriverMixin, self.instance)
        mock_super.return_value.get.assert_called_once_with('about:blank')
        self.assertEqual(len(mock_execute_script.mock_calls), 1)
        mock_webdriverwait_class.assert_called_once_with(self.instance,
                                                         10)
        mock_webdriverwait_instance = mock_webdriverwait_class.return_value
        mock_webdriverwait_instance.until_not.assert_called_once_with(
            self.instance._location_equals, 'about:blank'
        )
        mock_test_for_angular.assert_called_once_with()

    def test_get_does_not_test_for_angular_if_ignore_synchronization_is_true(
        self
    ):
        """Verify that get() does not call _test_for_angular if
        ignore_synchronization is set to True."""
        mock_url = MagicMock()
        with patch(
            super_str
        ) as mock_super, patch(
            'pytractor.mixins.WebDriverWait'
        ) as mock_webdriverwait_class, patch.multiple(
            self.instance, _test_for_angular=DEFAULT, execute_script=DEFAULT,
            create=True  # needed for execute_script
        ) as mock_methods:
            mock_test_for_angular = mock_methods['_test_for_angular']

            self.instance.ignore_synchronization = True
            self.instance.get(mock_url)

        self.assertEqual(mock_test_for_angular.call_count, 0)

    def test_refresh(self):
        with patch.multiple(
            self.instance, get=DEFAULT, execute_script=DEFAULT,
            create=True  # needed for execute_script()
        ) as mock_methods:
            self.instance.refresh()
        mock_execute_script, mock_get = (mock_methods['execute_script'],
                                         mock_methods['get'])
        self.assertEqual(mock_execute_script.call_count, 1)
        mock_get.assert_called_once_with(mock_execute_script.return_value)

    def test_find_element_by_exact_binding_calls_find_elements(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        mock_element = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_exact_binding',
            return_value=[mock_element]
        ) as mock_find_elements_by_exact_binding:
            result = self.instance.find_element_by_exact_binding(
                mock_descriptor, mock_using
            )
        mock_find_elements_by_exact_binding.assert_called_once_with(
            mock_descriptor, using=mock_using
        )
        self.assertIs(result, mock_element)

    def test_find_element_by_exact_binding_raises_error_if_nothing_found(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_exact_binding', return_value=[]
        ) as mock_find_elements_by_exact_binding:
            with self.assertRaises(NoSuchElementException):
                self.instance.find_element_by_exact_binding(
                    mock_descriptor, mock_using
                )
        mock_find_elements_by_exact_binding.assert_called_once_with(
            mock_descriptor, using=mock_using
        )

    def test_find_elements_by_exact_binding_calls_protractor_script(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()

        with patch.multiple(
            self.instance, wait_for_angular=DEFAULT,
            _execute_client_script=DEFAULT
        ) as mock_methods:
            result = self.instance.find_elements_by_exact_binding(
                mock_descriptor, mock_using
            )

        mock_methods['wait_for_angular'].assert_called_once_with()
        mock_methods['_execute_client_script'].assert_called_once_with(
            'findBindings', mock_descriptor, True, mock_using, is_async=False
        )
        self.assertIs(result,
                      mock_methods['_execute_client_script'].return_value)

    def test_find_element_by_model_calls_find_elements(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_model', return_value=[]
        ) as mock_find_elements_by_model:
            with self.assertRaises(NoSuchElementException):
                self.instance.find_element_by_model(
                    mock_descriptor, mock_using
                )
        mock_find_elements_by_model.assert_called_once_with(
            mock_descriptor, using=mock_using
        )

    def test_find_element_by_model_raises_error_if_nothing_found(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()
        with patch.object(
            self.instance, 'find_elements_by_model', return_value=[]
        ) as mock_find_elements_by_model:
            with self.assertRaises(NoSuchElementException):
                self.instance.find_element_by_model(
                    mock_descriptor, mock_using
                )
        mock_find_elements_by_model.assert_called_once_with(
            mock_descriptor, using=mock_using
        )

    def test_find_elements_by_model_calls_protractor_script(self):
        mock_descriptor = MagicMock()
        mock_using = MagicMock()

        with patch.multiple(
            self.instance, wait_for_angular=DEFAULT,
            _execute_client_script=DEFAULT
        ) as mock_methods:
            result = self.instance.find_elements_by_model(
                mock_descriptor, mock_using
            )

        mock_methods['wait_for_angular'].assert_called_once_with()
        mock_methods['_execute_client_script'].assert_called_once_with(
            'findByModel', mock_descriptor, mock_using, is_async=False
        )
        self.assertIs(result,
                      mock_methods['_execute_client_script'].return_value)

    def test_find_elements_by_model_returns_empty_list_if_script_returns_none(
        self
    ):
        """Verify that find_elements_by_model() returns an empty list if
        the protractor script returns None.
        This is a test for issue #10
        """
        with patch.object(self.instance, '_execute_client_script',
                          return_value=None):
            result = self.instance.find_elements_by_model('does-not-exist')

        self.assertIsInstance(result, list)
        self.assertEqual(len(result), 0)

    def test_location_abs_url_calls_protractor_script(self):
        with patch.multiple(
            self.instance, wait_for_angular=DEFAULT,
            _execute_client_script=DEFAULT
        ) as mock_methods:
            result = self.instance.location_abs_url

        mock_methods['wait_for_angular'].assert_called_once_with()
        mock_methods['_execute_client_script'].assert_called_once_with(
            'getLocationAbsUrl', self.instance._root_element, is_async=False
        )
        self.assertIs(result,
                      mock_methods['_execute_client_script'].return_value)

    def test_set_location_calls_protractor_script(self):
        url = 'http://a.new.locat.ion/'
        with patch.multiple(
            self.instance, wait_for_angular=DEFAULT,
            _execute_client_script=DEFAULT
        ) as mock_methods:
            result = self.instance.set_location(url)

        mock_methods['wait_for_angular'].assert_called_once_with()
        mock_methods['_execute_client_script'].assert_called_once_with(
            'setLocation', self.instance._root_element, url, is_async=False
        )
        self.assertIs(result,
                      mock_methods['_execute_client_script'].return_value)
