#!/usr/bin/env python3
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Michael Terry

import os
import shutil
import socket
import stat
from unittest import mock

from dogtail.predicate import GenericPredicate
from dogtail.rawinput import holdKey, keyCombo, pressKey, releaseKey, typeText
from gi.repository import GLib

from . import BaseTest, ResticMixin


class BrowserTest(BaseTest):
    def setUp(self):
        super().setUp()
        self.set_string("backend", "local")
        self.set_string("last-backup", "2000-01-01")  # to skip welcome screen
        self.set_string("last-run", "2000-01-01")

        shutil.rmtree(self.rootdir, ignore_errors=True)
        self.srcdir = "/tmp/deja-dup"
        shutil.rmtree(self.srcdir, ignore_errors=True)

        self.restoredir = self.rootdir + "/restore"
        self.password = None
        self.app = self.cmd(env="DEJA_DUP_RESTORE_IN_APP=1")

    def use_backup_dir(self, path):
        basedir = os.path.realpath(os.path.join(os.path.dirname(__file__)))
        srcfiles = basedir + "/" + path
        self.set_string("folder", srcfiles, child="local")

    def scan(self, error=None):
        self.app.button("Browse Backups").click()

        if self.password and self.restic:
            self.enter_browser_password(self.app, self.password)

        if error:
            self.app.child(roleName="label", name=error)
            return

        # Select most recent snapshot
        self.app.child(roleName="list item").click()

        if self.password and not self.restic:
            self.enter_browser_password(self.app, self.password)

        search = self.app.child(roleName="toggle button", name="Search")
        self.wait_for(lambda: search.sensitive)

    def scan_dir1(self):
        """
        Set up primary backup directory, used for most tests.

        We only look at the most current backup, which must include:
         dir1/three.txt ("three")
         four.txt ("four")
         one.txt ("one")
         two.txt ("two")
        """
        self.use_backup_dir("duplicity1")
        self.scan()

    def scan_dir2(self):
        """
        Set up secondary backup directory.

        If the backup tool supports both encrypted and unencrypted backups,
        it's useful to have the primary or secondary dirs be different.

        This dir should hold three different snapshots, whose dates should
        be stored in self.snapshots.

        The most recent snapshot should contain:
          dir1/three.txt
          one.txt
          two.txt

        The oldest snapshot should contain those as well as four.txt.
        """
        self.password = "test"
        self.use_backup_dir("duplicity2")
        self.scan()
        self.snapshots = [
            "Sun Jun \u20077 09:33:07 2020",
            "Sun Jun \u20077 09:29:40 2020",
            "Thu Jun \u20074 14:52:24 2020",
        ]

    def scan_dir3(self):
        """
        Set up third backup directory.

        This one emulates switching home dirs.

        The most recent snapshot should contain:
          /home/_not-a-user_/.deja-dup-test/src/docs/1.txt ("1")
          /home/_not-a-user_/.deja-dup-test/src/docs/subdir/2.txt ("2")

        We use so many subdirs so that we can safely restore to the original
        location while also swapping out the 'src' dir with a symlink that
        points to a different dir (to simulate Fedora Silverblue symlinked
        home dirs)
        """
        self.password = "t"
        self.use_backup_dir("duplicity3")
        self.scan()

    def assert_search_mode(self, searching=True):
        search = self.app.child(roleName="toggle button", name="Search")
        assert search.pressed == searching

        search_entry = self.app.findChild(
            GenericPredicate(roleName="entry", name="Search"),
            retry=False,
            requireResult=False,
        )
        if searching:
            assert search_entry and search_entry.focused
        else:
            assert search_entry is None

    def assert_selection(self, selecting=True):
        predicate = GenericPredicate(
            roleName="toggle button", name="Click on items to select them"
        )
        selection_button = self.app.findChild(
            predicate, retry=False, requireResult=False
        )
        if selecting:
            assert selection_button
        else:
            assert not selection_button

    def start_restore(self):
        self.click_restore_button(self.app)
        self.window = self.get_dialog(self.app)

    def select_location(self, where):
        self.addCleanup(shutil.rmtree, where, ignore_errors=True)
        self.window.child(
            roleName="list item", name="Restore files to a specific folder"
        ).click()
        self.window.child(roleName="button", name="Choose Folder").click()
        os.makedirs(where, exist_ok=True)
        dlg = self.get_file_chooser("Select a Folder")
        typeText(where + "\n")
        self.wait_for(lambda: dlg.dead)

    def walk_restore(self, where=None):
        self.start_restore()
        shutil.rmtree(self.srcdir, ignore_errors=True)

        if where:
            self.select_location(where)

        self.window.button("Restore").click()  # to where

        self.wait_for(
            lambda: not self.app.findChild(
                GenericPredicate(roleName="button", label="Stop"),
                retry=False,
                requireResult=False,
            ),
            timeout=600,
        )

    def check_files(self, *file_args, where=None):
        if not where:
            where = self.srcdir

        # confirm no extra files restored
        file_count = 0
        for root, dirs, files in os.walk(where):
            file_count += len(files)
        assert file_count == len(file_args)

        # confirm content itself
        for name, content in file_args:
            test_file = open(os.path.join(where, name), "r")
            assert test_file.read(None).strip() == content

    def select(self, *args):
        children = self.app.findChildren(lambda x: x.roleName == "table row")
        if not children:
            # Table rows are best grouping for search mode, and table cells for normal mode
            children = self.app.findChildren(lambda x: x.roleName == "table cell")
        for child in children:
            # Skip if this is already in the selection state we want
            label = child.child(roleName="label")
            on = label.name in args
            if on == child.selected:
                continue

            # Perform a ctrl+click to toggle selection
            holdKey("Control_L")
            child.click()
            releaseKey("Control_L")
            if on != child.selected:
                # Try harder
                keyCombo("space")

    def test_enable_search_mode(self):
        self.scan_dir1()

        self.assert_search_mode(False)
        keyCombo("<Control>f")
        self.assert_search_mode()

        keyCombo("Escape")

        self.assert_search_mode(False)
        self.app.child(roleName="toggle button", name="Search").click()
        self.assert_search_mode()

    def test_select_all(self):
        self.scan_dir1()

        icons = self.app.findChildren(lambda x: x.roleName == "table cell")
        assert len(icons) == 4 and len([i for i in icons if i.selected]) == 0

        keyCombo("<Control>a")

        assert len([i for i in icons if i.selected]) == 4

    def test_successful_restore(self):
        self.scan_dir1()

        # select one (new location)
        self.select("four.txt")
        self.walk_restore(where=self.restoredir)
        self.check_files(("four.txt", "four"), where=self.restoredir)

        # select multiple (old location)
        self.select("one.txt", "two.txt")
        self.walk_restore()
        self.check_files(("one.txt", "one"), ("two.txt", "two"))

        # select multiple from diff dirs (old location)
        self.app.child(roleName="toggle button", name="Search").click()
        typeText("txt")
        self.select("one.txt", "three.txt")
        self.walk_restore()
        self.check_files(("one.txt", "one"), ("dir1/three.txt", "three"))
        self.app.child(roleName="toggle button", name="Search").click()

    def test_encrypted_and_dates(self):
        self.scan_dir2()

        # test dir navigation (could go in any test, but thrown in here)
        view = self.app.child(roleName="table")
        up = self.app.child(roleName="button", name="Up")
        assert not up.sensitive
        assert len(view.children) == 3
        dir1 = view.child(roleName="label", name="dir1")
        dir1.doubleClick()
        assert up.sensitive
        assert len(view.children) == 1
        up.click()
        assert not up.sensitive
        assert len(view.children) == 3

        # Go back to date selection screen
        back = self.app.child(roleName="button", name="Back")
        back.click()

        rows = self.app.findChildren(lambda x: x.roleName == "list item")
        dates = [x.name for x in rows]
        assert self.snapshots == dates, dates

        # choose oldest date, it should have an extra item in it
        rows[-1].click()
        search = self.app.child(roleName="toggle button", name="Search")
        self.wait_for(lambda: search.sensitive)
        assert len(view.children) == 4

    def test_scan_error(self):
        self.set_string(
            "folder", GLib.get_home_dir() + "/.deja-dup-test/missing", child="local"
        )
        self.scan(error="No Backups Found")

    def test_bad_restore_permissions(self):
        os.makedirs(self.srcdir, exist_ok=True)
        open(self.srcdir + "/four.txt", "w+").close()
        os.chmod(self.srcdir + "/four.txt", stat.S_IRUSR)
        open(self.srcdir + "/one.txt", "w+").close()
        os.chmod(self.srcdir + "/one.txt", stat.S_IRUSR)

        self.scan_dir1()
        self.select("four.txt", "dir1")
        self.start_restore()
        button = self.window.button("Restore")

        # Confirm that we can't backup
        help_button = self.window.findChild(
            lambda x: x.roleName == "toggle button" and x.name == "Help"
        )
        help_button.click()
        self.window.findChild(
            lambda x: (
                x.roleName == "label"
                and x.name
                == "Backups does not have permission to restore the following files:"
            )
        )
        self.window.findChild(
            lambda x: x.roleName == "label" and x.name == self.srcdir + "/four.txt"
        )
        assert not button.sensitive
        keyCombo("Escape")  # close popup

        # Test that we can if you switch locations
        self.select_location(self.restoredir)
        help_button = self.window.findChild(
            lambda x: x.roleName == "toggle button" and x.name == "Help",
            retry=False,
            requireResult=False,
        )
        assert help_button is None
        assert button.sensitive

        self.window.button("Cancel").click()

    def test_moved_home(self):
        # Set up a symlink path (to simulate Fedora Silverblue symlinked home)
        self.srcdir = self.rootdir + "/src/docs"
        os.makedirs(self.rootdir + "/deeper/src", exist_ok=True)
        os.symlink("deeper/src", self.rootdir + "/src", target_is_directory=True)

        self.scan_dir3()
        self.select("1.txt", "subdir")
        self.walk_restore()
        self.check_files(("1.txt", "1"), ("subdir/2.txt", "2"))


# Restic doesn't normally use the browser, but it can in a flatpak on Ubuntu.
# (to work around an apparmor configuration that disallows fusermount)
# So we test the browser with Restic too.
# Triggered by DEJA_DUP_RESTORE_IN_APP=1, set in the base class.
class ResticBrowserTest(ResticMixin, BrowserTest):
    def scan_dir1(self):
        self.password = "test1"
        self.use_backup_dir("restic1")
        self.scan()

    def scan_dir2(self):
        self.password = "test2"
        self.use_backup_dir("restic2")
        self.scan()
        hostname = socket.gethostname()
        self.snapshots = [
            "Mon Aug \u20072 15:34:06 2021",
            "Mon Aug \u20072 15:33:43 2021",
            "Wed Jul 28 11:43:15 2021",
        ]

    def scan_dir3(self):
        self.password = "t"
        self.use_backup_dir("restic3")
        self.scan()
