#!/usr/bin/python3

# This program 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.  See http://www.gnu.org/copyleft/lgpl.html for the full
# text of the license.
#
# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under.


from enum import IntEnum
import os
import time
import unittest
import dbus
import dbusmock
import ddt


__author__ = 'Philip Withnall'
__email__ = 'withnall@endlessm.com'
__copyright__ = '© 2019 Endless Mobile Inc.'
__license__ = 'LGPL 2.1+'


class UpdaterState(IntEnum):
    '''eos-updater states; see its State property'''
    NONE = 0
    READY = 1
    ERROR = 2
    POLLING = 3
    UPDATE_AVAILABLE = 4
    FETCHING = 5
    UPDATE_READY = 6
    APPLYING_UPDATE = 7
    UPDATE_APPLIED = 8


@ddt.ddt
class ManualTest(dbusmock.DBusTestCase):
    '''A manual test of the eos-updater plugin in gnome-software.

    It creates a mock eos-updater D-Bus daemon, on the real system bus (because
    otherwise gnome-software’s other plugins can’t communicate with their
    system daemons; to fix this, we’d need to mock those up too). The test
    harness provides the user with instructions about how to run gnome-software
    and what to do in it, waiting for them to press enter between steps.

    FIXME: This test could potentially eventually be automated by doing the UI
    steps using Dogtail or OpenQA.

    It tests various classes of interaction between the plugin and the daemon:
    normal update process (with and without an update available); error returns
    from the daemon; cancellation of the daemon by another process;
    cancellation of the daemon from gnome-software; and the daemon unexpectedly
    going away (i.e. crashing).
    '''

    @classmethod
    def setUpClass(cls):
        # FIXME: See the comment below about why we currently run on the actual
        # system bus.
        # cls.start_system_bus()
        cls.dbus_con = cls.get_dbus(True)

    def setUp(self):
        # Work out the path to the dbusmock template in the same directory as
        # this file.
        self_path = os.path.dirname(os.path.realpath(__file__))
        template_path = os.path.join(self_path, 'eos_updater.py')

        # Spawn a python-dbusmock server. Use the actual system bus, since
        # gnome-software needs to access various other services (such as
        # packagekit) which we don’t currently mock (FIXME).
        (self.p_mock, self.obj_eos_updater) = self.spawn_server_template(
            template_path, {}, stdout=None)
        self.dbusmock = dbus.Interface(self.obj_eos_updater,
                                       dbusmock.MOCK_IFACE)

    def tearDown(self):
        self.kill_gnome_software()
        self.p_mock.terminate()
        self.p_mock.wait()

    def launch_gnome_software(self):
        '''Instruct the user to launch gnome-software'''
        print('Launch gnome-software with:')
        print('gnome-software --verbose')
        self.manual_check('Press enter to continue')

    def kill_gnome_software(self):
        '''Instruct the user to kill gnome-software'''
        print('Kill gnome-software with:')
        print('pkill gnome-software')
        self.manual_check('Press enter to continue')

    def await_state(self, state):
        '''Block until eos-updater reaches the given `state`'''
        print('Awaiting state %u' % state)
        props_iface = dbus.Interface(self.obj_eos_updater,
                                     dbus.PROPERTIES_IFACE)
        while props_iface.Get('com.endlessm.Updater', 'State') != state:
            time.sleep(0.2)

    def manual_check(self, prompt):
        '''Instruct the user to do a manual check and block until done'''
        input('\033[92;1m' + prompt + '\033[0m\n')

    def test_poll_no_update(self):
        '''Test that no updates are shown if eos-updater successfully says
        there are none.'''
        self.dbusmock.SetPollAction(
            'no-update', dbus.Dictionary({}, signature='sv'), '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check there are no EOS updates listed')
        self.await_state(UpdaterState.READY)

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.LiveBoot',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.NotOstreeSystem',
              'com.endlessm.Updater.Error.Cancelled')
    def test_poll_early_error(self, error_name):
        '''Test that a D-Bus error return from Poll() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'early-error', dbus.Dictionary({}, signature='sv'),
            error_name, 'Some error message.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.ERROR)

        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check there are no EOS updates listed, and a '
                              'GsPluginEosUpdater error is printed on the '
                              'terminal')
        else:
            self.manual_check('Check there are no EOS updates listed, and no '
                              'GsPluginEosUpdater cancellation error is '
                              'printed on the terminal')

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.LiveBoot',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.NotOstreeSystem',
              'com.endlessm.Updater.Error.Cancelled')
    def test_poll_late_error(self, error_name):
        '''Test that a transition to the Error state after successfully calling
        Poll() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'late-error', dbus.Dictionary({}, signature='sv'),
            error_name, 'Some error message.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check there are no EOS updates listed, and a '
                              'GsPluginEosUpdater error is printed on the '
                              'terminal')
        else:
            self.manual_check('Check there are no EOS updates listed, and no '
                              'GsPluginEosUpdater cancellation error is '
                              'printed on the terminal')
        self.await_state(UpdaterState.ERROR)

    @ddt.data(True, False)
    def test_update_available(self, manually_refresh):
        '''Test that the entire update process works if an update is
        available.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')
        self.dbusmock.SetApplyAction('success', '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        if manually_refresh:
            self.manual_check('Check an EOS update is listed; press the '
                              'Refresh button')

        # TODO: if you proceed through the test slowly, this sometimes doesn’t
        # work
        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.manual_check('Check the download has paused at ~75% complete '
                          '(waiting to apply)')
        self.await_state(UpdaterState.APPLYING_UPDATE)
        self.dbusmock.FinishApply()

        self.manual_check('Check the banner says to ‘Restart Now’ (don’t '
                          'click it)')
        self.await_state(UpdaterState.UPDATE_APPLIED)

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.Fetching',
              'com.endlessm.Updater.Error.MalformedAutoinstallSpec',
              'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec',
              'com.endlessm.Updater.Error.FlatpakRemoteConflict',
              'com.endlessm.Updater.Error.MeteredConnection',
              'com.endlessm.Updater.Error.Cancelled')
    def test_fetch_early_error(self, error_name):
        '''Test that a D-Bus error return from Fetch() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('early-error', error_name,
                                     'Some error or other.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')

        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check a fetch error is displayed')
        else:
            self.manual_check('Check no cancellation error is displayed')

        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()
        self.manual_check('Check an EOS update is listed again')

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.Fetching',
              'com.endlessm.Updater.Error.MalformedAutoinstallSpec',
              'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec',
              'com.endlessm.Updater.Error.FlatpakRemoteConflict',
              'com.endlessm.Updater.Error.MeteredConnection',
              'com.endlessm.Updater.Error.Cancelled')
    def test_fetch_late_error(self, error_name):
        '''Test that a transition to the Error state after successfully calling
        Fetch() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('late-error', error_name,
                                     'Some error or other.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.await_state(UpdaterState.ERROR)
        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check a fetch error is displayed')
        else:
            self.manual_check('Check no cancellation error is displayed')

        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()
        self.manual_check('Check an EOS update is listed again')

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.Cancelled')
    def test_apply_early_error(self, error_name):
        '''Test that a D-Bus error return from Apply() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')
        self.dbusmock.SetApplyAction('early-error', error_name,
                                     'Some error or other.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.await_state(UpdaterState.ERROR)
        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check an apply error is displayed after the '
                              'update reached ~75% completion')
        else:
            self.manual_check('Check no cancellation error is displayed after '
                              'the update reached ~75% completion')

        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()
        self.manual_check('Check an EOS update is listed again')

    @ddt.data('com.endlessm.Updater.Error.WrongState',
              'com.endlessm.Updater.Error.WrongConfiguration',
              'com.endlessm.Updater.Error.Cancelled')
    def test_apply_late_error(self, error_name):
        '''Test that a transition to the Error state after successfully calling
        Apply() is handled correctly.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')
        self.dbusmock.SetApplyAction('late-error', error_name,
                                     'Some error or other.')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.manual_check('Check the download has paused at ~75% complete '
                          '(waiting to apply)')
        self.await_state(UpdaterState.APPLYING_UPDATE)
        self.dbusmock.FinishApply()

        self.await_state(UpdaterState.ERROR)
        if error_name != 'com.endlessm.Updater.Error.Cancelled':
            self.manual_check('Check an apply error is displayed')
        else:
            self.manual_check('Check no cancellation error is displayed')

        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()
        self.manual_check('Check an EOS update is listed again')

    def test_no_eos_updater_running(self):
        '''Test that the plugin doesn’t make a fuss if eos-updater is
        unavailable.'''
        self.p_mock.kill()

        self.launch_gnome_software()

        self.manual_check('Check there are no EOS updates listed, and no '
                          'errors shown')

    def test_fetch_ui_cancellation(self):
        '''Test that cancelling a download from the UI works correctly.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button, then shortly afterwards press the Cancel '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.await_state(UpdaterState.ERROR)
        self.manual_check('Check a fetch cancellation error is displayed')

    def test_poll_eos_updater_dies(self):
        '''Test that gnome-software recovers if eos-updater dies while
        polling for updates.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.p_mock.kill()

        self.manual_check('Check no error is shown for the poll failure')
        self.setUp()
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')

        self.manual_check('Press the Refresh button and check an update is '
                          'shown')
        # TODO: It may take a few minutes for the update to appear on the
        # updates page
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

    def test_fetch_eos_updater_dies(self):
        '''Test that gnome-software recovers if eos-updater dies while
        fetching an update.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.p_mock.kill()

        self.manual_check('Check an error is shown for the fetch failure')

    def test_apply_eos_updater_dies(self):
        '''Test that gnome-software recovers if eos-updater dies while
        applying an update.'''
        self.dbusmock.SetPollAction(
            'update', dbus.Dictionary({}, signature='sv'), '', '')
        self.dbusmock.SetFetchAction('success', '', '')
        self.dbusmock.SetApplyAction('success', '', '')

        self.launch_gnome_software()
        self.await_state(UpdaterState.POLLING)
        self.dbusmock.FinishPoll()

        self.manual_check('Check an EOS update is listed; press the Download '
                          'button')
        self.await_state(UpdaterState.FETCHING)
        self.dbusmock.FinishFetch()

        self.manual_check('Check the download has paused at ~75% complete '
                          '(waiting to apply)')
        self.await_state(UpdaterState.APPLYING_UPDATE)
        self.p_mock.kill()

        self.manual_check('Check an error is shown for the apply failure')


if __name__ == '__main__':
    unittest.main()
