# 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 os
import sys

import mozinfo
import yaml
from marionette_driver.errors import JavascriptException, ScriptTimeoutException
from mozproxy import get_playback

AWSY_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if AWSY_PATH not in sys.path:
    sys.path.append(AWSY_PATH)

from awsy import process_perf_data, webservers
from awsy.awsy_test_case import AwsyTestCase


class TestMemoryUsage(AwsyTestCase):
    """Provides a test that collects memory usage at various checkpoints:
    - "Start" - Just after startup
    - "StartSettled" - After an additional wait time
    - "TabsOpen" - After opening all provided URLs
    - "TabsOpenSettled" - After an additional wait time
    - "TabsOpenForceGC" - After forcibly invoking garbage collection
    - "TabsClosed" - After closing all tabs
    - "TabsClosedSettled" - After an additional wait time
    - "TabsClosedForceGC" - After forcibly invoking garbage collection
    """

    def urls(self):
        return self._urls

    def perf_suites(self):
        return process_perf_data.PERF_SUITES

    def perf_checkpoints(self):
        return process_perf_data.CHECKPOINTS

    def perf_extra_opts(self):
        return self._extra_opts

    def setupTp5(self):
        urls = None
        default_tp5n_manifest = os.path.join(
            self._webroot_dir, "page_load_test", "tp5n", "tp5n.manifest"
        )
        tp5n_manifest = self.testvars.get("pageManifest", default_tp5n_manifest)
        with open(tp5n_manifest) as fp:
            urls = fp.readlines()
        # pylint --py3k: W1636
        urls = list(map(lambda x: x.replace("localhost", "localhost:{}"), urls))

        # We haven't set self._urls yet, so this value might be zero if
        # 'entities' wasn't specified.
        to_load = self.pages_to_load()
        if not to_load:
            to_load = len(urls)
        self._webservers = webservers.WebServers(
            "localhost", 8001, self._webroot_dir, to_load
        )
        self._webservers.start()
        for url, server in zip(urls, self._webservers.servers):
            self._urls.append(url.strip().format(server.port))

    def setupTp6(self):
        # tp5n stores its manifest in the zip file that gets extracted, tp6
        # doesn't so we just keep one in our project dir for now.
        default_tp6_pages_manifest = os.path.join(AWSY_PATH, "conf", "tp6-pages.yml")
        tp6_pages_manifest = self.testvars.get(
            "pageManifest", default_tp6_pages_manifest
        )
        urls = []
        with open(tp6_pages_manifest) as f:
            d = yaml.safe_load(f)
            for r in d:
                url = r["url"]
                if isinstance(url, list):
                    urls.extend(url)
                else:
                    urls.append(url)

        self._urls = urls

        # Indicate that we're using tp6 in the perf data.
        self._extra_opts = ["tp6"]

        if self.marionette.get_pref("fission.autostart"):
            self._extra_opts.append("fission")

        # Now we setup the mitm proxy with our tp6 pageset.
        tp6_pageset_manifest = os.path.join(AWSY_PATH, "tp6-pageset.manifest")
        config = {
            "playback_tool": "mitmproxy",
            "playback_version": "5.1.1",
            "playback_files": [tp6_pageset_manifest],
            "platform": mozinfo.os,
            "obj_path": self._webroot_dir,
            "binary": self._binary,
            "run_local": self._run_local,
            "app": "firefox",
            "host": "127.0.0.1",
            "ignore_mitmdump_exit_failure": True,
        }

        self._playback = get_playback(config)
        self._playback.start()

        # We need to reload after the mitmproxy cert is installed
        self.marionette.restart(in_app=False, clean=False)

        # Setup WebDriver capabilities that we need
        self.marionette.delete_session()
        caps = {
            "unhandledPromptBehavior": "dismiss",  # Ignore page navigation warnings
        }
        self.marionette.start_session(caps)
        self.marionette.set_context("chrome")

    def setUp(self):
        AwsyTestCase.setUp(self)
        self.logger.info("setting up")
        self._webroot_dir = self.testvars["webRootDir"]
        self._urls = []
        self._extra_opts = None

        if self.testvars.get("tp5", False):
            self.setupTp5()
        else:
            self.setupTp6()

        self.logger.info(
            "areweslimyet run by %d pages, %d iterations,"
            " %d perTabPause, %d settleWaitTime"
            % (
                self._pages_to_load,
                self._iterations,
                self._perTabPause,
                self._settleWaitTime,
            )
        )
        self.logger.info("done setting up!")

    def tearDown(self):
        self.logger.info("tearing down!")

        self.logger.info("tearing down webservers!")

        if self.testvars.get("tp5", False):
            self._webservers.stop()
        else:
            self._playback.stop()

        AwsyTestCase.tearDown(self)

        self.logger.info("done tearing down!")

    def clear_preloaded_browser(self):
        """
        Clears out the preloaded browser.
        """
        self.logger.info("closing preloaded browser")
        script = """
            if (window.NewTabPagePreloading) {
                return NewTabPagePreloading.removePreloadedBrowser(window);
            }
            return "NewTabPagePreloading.removePreloadedBrowser not available";
            """
        try:
            result = self.marionette.execute_script(script, script_timeout=180000)
        except JavascriptException as e:
            self.logger.error("removePreloadedBrowser() JavaScript error: %s" % e)
        except ScriptTimeoutException:
            self.logger.error("removePreloadedBrowser() timed out")
        except Exception:
            self.logger.error(
                "removePreloadedBrowser() Unexpected error: %s" % sys.exc_info()[0]
            )
        else:
            if result:
                self.logger.info(result)

    def test_open_tabs(self):
        """Marionette test entry that returns an array of checkpoint arrays.

        This will generate a set of checkpoints for each iteration requested.
        Upon successful completion the results will be stored in
        |self.testvars["results"]| and accessible to the test runner via the
        |testvars| object it passed in.
        """
        # setup the results array
        results = [[] for _ in range(self.iterations())]

        def create_checkpoint(name, iteration, minimize=False):
            checkpoint = self.do_memory_report(name, iteration, minimize)
            self.assertIsNotNone(checkpoint, "Checkpoint was recorded")
            results[iteration].append(checkpoint)

        # The first iteration gets Start and StartSettled entries before
        # opening tabs
        create_checkpoint("Start", 0)
        self.settle()
        create_checkpoint("StartSettled", 0)

        for itr in range(self.iterations()):
            self.open_pages()

            create_checkpoint("TabsOpen", itr)
            self.settle()
            create_checkpoint("TabsOpenSettled", itr)
            create_checkpoint("TabsOpenForceGC", itr, minimize=True)

            # Close all tabs
            self.reset_state()

            with self.marionette.using_context("content"):
                self.logger.info("navigating to about:blank")
                self.marionette.navigate("about:blank")
                self.logger.info("navigated to about:blank")
            self.signal_user_active()

            # Create checkpoint that may contain retained processes that will
            # be reused.
            create_checkpoint("TabsClosedExtraProcesses", itr)

            # Clear out the retained processes and measure again.
            self.clear_preloaded_browser()

            create_checkpoint("TabsClosed", itr)
            self.settle()
            create_checkpoint("TabsClosedSettled", itr)
            create_checkpoint("TabsClosedForceGC", itr, minimize=True)

        # TODO(ER): Temporary hack until bug 1121139 lands
        self.logger.info("setting results")
        self.testvars["results"] = results
