# -*- coding: utf-8 -*-
#
# Copyright 2011-2012 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the
# OpenSSL library under certain conditions as described in each
# individual source file, and distribute linked combinations
# including the two.
# You must obey the GNU General Public License in all respects
# for all of the code used other than OpenSSL.  If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so.  If you
# do not wish to do so, delete this exception statement from your
# version.  If you delete this exception statement from all source
# files in the program, then also delete it here.
"""Test the Ui code."""

from PyQt4 import QtGui, QtCore
from twisted.internet import defer
from twisted.trial.unittest import TestCase

from ubuntu_kylin_sso import main, NO_OP
from ubuntu_kylin_sso.qt import ERROR_STYLE, maybe_elide_text, TITLE_STYLE
from ubuntu_kylin_sso.tests import (
    APP_NAME,
    HELP_TEXT,
    PING_URL,
    POLICY_URL,
    TC_URL,
    WINDOW_ID,
)
from ubuntu_kylin_sso.utils import compat

KWARGS = dict(app_name=APP_NAME, help_text=HELP_TEXT, ping_url=PING_URL,
              policy_url=POLICY_URL, tc_url=TC_URL, window_id=WINDOW_ID)

# is ok to access private method/attrs in tests
# pylint: disable=W0212


def build_string_for_pixels(label, width):
    """Return a random string that will be as big as 'width' in pixels."""
    char = 'm'
    fm = QtGui.QFontMetrics(label.font())
    pixel_width = fm.width(char)
    chars = int(width / pixel_width)
    result = char * chars
    assert pixel_width * chars <= width
    return result


class FakedObject(object):
    """Fake an object, record every call."""

    next_result = None
    exposed_methods = []

    def __init__(self, *args, **kwargs):
        self._args = args
        self._kwargs = kwargs
        self._called = {}
        for i in self.exposed_methods:
            setattr(self, i, self._record_call(i))

    def _record_call(self, func_name):
        """Store values when calling 'func_name'."""

        def inner(*args, **kwargs):
            """Fake 'func_name'."""
            self._called[func_name] = (args, kwargs)
            return self.next_result

        return inner


class FakedSSOLoginBackend(FakedObject):
    """A faked sso_login backend."""

    exposed_methods = [
        'connect_to_signal',
        'generate_captcha',
        'login',
        'login_and_ping',
        'register_user',
        'request_password_reset_token',
        'set_new_password',
        'validate_email',
        'validate_email_and_ping',
    ]


class FakedBackend(object):
    """A faked backend."""

    sso_login = FakedSSOLoginBackend()


class FakePageUiStyle(object):
    """Fake the page."""

    def __init__(self):
        self.ui = self
        self.properties = {}
        super(FakePageUiStyle, self).__init__()

    def wizard(self):
        """Use itself as a fake wizard, too."""
        return self

    def text(self):
        """Return text."""
        return self.properties.get('text', '')

    # setText, setEnabled are inherited
    # pylint: disable=C0103
    def setText(self, text):
        """Save text."""
        self.properties['text'] = text

    def setEnabled(self, value):
        """Fake setEnabled."""
        self.properties['enabled'] = value

    def enabled(self):
        """Fake enabled."""
        return self.properties.get('enabled', False)

    def isEnabled(self):
        """Fake isEnabled."""
        return self.properties.get('enabled', False)

    def setProperty(self, key, val):
        """Fake setProperty to restyle some widget."""
        self.properties[key] = val

    def property(self, key):
        """Fake property from widget style."""
        return self.properties.get(key, False)

    def setDefault(self, val):
        """Fake button setDefault."""
        self.properties['default'] = val

    def style(self):
        """Fake style."""
        return self

    def unpolish(self, *args):
        """Fake unpolish."""
        self.properties['unpolish'] = len(args) > 0

    def polish(self, *args):
        """Fake polish."""
        self.properties['polish'] = len(args) > 0

    def setVisible(self, val):
        """Fake setVisible from Qt."""
        self.properties['visible'] = val

    def isVisible(self):
        """Fake isVisible from Qt."""
        return self.properties['visible']


class FakeOverlay(object):

    """A fake delay overlay."""

    def __init__(self, *args, **kwargs):
        """Initialize."""
        self.show_counter = 0
        self.hide_counter = 0
        self.args = (args, kwargs)

    def show(self):
        """Fake show."""
        self.show_counter += 1

    def hide(self):
        """Fake hide."""
        self.hide_counter += 1

    def resize(self, *args):
        """Fake resize."""


class FakeSignal(object):

    """A fake PyQt signal."""

    def __init__(self, *args, **kwargs):
        """Initialize."""
        self.target = None

    def connect(self, target):
        """Fake connect."""
        self.target = target

    def disconnect(self, *args):
        """Fake disconnect."""
        self.target = None

    def emit(self, *args):
        """Fake emit."""
        if self.target:
            self.target(*args)


class FakeOverlaySignal(FakeSignal):

    """Fake Signal for show and hide overlay."""

    def __init__(self, target):
        super(FakeOverlaySignal, self).__init__()
        self.target = target

    def connect(self, target):
        """We ignore the target and just call the function from the init."""


class FakeWizardPage(object):

    """A fake wizard page."""

    def __init__(self, *args, **kwargs):
        self.has_back_button = True

    # pylint: disable=C0103
    def initializePage(self):
        """Fake initializePage."""


class FakeMainWindow(object):

    """A fake MainWindow."""

    currentIdChanged = FakeSignal()
    loginSuccess = FakeSignal()
    registrationIncomplete = FakeSignal()
    registrationSuccess = FakeSignal()
    userCancellation = FakeSignal()
    shown = False
    SYNC_NOW_OR_LATER_PAGE = 4
    CONGRATULATIONS_PAGE = 5
    folders_page_id = 6
    local_folders_page_id = 7

    def __init__(self, close_callback=None):
        self.button_texts = []
        self.button_layout = None
        self.options = []
        self.overlay = FakeOverlay()
        self.local_folders_page = FakeWizardPage()
        self.folders_page = FakeWizardPage()
        self.app_name = APP_NAME

    def show(self):
        """Fake method."""
        self.shown = True

    # pylint: disable=C0103
    def setButtonText(self, button, text):
        """Fake setButtonText."""
        self.button_texts.append((button, text))

    def setButtonLayout(self, layout):
        """Fake setButtonText."""
        self.button_layout = layout

    def button(self, button):
        """Fake button method to obtain the wizard buttons."""
        return QtGui.QPushButton()

    def setOption(self, *args):
        """Fake setOption."""
        self.options.append(args)

    def next(self):
        """Fake next."""

    def reject(self):
        """Fake reject."""


class FakeController(object):

    """A fake controller for the tests."""

    is_fake = True
    set_next_validation = lambda *a, **kw: None

    def __init__(self, *args, **kwargs):
        self.args = (args, kwargs)

    # pylint: disable=C0103
    def setupUi(self, view):
        """Fake the setup."""
    # pylint: enable=C0103


class FakeWizard(object):

    """Replace wizard() function on wizard pages."""

    customButtonClicked = QtCore.QObject()
    sign_in_page_id = 1
    setup_account_id = 2
    current_user_id = 3
    email_verification_id = 4
    success_id = 5
    error_id = 6
    forgotten_id = 7
    reset_password_id = 8

    def __init__(self):
        self.overlay = FakeOverlay()
        self.called = []
        self.buttons = {}
        self.buttons_text = {}
        self._next_id = -1
        self.sign_in_page = object()
        self.setup_account = object()
        self.current_user = object()
        self.email_verification = object()
        self.success = object()
        self.error = object()
        self.forgotten = object()
        self.reset_password = object()
        self._pages = {
            self.sign_in_page: self.sign_in_page_id,
            self.setup_account: self.setup_account_id,
            self.current_user: self.current_user_id,
            self.email_verification: self.email_verification_id,
            self.success: self.success_id,
            self.error: self.error_id,
            self.forgotten: self.forgotten_id,
            self.reset_password: self.reset_password_id,
        }

    # Invalid name "setButtonLayout", "setOption"
    # pylint: disable=C0103

    def setButtonText(self, key, value):
        """Fake setButtonText."""
        self.buttons_text[key] = value

    def setButtonLayout(self, *args, **kwargs):
        """Fake the functionality of setButtonLayout on QWizard class."""
        self.called.append(('setButtonLayout', (args, kwargs)))

    def setOption(self, *args, **kwargs):
        """Fake the functionality of setOption on QWizard class."""
        self.called.append(('setOption', (args, kwargs)))

    # pylint: enable=C0103

    def button(self, button_id):
        """Fake the functionality of button on QWizard class."""
        return self.buttons.setdefault(button_id, QtGui.QPushButton())

    def next(self):
        """Fake next."""
        self.called.append('next')

    def page(self, p_id):
        """Fake page method."""
        return self

    def set_titles(self, title):
        """Fake set_titles."""
        self.called.append(('set_titles', title))


class FakeWizardButtonStyle(FakeWizard):

    """Fake Wizard with button style implementation."""

    SIGN_IN_PAGE_ID = 1

    # pylint: disable=C0103
    def __init__(self):
        super(FakeWizardButtonStyle, self).__init__()
        self.data = {}
        self.customButtonClicked = self

    def setDefault(self, value):
        """Fake setDefault for button."""
        self.data['default'] = value

    def isDefault(self):
        """Fake isDefault."""
        return self.data['default']
    # pylint: enable=C0103

    def connect(self, func):
        """Fake customButtonClicked connect."""
        self.data['connect'] = func

    def disconnect(self, func):
        """Fake customButtonClicked disconnect."""
        self.data['disconnect'] = func

    def button(self, button_id):
        """Fake the functionality of button on QWizard class."""
        return self

    def style(self):
        """Fake button style."""
        return self

    def next(self):
        """Fake next for wizard."""
        self.data['next'] = True


class BaseTestCase(TestCase):
    """The base test case."""

    kwargs = None
    ui_class = None
    ui_signals = ()
    ui_backend_signals = ()
    ui_wizard_class = FakeWizard

    @defer.inlineCallbacks
    def setUp(self):
        yield super(BaseTestCase, self).setUp()
        self._called = False
        backend = FakedBackend()
        self.sso_login_backend = backend.sso_login
        self.patch(main, 'get_sso_client',
                   lambda *a: defer.succeed(backend))

        self.app_name = APP_NAME
        self.ping_url = PING_URL
        self.signal_results = []

        # self.ui_class is not callable
        # pylint: disable=E1102, C0103, W0212
        self.ui = None
        self.wizard = None
        if self.ui_class is not None:
            kwargs = dict(KWARGS) if self.kwargs is None else self.kwargs
            self.ui = self.ui_class(**kwargs)

            for signal in self.ui_signals:
                self.patch(self.ui_class, signal, FakeSignal())

            if self.ui_wizard_class is not None:
                self.wizard = self.ui_wizard_class()
                self.patch(self.ui, 'wizard', lambda: self.wizard)

            self.ui.show()
            self.addCleanup(self.ui.hide)

    def _set_called(self, *args, **kwargs):
        """Store 'args' and 'kwargs' for test assertions."""
        self._called = (args, kwargs)

    def assert_backend_called(self, method, *args, **kwargs):
        """Check that 'method(*args, **kwargs)' was called in the backend."""
        self.assertIn(method, self.ui.backend._called)

        call = self.ui.backend._called[method]
        self.assertEqual(call[0], args)

        reply_handler = call[1].pop('reply_handler')
        self.assertEqual(reply_handler, NO_OP)

        error_handler = call[1].pop('error_handler')
        self.assertEqual(error_handler.func, self.ui._handle_error)

        self.assertEqual(call[1], kwargs)

    def assert_signal_emitted(self, signal, signal_args,
                              trigger, *args, **kwargs):
        """Check that 'trigger(*signal_args)' emits 'signal(*signal_args)'."""
        signal.connect(lambda *a: self.signal_results.append(a))

        trigger(*args, **kwargs)

        self.assertEqual(self.signal_results, [signal_args])

    def assert_title_correct(self, title_label, expected, max_width):
        """Check that the label's text is equal to 'expected'."""
        label = QtGui.QLabel()
        maybe_elide_text(label, expected, max_width)

        self.assertEqual(TITLE_STYLE % compat.text_type(label.text()),
                         compat.text_type(title_label.text()))
        self.assertEqual(compat.text_type(label.toolTip()),
                         compat.text_type(title_label.toolTip()))
        self.assertTrue(title_label.isVisible())

    def assert_subtitle_correct(self, subtitle_label, expected, max_width):
        """Check that the subtitle is equal to 'expected'."""
        label = QtGui.QLabel()
        maybe_elide_text(label, expected, max_width)

        self.assertEqual(compat.text_type(label.text()),
                         compat.text_type(subtitle_label.text()))
        self.assertEqual(compat.text_type(label.toolTip()),
                         compat.text_type(subtitle_label.toolTip()))
        self.assertTrue(subtitle_label.isVisible())

    def assert_error_correct(self, error_label, expected, max_width):
        """Check that the error 'error_label' displays 'expected' as text."""
        label = QtGui.QLabel()
        maybe_elide_text(label, expected, max_width)

        self.assertEqual(ERROR_STYLE % compat.text_type(label.text()),
                         compat.text_type(error_label.text()))
        self.assertEqual(compat.text_type(label.toolTip()),
                         compat.text_type(error_label.toolTip()))
        self.assertTrue(error_label.isVisible())

    def get_pixmap_data(self, pixmap):
        """Get the raw data of a QPixmap."""
        byte_array = QtCore.QByteArray()
        array_buffer = QtCore.QBuffer(byte_array)
        pixmap.save(array_buffer, "PNG")
        return byte_array

    # Invalid name "assertEqualPixmap"
    # pylint: disable=C0103

    def assertEqualPixmaps(self, pixmap1, pixmap2):
        """Compare two Qt pixmaps."""
        d1 = self.get_pixmap_data(pixmap1)
        d2 = self.get_pixmap_data(pixmap2)
        self.assertEqual(d1, d2)

    # pylint: enable=C0103

    @defer.inlineCallbacks
    def test_setup_page(self):
        """Test the backend signal connection."""
        if self.ui is None or getattr(self.ui, 'setup_page', None) is None:
            return

        called = []
        self.patch(self.ui, '_setup_signals',
                   lambda *a: called.append('_setup_signals'))
        self.patch(self.ui, '_connect_ui',
                   lambda *a: called.append('_connect_ui'))
        self.patch(self.ui, '_set_translated_strings',
                   lambda *a: called.append('_set_translated_strings'))

        yield self.ui.setup_page()

        self.assertEqual(self.ui.backend, self.sso_login_backend)

        for signal in self.ui_backend_signals:
            self.assertIn(signal, self.ui._signals)
            self.assertTrue(callable(self.ui._signals[signal]))

        expected = ['_set_translated_strings', '_connect_ui', '_setup_signals']
        self.assertEqual(expected, called)


class PageBaseTestCase(BaseTestCase):

    """BaseTestCase with some specialization for the Wizard Pages."""

    @defer.inlineCallbacks
    def setUp(self):
        yield super(PageBaseTestCase, self).setUp()
        self._overlay_show_counter = 0
        self._overlay_hide_counter = 0

        if self.ui is not None:
            fake_show_overlay_signal = FakeOverlaySignal(
                self._show_overlay_slot)
            fake_hide_overlay_signal = FakeOverlaySignal(
                self._hide_overlay_slot)
            self.patch(self.ui, "processingStarted", fake_show_overlay_signal)
            self.patch(self.ui, "processingFinished", fake_hide_overlay_signal)

    def _show_overlay_slot(self):
        """Fake show overlay slot."""
        self._overlay_show_counter += 1

    def _hide_overlay_slot(self):
        """Fake hide overlay slot."""
        self._overlay_hide_counter += 1

    def assert_signal_emitted(self, signal, signal_args,
                              trigger, *args, **kwargs):
        """Check that 'trigger(*args, **kwargs)' emits 'signal(*signal_args)'.

        Also check that the _overlay_hide_counter was increased by one, and
        that the ui is enabled.

        """
        super(PageBaseTestCase, self).assert_signal_emitted(signal,
            signal_args, trigger, *args, **kwargs)
        self.assertEqual(self._overlay_hide_counter, 1)
        self.assertTrue(self.ui.isEnabled())

    # pylint: disable=W0221

    def assert_title_correct(self, expected):
        """Check that the title is equal to 'expected'."""
        check = super(PageBaseTestCase, self).assert_title_correct
        check(self.ui.header.title_label, expected,
              self.ui.header.max_title_width)

    def assert_subtitle_correct(self, expected):
        """Check that the subtitle is equal to 'expected'."""
        check = super(PageBaseTestCase, self).assert_subtitle_correct
        check(self.ui.header.subtitle_label, expected,
              self.ui.header.max_subtitle_width)

    # pylint: enable=W0221
