# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import glob
import os
import shutil
import sys

from io import StringIO

from marionette_driver import Wait
from marionette_driver.errors import (
    InvalidSessionIdException,
    NoSuchWindowException,
    TimeoutException,
)

from marionette_harness import MarionetteTestCase, expectedFailure

# Import runner module to monkey patch mozcrash module
from mozrunner.base import runner


class MockMozCrash(object):
    """Mock object to replace original mozcrash methods."""

    def __init__(self, marionette):
        self.marionette = marionette

        with self.marionette.using_context("chrome"):
            self.crash_reporter_enabled = self.marionette.execute_script(
                """
                const { AppConstants } = ChromeUtils.importESModule(
                  "resource://gre/modules/AppConstants.sys.mjs"
                );
                return AppConstants.MOZ_CRASHREPORTER;
            """
            )

    def check_for_crashes(self, dump_directory, *args, **kwargs):
        if self.crash_reporter_enabled:
            # Workaround until bug 1376795 has been fixed
            # Wait at maximum 5s for the minidump files being created
            # minidump_files = glob.glob('{}/*.dmp'.format(dump_directory))
            try:
                minidump_files = Wait(None, timeout=5).until(
                    lambda _: glob.glob("{}/*.dmp".format(dump_directory))
                )
            except TimeoutException:
                minidump_files = []

            if os.path.isdir(dump_directory):
                shutil.rmtree(dump_directory)

            return len(minidump_files)
        else:
            return len(minidump_files) == 0

    def log_crashes(self, logger, dump_directory, *args, **kwargs):
        return self.check_for_crashes(dump_directory, *args, **kwargs)


class BaseCrashTestCase(MarionetteTestCase):
    # Reduce the timeout for faster processing of the tests
    socket_timeout = 10

    def setUp(self):
        super(BaseCrashTestCase, self).setUp()

        # Monkey patch mozcrash to avoid crash info output only for our triggered crashes.
        mozcrash_mock = MockMozCrash(self.marionette)
        if not mozcrash_mock.crash_reporter_enabled:
            self.skipTest("Crash reporter disabled")
            return

        self.mozcrash = runner.mozcrash
        runner.mozcrash = mozcrash_mock

        self.crash_count = self.marionette.crashed
        self.pid = self.marionette.process_id

    def tearDown(self):
        # Replace mockup with original mozcrash instance
        runner.mozcrash = self.mozcrash

        self.marionette.crashed = self.crash_count

        super(BaseCrashTestCase, self).tearDown()

    def crash(self, parent=True):
        socket_timeout = self.marionette.client.socket_timeout
        self.marionette.client.socket_timeout = self.socket_timeout

        self.marionette.set_context("content")
        try:
            self.marionette.navigate(
                "about:crash{}".format("parent" if parent else "content")
            )
        finally:
            self.marionette.client.socket_timeout = socket_timeout


class TestCrash(BaseCrashTestCase):
    def setUp(self):
        if os.environ.get("MOZ_AUTOMATION"):
            # Capture stdout, otherwise the Gecko output causes mozharness to fail
            # the task due to "A content process has crashed" appearing in the log.
            # To view stdout for debugging, use `print(self.new_out.getvalue())`
            print(
                "Suppressing GECKO output. To view, add `print(self.new_out.getvalue())` "
                "to the end of this test."
            )
            self.new_out, self.new_err = StringIO(), StringIO()
            self.old_out, self.old_err = sys.stdout, sys.stderr
            sys.stdout, sys.stderr = self.new_out, self.new_err

        super(TestCrash, self).setUp()

    def tearDown(self):
        super(TestCrash, self).tearDown()

        if os.environ.get("MOZ_AUTOMATION"):
            sys.stdout, sys.stderr = self.old_out, self.old_err

    def test_crash_chrome_process(self):
        self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True)

        # A crash results in a non zero exit code
        self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)
        with self.assertRaisesRegex(
            InvalidSessionIdException, "Please start a session"
        ):
            self.marionette.get_url()

        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)

        self.marionette.get_url()

    def test_crash_content_process(self):
        # For a content process crash and MOZ_CRASHREPORTER_SHUTDOWN set the top
        # browsing context will be gone first. As such the raised NoSuchWindowException
        # has to be ignored. To check for the IOError, further commands have to
        # be executed until the process is gone.
        with self.assertRaisesRegex(IOError, "Content process crashed"):
            self.crash(parent=False)
            Wait(
                self.marionette,
                timeout=self.socket_timeout,
                ignored_exceptions=NoSuchWindowException,
            ).until(
                lambda _: self.marionette.get_url(),
                message="Expected IOError exception for content crash not raised.",
            )

        # A crash when loading about:crashcontent results in a SIGUSR1 exit code.
        self.assertEqual(self.marionette.instance.runner.returncode, 245)

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)
        with self.assertRaisesRegex(
            InvalidSessionIdException, "Please start a session"
        ):
            self.marionette.get_url()

        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)
        self.marionette.get_url()

    @expectedFailure
    def test_unexpected_crash(self):
        self.crash(parent=True)


class TestCrashInSetUp(BaseCrashTestCase):
    def setUp(self):
        super(TestCrashInSetUp, self).setUp()

        self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True)

        # A crash results in a non zero exit code
        self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

        self.assertEqual(self.marionette.crashed, 1)
        self.assertIsNone(self.marionette.session)

    def test_crash_in_setup(self):
        self.marionette.start_session()
        self.assertNotEqual(self.marionette.process_id, self.pid)


class TestCrashInTearDown(BaseCrashTestCase):
    def tearDown(self):
        try:
            self.assertRaisesRegex(IOError, "Process crashed", self.crash, parent=True)

            # A crash results in a non zero exit code
            self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0))

            self.assertEqual(self.marionette.crashed, 1)
            self.assertIsNone(self.marionette.session)

        finally:
            super(TestCrashInTearDown, self).tearDown()

    def test_crash_in_teardown(self):
        pass
