from contextlib import contextmanager

from marionette_harness import MarionetteTestCase


class ExperimentStatus:
    UNENROLLED = 0
    ENROLLED_CONTROL = 1
    ENROLLED_TREATMENT = 2
    DISQUALIFIED = 3


class ContentWin32kLockdownState:
    LockdownEnabled = 1
    MissingWebRender = 2
    OperatingSystemNotSupported = 3
    PrefNotSet = 4
    MissingRemoteWebGL = 5
    MissingNonNativeTheming = 6
    DisabledByEnvVar = 7
    DisabledBySafeMode = 8
    DisabledByE10S = 9
    DisabledByUserPref = 10
    EnabledByUserPref = 11
    DisabledByControlGroup = 12
    EnabledByTreatmentGroup = 13
    DisabledByDefault = 14
    EnabledByDefault = 15


class Prefs:
    ENROLLMENT_STATUS = "security.sandbox.content.win32k-experiment.enrollmentStatus"
    STARTUP_ENROLLMENT_STATUS = (
        "security.sandbox.content.win32k-experiment.startupEnrollmentStatus"
    )
    WIN32K = "security.sandbox.content.win32k-disable"
    WEBGL = "webgl.out-of-process"


ENV_DISABLE_WIN32K = "MOZ_ENABLE_WIN32K"
ENV_DISABLE_E10S = "MOZ_FORCE_DISABLE_E10S"


class TestWin32kAutostart(MarionetteTestCase):
    SANDBOX_NAME = "win32k-autostart"

    def execute_script(self, code, *args, **kwargs):
        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
            return self.marionette.execute_script(
                code, new_sandbox=False, sandbox=self.SANDBOX_NAME, *args, **kwargs
            )

    def get_win32k_status(self):
        return self.execute_script(
            r"""
          let win = Services.wm.getMostRecentWindow("navigator:browser");
          let ses = "security.sandbox.content.win32k-experiment.startupEnrollmentStatus";
          return {
            win32kSessionStatus: Services.appinfo.win32kSessionStatus,
            win32kStatus: Services.appinfo.win32kLiveStatusTestingOnly,
            win32kExperimentStatus: Services.appinfo.win32kExperimentStatus,
            win32kPref: Services.prefs.getBoolPref("security.sandbox.content.win32k-disable"),
            win32kStartupEnrollmentStatusPref: Services.prefs.getIntPref(ses),
          };
        """
        )

    def check_win32k_status(
        self, status, sessionStatus, experimentStatus, pref, enrollmentStatusPref
    ):
        # We CANNOT check win32kEnrollmentStatusPref after a restart because we only set this
        # pref on the default branch, and it goes away after a restart, so we only check
        # the startupEnrollmentStatusPref
        expected = {
            "win32kSessionStatus": sessionStatus,
            "win32kStatus": status,
            "win32kExperimentStatus": experimentStatus,
            "win32kPref": pref,
            "win32kStartupEnrollmentStatusPref": enrollmentStatusPref,
        }

        status = self.get_win32k_status()

        for prop, value in expected.items():
            self.assertEqual(
                status[prop],
                value,
                "%s should have the value `%r`, but has `%r`"
                % (prop, value, status[prop]),
            )

    def set_env(self, env, value):
        self.execute_script(
            "env.set(arguments[0], arguments[1]);", script_args=(env, value)
        )

    def get_env(self, env):
        return self.execute_script("return env.get(arguments[0]);", script_args=(env,))

    def set_enrollment_status(self, status):
        self.marionette.set_pref(Prefs.ENROLLMENT_STATUS, status, default_branch=True)

        updated_status = self.marionette.get_pref(Prefs.ENROLLMENT_STATUS)
        self.assertTrue(
            status == updated_status or updated_status == ExperimentStatus.DISQUALIFIED
        )
        startup_status = self.marionette.get_pref(Prefs.STARTUP_ENROLLMENT_STATUS)
        self.assertEqual(
            startup_status,
            updated_status,
            "Startup enrollment status (%r) should match "
            "session status (%r)" % (startup_status, updated_status),
        )

    def restart(self, prefs=None, env=None):
        if prefs:
            self.marionette.set_prefs(prefs)

        if env:
            for name, value in env.items():
                self.set_env(name, value)

        self.marionette.restart(in_app=True, clean=False)
        self.setUpSession()

        # Sanity check our environment.
        if prefs:
            for key, val in prefs.items():
                if val is not None:
                    self.assertEqual(self.marionette.get_pref(key), val)
        if env:
            for key, val in env.items():
                self.assertEqual(self.get_env(key), val or "")

    def setUpSession(self):
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)

        self.execute_script(
            r"""
          // We're running in a function, in a sandbox, that inherits from an
          // X-ray wrapped window. Anything we want to be globally available
          // needs to be defined on that window.
          window.env = Services.env;
        """
        )

    @contextmanager
    def full_restart(self):
        profile = self.marionette.instance.profile
        try:
            self.marionette.quit()
            yield profile
        finally:
            self.marionette.start_session()
            self.setUpSession()

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

        # If we have configured marionette to require a particular value for
        # `win32k.autostart`, remove it as a forced pref until `tearDown`, and
        # perform a clean restart, so we run this test without the pref
        # pre-configured.
        self.win32kRequired = None
        if Prefs.WIN32K in self.marionette.instance.required_prefs:
            self.win32kRequired = self.marionette.instance.required_prefs[Prefs.WIN32K]
            del self.marionette.instance.required_prefs[Prefs.WIN32K]
            self.marionette.restart(in_app=False, clean=True)

        self.setUpSession()

        # Marionette doesn't let you set preferences on the default branch before startup
        # so we can't test the default=False and default=True scenarios in one test
        # What we can do is generate all the tests, and then only run the runs for which
        # the default is.  (And run the other ones locally to make sure they work before
        # we land it.)
        prefJS = 'return Services.prefs.getBoolPref("security.sandbox.content.win32k-disable");'
        self.default_is = self.execute_script(prefJS)

        if self.default_is is False:
            # Win32k status must start out with `disabledByDefault`
            self.check_win32k_status(
                status=ContentWin32kLockdownState.DisabledByDefault,
                sessionStatus=ContentWin32kLockdownState.DisabledByDefault,
                experimentStatus=ExperimentStatus.UNENROLLED,
                pref=False,
                enrollmentStatusPref=ExperimentStatus.UNENROLLED,
            )
        else:
            # Win32k status must start out with `enabledByDefault`
            self.check_win32k_status(
                status=ContentWin32kLockdownState.EnabledByDefault,
                sessionStatus=ContentWin32kLockdownState.EnabledByDefault,
                experimentStatus=ExperimentStatus.UNENROLLED,
                pref=True,
                enrollmentStatusPref=ExperimentStatus.UNENROLLED,
            )

    def tearDown(self):
        if self.win32kRequired is not None:
            self.marionette.instance.required_prefs[Prefs.WIN32K] = self.win32kRequired
        self.marionette.restart(in_app=False, clean=True)

        super(TestWin32kAutostart, self).tearDown()
