import os, shutil
from marionette import MarionetteTestCase


class TestFirefoxRefresh(MarionetteTestCase):
    _username = "marionette-test-login"
    _password = "marionette-test-password"
    _bookmarkURL = "about:mozilla"
    _bookmarkText = "Some bookmark from Marionette"

    _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
    _cookiePath = "some/cookie/path"
    _cookieName = "somecookie"
    _cookieValue = "some cookie value"

    _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
    _historyTitle = "Test visit for Firefox Reset"

    _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
    _formHistoryValue = "special-pumpkin-value"

    _expectedURLs = ["about:robots", "about:mozilla"]

    def savePassword(self):
        self.runCode("""
          let myLogin = new global.LoginInfo(
            "test.marionette.mozilla.com",
            "http://test.marionette.mozilla.com/some/form/",
            null,
            arguments[0],
            arguments[1],
            "username",
            "password"
          );
          Services.logins.addLogin(myLogin)
        """, script_args=[self._username, self._password])

    def createBookmark(self):
        self.marionette.execute_script("""
          let url = arguments[0];
          let title = arguments[1];
          PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
            makeURI(url), 0, title);
        """, script_args=[self._bookmarkURL, self._bookmarkText])

    def createHistory(self):
        error = self.runAsyncCode("""
          // Copied from PlacesTestUtils, which isn't available in Marionette tests.
          let didReturn;
          PlacesUtils.asyncHistory.updatePlaces(
            [{title: arguments[1], uri: makeURI(arguments[0]), visits: [{
                transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
                visitDate: (Date.now() - 5000) * 1000,
                referrerURI: makeURI("about:mozilla"),
              }]
            }],
            {
              handleError(resultCode, place) {
                didReturn = true;
                marionetteScriptFinished("Unexpected error in adding visit: " + resultCode);
              },
              handleResult() {},
              handleCompletion() {
                if (!didReturn) {
                  marionetteScriptFinished(false);
                }
              },
            }
          );
        """, script_args=[self._historyURL, self._historyTitle])
        if error:
            print error

    def createFormHistory(self):
        error = self.runAsyncCode("""
          let updateDefinition = {
            op: "add",
            fieldname: arguments[0],
            value: arguments[1],
            firstUsed: (Date.now() - 5000) * 1000,
          };
          let finished = false;
          global.FormHistory.update(updateDefinition, {
            handleError(error) {
              finished = true;
              marionetteScriptFinished(error);
            },
            handleCompletion() {
              if (!finished) {
                marionetteScriptFinished(false);
              }
            }
          });
        """, script_args=[self._formHistoryFieldName, self._formHistoryValue])
        if error:
          print error

    def createCookie(self):
        self.runCode("""
          // Expire in 15 minutes:
          let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
          Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
                               true, false, false, expireTime);
        """, script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue])

    def createSession(self):
        self.runAsyncCode("""
          const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
                                 Ci.nsIWebProgressListener.STATE_IS_NETWORK;
          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
          let expectedURLs = Array.from(arguments[0])
          gBrowser.addTabsProgressListener({
            onStateChange(browser, webprogress, request, flags, status) {
              try {
                request && request.QueryInterface(Ci.nsIChannel);
              } catch (ex) {}
              let uriLoaded = request.originalURI && request.originalURI.spec;
              if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
                  expectedURLs.includes(uriLoaded)) {
                TabStateFlusher.flush(browser).then(function() {
                  expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
                  if (!expectedURLs.length) {
                    gBrowser.removeTabsProgressListener(this);
                    marionetteScriptFinished();
                  }
                });
              }
            }
          });
          for (let url of expectedURLs) {
            gBrowser.addTab(url);
          }
        """, script_args=[self._expectedURLs])

    def checkPassword(self):
        loginInfo = self.marionette.execute_script("""
          let ary = Services.logins.findLogins({},
            "test.marionette.mozilla.com",
            "http://test.marionette.mozilla.com/some/form/",
            null, {});
          return ary.length ? ary : {username: "null", password: "null"};
        """)
        self.assertEqual(len(loginInfo), 1)
        self.assertEqual(loginInfo[0]['username'], self._username)
        self.assertEqual(loginInfo[0]['password'], self._password)

        loginCount = self.marionette.execute_script("""
          return Services.logins.getAllLogins().length;
        """)
        self.assertEqual(loginCount, 1, "No other logins are present")

    def checkBookmark(self):
        titleInBookmarks = self.marionette.execute_script("""
          let url = arguments[0];
          let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {});
          return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : "";
        """, script_args=[self._bookmarkURL])
        self.assertEqual(titleInBookmarks, self._bookmarkText)

    def checkHistory(self):
        historyResults = self.runAsyncCode("""
          let placeInfos = [];
          PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), {
            handleError(resultCode, place) {
              placeInfos = null;
              marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode);
            },
            handleResult(placeInfo) {
              placeInfos.push(placeInfo);
            },
            handleCompletion() {
              if (placeInfos) {
                if (!placeInfos.length) {
                  marionetteScriptFinished("No visits found");
                } else {
                  marionetteScriptFinished(placeInfos);
                }
              }
            },
          });
        """, script_args=[self._historyURL])
        if type(historyResults) == str:
            self.fail(historyResults)
            return

        historyCount = len(historyResults)
        self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount)
        if historyCount == 1:
            self.assertEqual(historyResults[0]['title'], self._historyTitle)

    def checkFormHistory(self):
        formFieldResults = self.runAsyncCode("""
          let results = [];
          global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
            handleError(error) {
              results = error;
            },
            handleResult(result) {
              results.push(result);
            },
            handleCompletion() {
              marionetteScriptFinished(results);
            },
          });
        """, script_args=[self._formHistoryFieldName])
        if type(formFieldResults) == str:
            self.fail(formFieldResults)
            return

        formFieldResultCount = len(formFieldResults)
        self.assertEqual(formFieldResultCount, 1, "Should have exactly 1 entry for this field, got %d" % formFieldResultCount)
        if formFieldResultCount == 1:
            self.assertEqual(formFieldResults[0]['value'], self._formHistoryValue)

        formHistoryCount = self.runAsyncCode("""
          let count;
          let callbacks = {
            handleResult: rv => count = rv,
            handleCompletion() {
              marionetteScriptFinished(count);
            },
          };
          global.FormHistory.count({}, callbacks);
        """)
        self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history")

    def checkCookie(self):
        cookieInfo = self.runCode("""
          try {
            let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]);
            let cookie = null;
            while (cookieEnum.hasMoreElements()) {
              let hostCookie = cookieEnum.getNext();
              hostCookie.QueryInterface(Ci.nsICookie2);
              // getCookiesFromHost returns any cookie from the BASE host.
              if (hostCookie.rawHost != arguments[0])
                continue;
              if (cookie != null) {
                return "more than 1 cookie! That shouldn't happen!";
              }
              cookie = hostCookie;
            }
            return {path: cookie.path, name: cookie.name, value: cookie.value};
          } catch (ex) {
            return "got exception trying to fetch cookie: " + ex;
          }
        """, script_args=[self._cookieHost])
        if not isinstance(cookieInfo, dict):
            self.fail(cookieInfo)
            return
        self.assertEqual(cookieInfo['path'], self._cookiePath)
        self.assertEqual(cookieInfo['value'], self._cookieValue)
        self.assertEqual(cookieInfo['name'], self._cookieName)

    def checkSession(self):
        tabURIs = self.runCode("""
          return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
        """)
        self.assertSequenceEqual(tabURIs, ["about:welcomeback"])

        tabURIs = self.runAsyncCode("""
          let mm = gBrowser.selectedBrowser.messageManager;
          let fs = function() {
            content.document.getElementById("errorTryAgain").click();
          };
          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
          window.addEventListener("SSWindowStateReady", function testSSPostReset() {
            window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
            Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
              marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
            });
          }, false);
          mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
        """)
        self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs)
        pass

    def checkProfile(self, hasMigrated=False):
        self.checkPassword()
        self.checkBookmark()
        self.checkHistory()
        self.checkFormHistory()
        self.checkCookie()
        if hasMigrated:
            self.checkSession()

    def createProfileData(self):
        self.savePassword()
        self.createBookmark()
        self.createHistory()
        self.createFormHistory()
        self.createCookie()
        self.createSession()

    def setUpScriptData(self):
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        self.marionette.execute_script("""
          global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
          global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
          global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
          global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
        """, new_sandbox=False, sandbox='system')

    def runCode(self, script, *args, **kwargs):
        return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)

    def runAsyncCode(self, script, *args, **kwargs):
        return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)

    def setUp(self):
        MarionetteTestCase.setUp(self)
        self.setUpScriptData()

        self.reset_profile_path = None
        self.desktop_backup_path = None

        self.createProfileData()

    def tearDown(self):
        # Force yet another restart with a clean profile to disconnect from the
        # profile and environment changes we've made, to leave a more or less
        # blank slate for the next person.
        self.marionette.restart(clean=True, in_app=False)
        self.setUpScriptData()

        # Super
        MarionetteTestCase.tearDown(self)

        # Some helpers to deal with removing a load of files
        import errno, stat
        def handleRemoveReadonly(func, path, exc):
            excvalue = exc[1]
            if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
                os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
                func(path)
            else:
                raise

        if self.desktop_backup_path:
            shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly)

        if self.reset_profile_path:
            # Remove ourselves from profiles.ini
            profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path))
            self.runCode("""
              let [salt, name] = arguments[0].split(".");
              let profile = global.profSvc.getProfileByName(name);
              profile.remove(false)
              global.profSvc.flush();
            """, script_args=[profileLeafName])
            # And delete all the files.
            shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly)

    def doReset(self):
        self.runCode("""
          // Ensure the current (temporary) profile is in profiles.ini:
          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
          let profileName = "marionette-test-profile-" + Date.now();
          let myProfile = global.profSvc.createProfile(profD, profileName);
          global.profSvc.flush()

          // Now add the reset parameters:
          let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
          let allMarionettePrefs = Services.prefs.getChildList("marionette.");
          let prefObj = {};
          for (let pref of allMarionettePrefs) {
            let prefSuffix = pref.substr("marionette.".length);
            let prefVal = global.Preferences.get(pref);
            prefObj[prefSuffix] = prefVal;
          }
          let marionetteInfo = JSON.stringify(prefObj);
          env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo);
          env.set("MOZ_RESET_PROFILE_RESTART", "1");
          env.set("XRE_PROFILE_PATH", arguments[0]);
          env.set("XRE_PROFILE_NAME", profileName);
        """, script_args=[self.marionette.instance.profile.profile])

        profileLeafName = os.path.basename(os.path.normpath(self.marionette.instance.profile.profile))

        # Now restart the browser to get it reset:
        self.marionette.restart(clean=False, in_app=True)
        self.setUpScriptData()

        # Determine the new profile path (we'll need to remove it when we're done)
        self.reset_profile_path = self.runCode("""
          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
          return profD.path;
        """)

        # Determine the backup path
        self.desktop_backup_path = self.runCode("""
          let container;
          try {
            container = Services.dirsvc.get("Desk", Ci.nsIFile);
          } catch (ex) {
            container = Services.dirsvc.get("Home", Ci.nsIFile);
          }
          let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
          let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name], 1);
          container.append(dirName);
          container.append(arguments[0]);
          return container.path;
        """, script_args = [profileLeafName])

        self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present")
        self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present")

    def testReset(self):
        self.checkProfile()

        self.doReset()

        # Now check that we're doing OK...
        self.checkProfile(hasMigrated=True)
