# coding: UTF-8
'''GTK Apport user interface tests.'''

# Copyright (C) 2012 Canonical Ltd.
# Author: Evan Dandrea <evan.dandrea@canonical.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

import unittest
import tempfile
import sys
import os
import imp
import apport
import shutil
import subprocess
from gi.repository import GLib, Gtk
from apport import unicode_gettext as _
from mock import patch

import apport.crashdb_impl.memory

GLib.log_set_always_fatal(GLib.LogLevelFlags.LEVEL_WARNING | GLib.LogLevelFlags.LEVEL_CRITICAL)

if os.environ.get('APPORT_TEST_LOCAL'):
    apport_gtk_path = 'gtk/apport-gtk'
    kernel_oops_path = 'data/kernel_oops'
else:
    apport_gtk_path = os.path.join(os.environ.get('APPORT_DATA_DIR', '/usr/share/apport'), 'apport-gtk')
    kernel_oops_path = os.path.join(os.environ.get('APPORT_DATA_DIR', '/usr/share/apport'), 'kernel_oops')
GTKUserInterface = imp.load_source('', apport_gtk_path).GTKUserInterface


class T(unittest.TestCase):
    @classmethod
    def setUpClass(klass):
        r = apport.Report()
        r.add_os_info()
        klass.distro = r['DistroRelease'].split()[0]

    def setUp(self):
        self.report_dir = tempfile.mkdtemp()
        apport.fileutils.report_dir = self.report_dir
        os.environ['APPORT_REPORT_DIR'] = self.report_dir
        # do not cause eternal hangs because of error dialog boxes
        os.environ['APPORT_DISABLE_DISTRO_CHECK'] = '1'

        saved = sys.argv[0]
        # Work around GTKUserInterface using basename to find the GtkBuilder UI
        # file.
        sys.argv[0] = apport_gtk_path
        self.app = GTKUserInterface()
        sys.argv[0] = saved

        # use in-memory crashdb
        self.app.crashdb = apport.crashdb_impl.memory.CrashDatabase(None, {})

        # test report
        self.app.report_file = os.path.join(self.report_dir, 'bash.crash')

        self.app.report = apport.Report()
        self.app.report['ExecutablePath'] = '/bin/bash'
        self.app.report['Signal'] = '11'
        self.app.report['CoreDump'] = b'\x01\x02'
        self.app.report['DistroRelease'] = self.distro
        with open(self.app.report_file, 'wb') as f:
            self.app.report.write(f)

        # disable package hooks, as they might ask for sudo password and other
        # interactive bits; allow tests to install their own hooks
        self.hook_dir = tempfile.mkdtemp()
        apport.report._hook_dir = self.hook_dir
        apport.report._common_hook_dir = self.hook_dir

    def tearDown(self):
        shutil.rmtree(self.report_dir)
        shutil.rmtree(self.hook_dir)

    def test_close_button(self):
        '''Clicking the close button on the window does not report the crash.'''

        def c(*args):
            self.app.w('dialog_crash_new').destroy()
        self.app.w('send_error_report').set_active(True)
        GLib.idle_add(c)
        result = self.app.ui_present_report_details(True)
        self.assertFalse(result['report'])

    def test_kernel_crash_layout(self):
        '''
        +-----------------------------------------------------------------+
        | [ logo] YourDistro has experienced an internal error.           |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |                                                                 |
        | [ Show Details ]                                   [ Continue ] |
        +-----------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'KernelCrash'
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Sorry, %s has experienced an internal error.') % self.distro)
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
        self.assertFalse(self.app.w('ignore_future_problems').get_property('visible'))

    def test_package_crash_layout(self):
        '''
        +-----------------------------------------------------------------+
        | [ error  ] Sorry, a problem occurred while installing software. |
        |            Package: apport 1.2.3~0ubuntu1                       |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |                                                                 |
        | [ Show Details ]                                   [ Continue ] |
        +-----------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'Package'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Sorry, a problem occurred while installing software.'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertTrue(self.app.w('subtitle_label').get_property('visible'))
        self.assertEqual(self.app.w('subtitle_label').get_text(),
                         _('Package: apport 1.2.3~0ubuntu1'))

    def test_regular_crash_thread_layout(self):
        '''A thread of execution has failed, but the application persists.'''
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['ProcStatus'] = 'Name:\tupstart\nPid:\t1'
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(), _('Continue'))

    def test_regular_crash_layout(self):
        '''
        +-----------------------------------------------------------------+
        | [ apport ] The application Apport has closed unexpectedly.      |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |            [ ] Ignore future problems of this program version.  |
        |                                                                 |
        | [ Show Details ]                                   [ Continue ] |
        +-----------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['CrashCounter'] = '1'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(b'''[Desktop Entry]
Version=1.0
Name=Apport
Type=Application''')
            fp.flush()
            self.app.report['DesktopFile'] = fp.name
            GLib.idle_add(Gtk.main_quit)
            self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('The application Apport has closed unexpectedly.'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        # no ProcCmdline, cannot restart
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_label().endswith(
            'of this program version'))

    def test_regular_crash_layout_restart(self):
        '''
        +-----------------------------------------------------------------+
        | [ apport ] The application Apport has closed unexpectedly.      |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |            [ ] Ignore future problems of this program version.  |
        |                                                                 |
        | [ Show Details ]                 [ Leave Closed ]  [ Relaunch ] |
        +-----------------------------------------------------------------+
        '''
        # pretend we got called through run_crashes() which sets offer_restart
        self.app.offer_restart = True
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['CrashCounter'] = '1'
        self.app.report['ProcCmdline'] = 'apport-bug apport'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(b'''[Desktop Entry]
Version=1.0
Name=Apport
Type=Application''')
            fp.flush()
            self.app.report['DesktopFile'] = fp.name
            GLib.idle_add(Gtk.main_quit)
            self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('The application Apport has closed unexpectedly.'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Relaunch'))
        self.assertTrue(self.app.w('closed_button').get_property('visible'))
        self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_label().endswith(
            'of this program version'))

    def test_regular_crash_layout_norestart(self):
        '''
        +-----------------------------------------------------------------+
        | [ apport ] The application Apport has closed unexpectedly.      |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |            [ ] Ignore future problems of this program version.  |
        |                                                                 |
        | [ Show Details ]                                   [ Continue ] |
        +-----------------------------------------------------------------+
        '''
        # pretend we did not get called through run_crashes(), thus no offer_restart
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['CrashCounter'] = '1'
        self.app.report['ProcCmdline'] = 'apport-bug apport'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(b'''[Desktop Entry]
Version=1.0
Name=Apport
Type=Application''')
            fp.flush()
            self.app.report['DesktopFile'] = fp.name
            GLib.idle_add(Gtk.main_quit)
            self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('The application Apport has closed unexpectedly.'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))

    def test_hang_layout(self):
        '''
        +-----------------------------------------------------------------+
        | [ apport ] The application Apport has stopped responding.       |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |                                                                 |
        | [ Show Details ]                 [ Force Closed ]  [ Relaunch ] |
        +-----------------------------------------------------------------+
        '''
        # pretend we got called through run_crashes() which sets offer_restart
        self.app.offer_restart = True
        self.app.report['ProblemType'] = 'Hang'
        self.app.report['ProcCmdline'] = 'apport-bug apport'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(b'''[Desktop Entry]
Version=1.0
Name=Apport
Type=Application''')
            fp.flush()
            self.app.report['DesktopFile'] = fp.name
            GLib.idle_add(Gtk.main_quit)
            self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('The application Apport has stopped responding.'))
        self.assertEqual(self.app.w('subtitle_label').get_text(),
                         _('You can wait to see if it wakes up, or close or '
                           'relaunch it.'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Relaunch'))
        self.assertTrue(self.app.w('closed_button').get_property('visible'))
        self.assertEqual(self.app.w('closed_button').get_label(),
                         _('Force Closed'))
        self.assertTrue(self.app.w('subtitle_label').get_property('visible'))
        self.assertFalse(self.app.w('ignore_future_problems').get_property('visible'))

    def test_system_crash_layout(self):
        '''
        +---------------------------------------------------------------+
        | [ logo ] Sorry, YourDistro has experienced an internal error. |
        |          If you notice further problems, try restarting the   |
        |          computer                                             |
        |                                                               |
        |            [x] Send an error report to help fix this problem. |
        |            [ ] Ignore future problems of this type.           |
        |                                                               |
        | [ Show Details ]                                 [ Continue ] |
        +---------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['CrashCounter'] = '1'
        self.app.report['Package'] = 'bash 5'
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Sorry, %s has experienced an internal error.') % self.distro)
        self.assertEqual(self.app.w('subtitle_label').get_text(),
                         _('If you notice further problems, try restarting the computer.'))
        self.assertTrue(self.app.w('subtitle_label').get_property('visible'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_property('visible'))
        self.assertTrue(self.app.w('ignore_future_problems').get_label().endswith(
            'of this type'))

    def test_system_crash_from_console_layout(self):
        '''
        +-------------------------------------------------------------------+
        | [ ubuntu ] Sorry, the application apport has closed unexpectedly. |
        |            If you notice further problems, try restarting the     |
        |            computer                                               |
        |                                                                   |
        |            [x] Send an error report to help fix this problem.     |
        |                                                                   |
        | [ Show Details ]                                     [ Continue ] |
        +-------------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['Package'] = 'bash 5'
        self.app.report['ProcEnviron'] = ('LANGUAGE=en_GB:en\n'
                                          'SHELL=/bin/sh\n'
                                          'TERM=xterm')
        self.app.report['ExecutablePath'] = '/usr/bin/apport'
        # This will be set by apport/ui.py in load_report()
        self.app.cur_package = 'apport'
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Sorry, the application apport has stopped unexpectedly.'))
        self.assertEqual(self.app.w('subtitle_label').get_text(),
                         _('If you notice further problems, try restarting the computer.'))
        self.assertTrue(self.app.w('subtitle_label').get_property('visible'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))

        del self.app.report['ExecutablePath']
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Sorry, apport has closed unexpectedly.'))

        # no crash counter
        self.assertFalse(self.app.w('ignore_future_problems').get_property('visible'))

    @patch.object(GTKUserInterface, 'can_examine_locally')
    def test_examine_button(self, *args):
        '''
        +---------------------------------------------------------------------+
        | [ apport ] The application Apport has closed unexpectedly.          |
        |                                                                     |
        |            [x] Send an error report to help fix this problem.       |
        |                                                                     |
        | [ Show Details ] [ Examine locally ]  [ Leave Closed ] [ Relaunch ] |
        +---------------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['Package'] = 'bash 5'

        GLib.idle_add(Gtk.main_quit)
        self.app.can_examine_locally.return_value = False
        self.app.ui_present_report_details(True)
        self.assertFalse(self.app.w('examine').get_property('visible'))

        GLib.idle_add(self.app.w('examine').clicked)
        self.app.can_examine_locally.return_value = True
        result = self.app.ui_present_report_details(True)
        self.assertTrue(self.app.w('examine').get_property('visible'))
        self.assertTrue(result['examine'])

    def test_apport_bug_package_layout(self):
        '''
        +-------------------------------------------------------------------+
        | [ error  ] Send problem report to the developers?                 |
        |                                                                   |
        |            +----------------------------------------------------+ |
        |            | |> ApportVersion                                   | |
        |            | ...                                                | |
        |            +----------------------------------------------------+ |
        |                                                                   |
        | [ Cancel ]                                               [ Send ] |
        +-------------------------------------------------------------------+
        '''
        self.app.report_file = None
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Send problem report to the developers?'))
        self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
        send_error_report = self.app.w('send_error_report')
        self.assertFalse(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertFalse(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Send'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertTrue(self.app.w('cancel_button').get_property('visible'))
        self.assertTrue(self.app.w('details_scrolledwindow').get_property('visible'))
        self.assertTrue(self.app.w('dialog_crash_new').get_resizable())

    def test_apport_bug_package_layout_load_file(self):
        '''bug layout from a loaded report'''

        self.app.report_file = '/tmp/foo.apport'
        self.app.report = apport.Report('Bug')
        self.app.report['Package'] = 'libfoo1'
        self.app.report['SourcePackage'] = 'foo'

        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('title_label').get_text(),
                         _('Send problem report to the developers?'))
        self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
        send_error_report = self.app.w('send_error_report')
        self.assertFalse(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertFalse(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Send'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))
        self.assertTrue(self.app.w('cancel_button').get_property('visible'))
        self.assertTrue(self.app.w('details_scrolledwindow').get_property('visible'))
        self.assertTrue(self.app.w('dialog_crash_new').get_resizable())

    def test_recoverable_crash_layout(self):
        '''
        +-----------------------------------------------------------------+
        | [ logo ] The application Foo has experienced an internal error. |
        |          Developer-specified error text.                        |
        |                                                                 |
        |            [x] Send an error report to help fix this problem.   |
        |                                                                 |
        | [ Show Details ]                                   [ Continue ] |
        +-----------------------------------------------------------------+
        '''
        self.app.report['ProblemType'] = 'RecoverableProblem'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        self.app.report['DialogBody'] = 'Some developer-specified error text.'
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(b'''[Desktop Entry]
Version=1.0
Name=Apport
Type=Application''')
            fp.flush()
            self.app.report['DesktopFile'] = fp.name
            GLib.idle_add(Gtk.main_quit)
            self.app.ui_present_report_details(True)
        self.assertEqual(self.app.w('dialog_crash_new').get_title(),
                         self.distro)
        msg = 'The application Apport has experienced an internal error.'
        self.assertEqual(self.app.w('title_label').get_text(), msg)
        msg = 'Some developer-specified error text.'
        self.assertEqual(self.app.w('subtitle_label').get_text(), msg)
        self.assertTrue(self.app.w('subtitle_label').get_property('visible'))
        send_error_report = self.app.w('send_error_report')
        self.assertTrue(send_error_report.get_property('visible'))
        self.assertTrue(send_error_report.get_active())
        self.assertTrue(self.app.w('show_details').get_property('visible'))
        self.assertTrue(self.app.w('continue_button').get_property('visible'))
        self.assertEqual(self.app.w('continue_button').get_label(),
                         _('Continue'))
        self.assertFalse(self.app.w('closed_button').get_property('visible'))

    def test_administrator_disabled_reporting(self):
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(False)
        send_error_report = self.app.w('send_error_report')
        self.assertFalse(send_error_report.get_property('visible'))
        self.assertFalse(send_error_report.get_active())

    @patch.object(GTKUserInterface, 'open_url')
    @patch.object(GTKUserInterface, 'ui_start_upload_progress')
    @patch.object(GTKUserInterface, 'ui_stop_upload_progress')
    def test_crash_nodetails(self, *args):
        '''Crash report without showing details'''

        self.visible_progress = None

        def cont(*args):
            if not self.app.w('continue_button').get_visible():
                return True
            self.app.w('continue_button').clicked()
            GLib.timeout_add(200, check_progress)
            return False

        def check_progress(*args):
            self.visible_progress = self.app.w(
                'window_information_collection').get_property('visible')
            return False

        GLib.timeout_add_seconds(1, cont)
        self.app.run_crash(self.app.report_file)

        # we should have reported one crash
        self.assertEqual(self.app.crashdb.latest_id(), 0)
        r = self.app.crashdb.download(0)
        self.assertEqual(r['ProblemType'], 'Crash')
        self.assertEqual(r['ExecutablePath'], '/bin/bash')

        # should show a progress bar for info collection
        self.assertEqual(self.visible_progress, True)

        # data was collected
        self.assertTrue(r['Package'].startswith('bash '))
        self.assertTrue('libc' in r['Dependencies'])
        self.assertTrue('Stacktrace' in r)

        # upload dialog shown
        self.assertEqual(self.app.ui_start_upload_progress.call_count, 1)
        self.assertEqual(self.app.ui_stop_upload_progress.call_count, 1)

        # URL was opened
        self.assertEqual(self.app.open_url.call_count, 1)

    @patch.object(GTKUserInterface, 'open_url')
    @patch.object(GTKUserInterface, 'ui_start_upload_progress')
    @patch.object(GTKUserInterface, 'ui_stop_upload_progress')
    def test_crash_details(self, *args):
        '''Crash report with showing details'''

        self.visible_progress = None

        def show_details(*args):
            if not self.app.w('show_details').get_visible():
                return True
            self.app.w('show_details').clicked()
            GLib.timeout_add(200, cont)
            return False

        def cont(*args):
            # wait until data collection is done and tree filled
            if self.app.tree_model.get_iter_first() is None:
                return True

            self.assertTrue(self.app.w('continue_button').get_visible())
            self.app.w('continue_button').clicked()
            GLib.timeout_add(200, check_progress)
            return False

        def check_progress(*args):
            self.visible_progress = self.app.w(
                'window_information_collection').get_property('visible')
            return False

        GLib.timeout_add(200, show_details)
        self.app.run_crash(self.app.report_file)

        # we should have reported one crash
        self.assertEqual(self.app.crashdb.latest_id(), 0)
        r = self.app.crashdb.download(0)
        self.assertEqual(r['ProblemType'], 'Crash')
        self.assertEqual(r['ExecutablePath'], '/bin/bash')

        # we already collected details, do not show the progress dialog again
        self.assertNotEqual(self.visible_progress, True)

        # data was collected
        self.assertTrue(r['Package'].startswith('bash '))
        self.assertTrue('libc' in r['Dependencies'])
        self.assertTrue('Stacktrace' in r)

        # upload dialog shown
        self.assertEqual(self.app.ui_start_upload_progress.call_count, 1)
        self.assertEqual(self.app.ui_stop_upload_progress.call_count, 1)

        # URL was opened
        self.assertEqual(self.app.open_url.call_count, 1)

    @patch.object(GTKUserInterface, 'open_url')
    @patch.object(GTKUserInterface, 'ui_start_upload_progress')
    @patch.object(GTKUserInterface, 'ui_stop_upload_progress')
    def test_broken_crash_details(self, *args):
        '''Broken crash report with showing details'''

        self.error_title = None
        self.error_text = None

        def show_details(*args):
            if not self.app.w('show_details').get_visible():
                return True
            self.app.w('show_details').clicked()
            GLib.timeout_add(200, cont)
            return False

        def cont(*args):
            # wait until data collection is done and tree filled
            if self.app.tree_model.get_iter_first() is None:
                return True

            self.assertTrue(self.app.w('continue_button').get_visible())
            self.app.w('continue_button').clicked()
            GLib.timeout_add(100, ack_error)
            return False

        def ack_error(*args):
            # wait until error dialog gets visible
            if not self.app.md:
                return True
            self.error_title = self.app.md.get_title()
            self.error_text = self.app.md.get_property('text')
            self.app.md.response(0)
            return False

        # damage core dump in report file
        with open(self.app.report_file) as f:
            lines = f.readlines()
        lines[-1] = ' iiiiiiiiiiiiAAAA\n'
        with open(self.app.report_file, 'w') as f:
            f.write(''.join(lines))
        self.app.report = None
        GLib.timeout_add(200, show_details)
        self.app.run_crash(self.app.report_file)

        # upload dialog not shown
        self.assertEqual(self.app.ui_start_upload_progress.call_count, 0)
        self.assertEqual(self.app.ui_stop_upload_progress.call_count, 0)

        # no URL was opened
        self.assertEqual(self.app.open_url.call_count, 0)

        # no crash uploaded
        self.assertEqual(self.app.crashdb.latest_id(), -1)

        # proper error message
        self.assertNotEqual(self.error_title, None)
        self.assertIn('cannot be reported', self.error_text)
        self.assertIn('decompressing', self.error_text)

    @patch.object(GTKUserInterface, 'open_url')
    def test_crash_noaccept(self, *args):
        '''Crash report with non-accepting crash DB'''

        self.visible_progress = None

        def cont(*args):
            if not self.app.w('continue_button').get_visible():
                return True
            self.app.w('continue_button').clicked()
            GLib.timeout_add(200, check_progress)
            return False

        def check_progress(*args):
            self.visible_progress = self.app.w(
                'window_information_collection').get_property('visible')
            return False

        GLib.timeout_add_seconds(1, cont)
        self.app.crashdb.options['problem_types'] = ['bug']
        self.app.run_crash(self.app.report_file)

        # we should not have reported the crash
        self.assertEqual(self.app.crashdb.latest_id(), -1)
        self.assertEqual(self.app.open_url.call_count, 0)

        # no progress dialog for non-accepting DB
        self.assertNotEqual(self.visible_progress, True)

        # data was collected for whoopsie
        r = self.app.report
        self.assertEqual(r['ProblemType'], 'Crash')
        self.assertEqual(r['ExecutablePath'], '/bin/bash')
        self.assertTrue(r['Package'].startswith('bash '))
        self.assertTrue('libc' in r['Dependencies'])
        self.assertTrue('Stacktrace' in r)

    @patch.object(GTKUserInterface, 'open_url')
    def test_kerneloops_nodetails(self, *args):
        '''Kernel oops report without showing details'''

        def cont(*args):
            if not self.app.w('continue_button').get_visible():
                return True
            self.app.w('continue_button').clicked()
            return False

        # remove the crash from setUp() and create a kernel oops
        os.remove(self.app.report_file)
        kernel_oops = subprocess.Popen([kernel_oops_path], stdin=subprocess.PIPE)
        kernel_oops.communicate(b'Plasma conduit phase misalignment')
        self.assertEqual(kernel_oops.returncode, 0)

        GLib.timeout_add_seconds(1, cont)
        self.app.run_crashes()

        # we should have reported one crash
        self.assertEqual(self.app.crashdb.latest_id(), 0)
        r = self.app.crashdb.download(0)
        self.assertEqual(r['ProblemType'], 'KernelOops')
        self.assertEqual(r['OopsText'], 'Plasma conduit phase misalignment')

        # data was collected
        self.assertTrue('linux' in r['Package'])
        self.assertTrue('Dependencies' in r)
        self.assertTrue('Plasma conduit' in r['Title'])

        # URL was opened
        self.assertEqual(self.app.open_url.call_count, 1)

    def test_bug_report_installed_package(self):
        '''Bug report for installed package'''

        def c(*args):
            if not self.app.w('cancel_button').get_visible():
                return True
            self.app.w('cancel_button').clicked()
            return False

        self.app.report_file = None
        self.app.options.package = 'bash'
        GLib.timeout_add_seconds(1, c)
        self.app.run_report_bug()

        self.assertEqual(self.app.report['ProblemType'], 'Bug')
        self.assertEqual(self.app.report['SourcePackage'], 'bash')
        self.assertTrue(self.app.report['Package'].startswith('bash '))
        self.assertNotEqual(self.app.report['Dependencies'], '')

    def test_bug_report_uninstalled_package(self):
        '''Bug report for uninstalled package'''

        def c(*args):
            if not self.app.w('cancel_button').get_visible():
                return True
            self.app.w('cancel_button').clicked()
            return False

        pkg = apport.packaging.get_uninstalled_package()
        self.app.report_file = None
        self.app.options.package = pkg
        GLib.timeout_add_seconds(1, c)
        self.app.run_report_bug()

        self.assertEqual(self.app.report['ProblemType'], 'Bug')
        self.assertEqual(self.app.report['SourcePackage'],
                         apport.packaging.get_source(pkg))
        self.assertEqual(self.app.report['Package'], '%s (not installed)' % pkg)

    @patch.object(GTKUserInterface, 'open_url')
    def test_update_report(self, *args):
        '''Updating an existing report'''

        self.app.report_file = None

        def cont(*args):
            if self.app.tree_model.get_iter_first() is None:
                return True
            self.app.w('continue_button').clicked()
            return False

        # upload empty report
        id = self.app.crashdb.upload({})
        self.assertEqual(id, 0)
        self.app.options.update_report = 0
        self.app.options.package = 'bash'

        GLib.timeout_add(200, cont)
        self.app.run_update_report()

        # no new bug reported
        self.assertEqual(self.app.crashdb.latest_id(), 0)

        # bug was updated
        r = self.app.crashdb.download(0)
        self.assertTrue(r['Package'].startswith('bash '))
        self.assertTrue('libc' in r['Dependencies'])
        self.assertTrue('DistroRelease' in r)

        # No URL in this mode
        self.assertEqual(self.app.open_url.call_count, 0)

    @patch.object(GTKUserInterface, 'open_url')
    def test_update_report_different_binary_source(self, *args):
        '''Updating an existing report on a source package which does not have a binary of the same name'''

        self.app.report_file = None

        def cont(*args):
            if self.app.tree_model.get_iter_first() is None:
                return True
            self.app.w('continue_button').clicked()
            return False

        kernel_pkg = apport.packaging.get_kernel_package()
        kernel_src = apport.packaging.get_source(kernel_pkg)
        self.assertNotEqual(kernel_pkg, kernel_src,
                            'this test assumes that the kernel binary package != kernel source package')
        self.assertNotEqual(apport.packaging.get_version(kernel_pkg), '',
                            'this test assumes that the kernel binary package %s is installed' % kernel_pkg)
        # this test assumes that the kernel source package name is not an
        # installed binary package
        self.assertRaises(ValueError, apport.packaging.get_version, kernel_src)

        # create source package hook, as otherwise there is nothing to collect
        with open(os.path.join(self.hook_dir, 'source_%s.py' % kernel_src), 'w') as f:
            f.write('def add_info(r, ui):\n r["MachineType"]="Laptop"\n')

        # upload empty report
        id = self.app.crashdb.upload({})
        self.assertEqual(id, 0)

        # run in update mode for that bug
        self.app.options.update_report = 0
        self.app.options.package = kernel_src

        GLib.timeout_add(200, cont)
        self.app.run_update_report()

        # no new bug reported
        self.assertEqual(self.app.crashdb.latest_id(), 0)

        # bug was updated
        r = self.app.crashdb.download(0)
        self.assertTrue('ProcEnviron' in r)
        self.assertTrue('DistroRelease' in r)
        self.assertTrue('Uname' in r)
        self.assertEqual(r['MachineType'], 'Laptop')

        # No URL in this mode
        self.assertEqual(self.app.open_url.call_count, 0)

    @patch.object(GTKUserInterface, 'get_desktop_entry')
    def test_missing_icon(self, *args):
        # LP: 937354
        self.app.report['ProblemType'] = 'Crash'
        self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
        self.app.get_desktop_entry.return_value = {'name': 'apport', 'icon': 'nonexistent'}
        GLib.idle_add(Gtk.main_quit)
        self.app.ui_present_report_details(True)

    def test_resizing(self):
        '''Problem report window resizability and sizing.'''

        def show_details(data):
            if not self.app.w('show_details').get_visible():
                return True

            data['orig_size'] = self.app.w('dialog_crash_new').get_size()
            data['orig_resizable'] = self.app.w('dialog_crash_new').get_resizable()
            self.app.w('show_details').clicked()
            GLib.timeout_add(200, hide_details, data)
            return False

        def hide_details(data):
            # wait until data collection is done and tree filled
            if self.app.tree_model.get_iter_first() is None:
                return True

            data['detail_size'] = self.app.w('dialog_crash_new').get_size()
            data['detail_resizable'] = self.app.w('dialog_crash_new').get_resizable()
            self.app.w('show_details').clicked()
            GLib.timeout_add(200, details_hidden, data)
            return False

        def details_hidden(data):
            # wait until data collection is done and tree filled
            if self.app.w('details_scrolledwindow').get_visible():
                return True

            data['hidden_size'] = self.app.w('dialog_crash_new').get_size()
            data['hidden_resizable'] = self.app.w('dialog_crash_new').get_resizable()
            Gtk.main_quit()

        data = {}
        GLib.timeout_add(200, show_details, data)
        self.app.run_crash(self.app.report_file)

        # when showing details, dialog should get considerably bigger
        self.assertGreater(data['detail_size'][1], data['orig_size'][1] + 100)

        # when hiding details, original size should be restored
        self.assertEqual(data['orig_size'], data['hidden_size'])

        # should only be resizable in details mode
        self.assertFalse(data['orig_resizable'])
        self.assertTrue(data['detail_resizable'])
        self.assertFalse(data['hidden_resizable'])

    def test_dialog_nonascii(self):
        '''Non-ASCII title/text in dialogs'''

        def close(response):
            if not self.app.md:
                return True
            self.app.md.response(response)
            return False

        # unicode arguments
        GLib.timeout_add(200, close, 0)
        self.app.ui_info_message(b'title \xe2\x99\xaa'.decode('UTF-8'), b'text \xe2\x99\xaa'.decode('UTF-8'))
        # byte array arguments (in Python 2)
        if sys.version < '3':
            GLib.timeout_add(200, close, 0)
            self.app.ui_info_message(b'title \xe2\x99\xaa', b'text \xe2\x99\xaa')

        # with URLs
        GLib.timeout_add(200, close, 0)
        self.app.ui_info_message('title', b'http://example.com \xe2\x99\xaa'.decode('UTF-8'))
        if sys.version < '3':
            GLib.timeout_add(200, close, 0)
            self.app.ui_info_message('title', b'http://example.com \xe2\x99\xaa')

    def test_immediate_close(self):
        '''Close details window immediately'''

        # this reproduces https://launchpad.net/bugs/938090
        self.app.w('dialog_crash_new').destroy()
        GLib.idle_add(Gtk.main_quit)
        self.app.run_crash(self.app.report_file)

    @patch.object(GTKUserInterface, 'ui_start_upload_progress')
    def test_close_during_collect(self, *args):
        '''Close details window during information collection'''

        def show_details(*args):
            if not self.app.w('show_details').get_visible():
                return True
            self.app.w('show_details').clicked()
            GLib.timeout_add(200, close)
            return False

        def close(*args):
            self.app.w('dialog_crash_new').destroy()
            return False

        GLib.timeout_add(200, show_details)
        self.app.run_crash(self.app.report_file)

        self.assertEqual(self.app.ui_start_upload_progress.call_count, 0)

unittest.main()
