# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import argparse
import inspect
import os
import shutil
import sys
import time
import unittest
import yaml
from functools import wraps

import gnupg

try:
    from unittest.mock import patch, Mock
    from io import StringIO
except ImportError:
    # backwards compatbility with Python2
    from mock import patch, Mock
    from StringIO import StringIO

__file__ = os.path.relpath(inspect.getsourcefile(lambda _: None))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.relpath(__file__))))
import pysswords
from pysswords.db import (
    Database,
    Credential,
    CredentialNotFoundError,
    CredentialExistsError
)
from pysswords.python_two import BUILTINS_NAME


TEST_DIR = os.path.join(os.path.dirname(os.path.relpath(__file__)))
TEST_DATA_DIR = os.path.join(TEST_DIR, "data")
BENCHMARK = os.environ.get("BENCHMARK")


def timethis(func):
    ''' Decorator that reports the execution time.
    '''
    if BENCHMARK:
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            print("[{:.2f}]".format(end-start), func.__name__)
            return result
        return wrapper
    else:
        return func


def build_keys():
    gpg = gnupg.GPG(homedir="/tmp/pysswords")
    key_input = gpg.gen_key_input(
        name_real="Pysswords",
        name_email="pysswords@pysswords",
        name_comment="Auto-generated by Pysswords",
        key_length=512,
        expire_date=0,
        passphrase="dummy_passphrase"
    )
    key = gpg.gen_key(key_input)
    ascii_armored_public_keys = gpg.export_keys(key)
    ascii_armored_private_keys = gpg.export_keys(key, True)
    with open(os.path.join(TEST_DATA_DIR, "newkey.asc"), 'w') as f:
        f.write(ascii_armored_public_keys)
        f.write(ascii_armored_private_keys)


def mock_create_keyring(path, *args, **kwargs):
    """Import key.asc instead of generating new key
    passphrase used to create the key was 'dummy_database'"""
    gpg = gnupg.GPG(binary=pysswords.utils.which("gpg"), homedir=path)
    with open(os.path.join(TEST_DATA_DIR, "key.asc")) as keyfile:
        gpg.import_keys(keyfile.read())
    return gpg.list_keys()[0]


def mock_gen_key(self, key_input):
    return mock_create_keyring(self.homedir)


def some_credential(**kwargs):
    return pysswords.db.Credential(
        name=kwargs.get("name", "example.com"),
        login=kwargs.get("login", "john.doe"),
        password=kwargs.get("password", "--BEGIN GPG-- X --END GPG--"),
        comment=kwargs.get("comment", "Some comments"),
    )


def some_credential_dict(**kwargs):
    return pysswords.db.credential.asdict(
        some_credential(**kwargs)
    )


def clean(path):
    if os.path.exists(path):
        shutil.rmtree(path)


class CryptTests(unittest.TestCase):

    def setUp(self):
        self.path = os.path.join(TEST_DATA_DIR, "database")
        self.passphrase = "dummy_passphrase"
        self.cleanup()

    def tearDown(self):
        self.cleanup()

    def cleanup(self):
        if os.path.exists(self.path):
            shutil.rmtree(self.path)

    @timethis
    @patch("pysswords.crypt.create_keyring", new=mock_create_keyring)
    def test_create_keyring_adds_gpg_keys_to_path(self):
        keyring_path = os.path.join(self.path, ".keys")
        pysswords.crypt.create_keyring(keyring_path, self.passphrase)
        pubring = os.path.join(keyring_path, "pubring.gpg")
        secring = os.path.join(keyring_path, "secring.gpg")
        self.assertTrue(os.path.isfile(pubring))
        self.assertTrue(os.path.isfile(secring))

    @timethis
    @patch("pysswords.crypt.gnupg.GPG.gen_key", new=mock_gen_key)
    def test_generate_keys_return_valid_key(self):
        key = pysswords.crypt.generate_keys(self.path, self.passphrase)
        self.assertIsNotNone(key)
        self.assertEqual(
            key["fingerprint"],
            '2B88BF1F03FC2E3871894966F77B7A363E2EAE61'
        )

    @timethis
    def test_generate_key_input_returns_batch_string_with_passphrase(self):
        batch = pysswords.crypt.generate_key_input(self.path, self.passphrase)
        self.assertIn("\nPassphrase: {}".format(self.passphrase), batch)

    @timethis
    def test_create_keyring_generate_keys(self):
        self.cleanup()
        with patch("pysswords.crypt.generate_keys") as mocked_generate:
            pysswords.crypt.create_keyring(self.path, self.passphrase)
            self.assertTrue(mocked_generate.called)


class DatabaseTests(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.path = os.path.join(TEST_DATA_DIR, "database")
        cls.passphrase = "dummy_passphrase"
        to_patch = "pysswords.db.database.create_keyring"
        with patch(to_patch, new=mock_create_keyring):
            cls.database = pysswords.db.Database.create(
                cls.path,
                cls.passphrase
            )

    @classmethod
    def tearDownClass(cls):
        clean(cls.path)

    def setUp(self):
        for cred in (d for d in os.listdir(self.path) if d != ".keys"):
            fullpath = os.path.join(self.path, cred)
            shutil.rmtree(fullpath)

    @timethis
    def test_create_keyring(self):
        self.assertIsInstance(self.database, pysswords.db.Database)
        self.assertTrue(len(self.database.gpg.list_keys()) == 1)

    @timethis
    def test_keys_path_returns_database_path_joined_with_dot_keys(self):
        keys_path = self.database.keys_path
        self.assertEqual(keys_path, os.path.join(self.path, ".keys"))

    @timethis
    def test_add_credential_make_dir_in_dbpath_with_credential_name(self):
        credential = some_credential()
        self.database.add(**credential._asdict())
        credential_dir = os.path.join(self.path, credential.name)
        self.assertTrue(os.path.exists(credential_dir))
        self.assertTrue(os.path.isdir(credential_dir))

    @timethis
    def test_add_credential_createas_pyssword_file_named_after_login(self):
        credential = some_credential()
        self.database.add(**credential._asdict())
        credential_dir = os.path.join(self.path, credential.name)
        credential_filename = "{}.pyssword".format(credential.login)
        credential_file = os.path.join(credential_dir, credential_filename)
        self.assertTrue(os.path.isfile(credential_file))

    @timethis
    def test_add_credential_creates_dir_when_credential_name_is_a_dir(self):
        credential = some_credential(name="emails/misc/example.com")
        emails_dir = os.path.join(self.path, "emails")
        misc_dir = os.path.join(emails_dir, "misc")
        self.database.add(**credential._asdict())
        self.assertTrue(os.path.isdir(emails_dir))
        self.assertTrue(os.path.isdir(misc_dir))

    @timethis
    def test_add_credential_returns_credential(self):
        credential = some_credential_dict()
        returned = self.database.add(**credential)
        self.assertIsInstance(returned, Credential)

    @timethis
    def test_gpg_returns_valid_gnupg_gpg_object(self):
        gpg = self.database.gpg
        self.assertIsInstance(gpg, pysswords.crypt.gnupg.GPG)

    @timethis
    def test_credentials_returns_a_list_of_all_added_credentials(self):
        self.database.add(**some_credential(name="example.com")._asdict())
        self.database.add(**some_credential(name="archive.org")._asdict())
        credentials = self.database.credentials
        self.assertIsInstance(credentials, list)
        self.assertEqual(2, len(credentials))
        for credential in credentials:
            self.assertIsInstance(credential, pysswords.db.Credential)

    @timethis
    def test_add_repeated_credential_without_overwrite_on_raises_error(self):
        credential = some_credential_dict()
        self.database.add(**credential)
        with self.assertRaises(pysswords.db.CredentialExistsError):
            self.database.add(**credential)

    @timethis
    def test_remove_deletes_pysswords_file(self):
        credential = some_credential_dict()
        credential_path = pysswords.db.credential.expandpath(
            self.path,
            credential["name"],
            credential["login"]
        )
        self.database.add(**credential)
        self.assertTrue(os.path.isfile(credential_path))
        self.database.remove(credential["name"], credential["login"])
        self.assertFalse(os.path.isfile(credential_path))

    @timethis
    def test_remove_deletes_pyssword_dir_if_empty_after_deletion(self):
        credential = some_credential_dict()
        credential_path = pysswords.db.credential.expandpath(
            self.path,
            credential["name"],
            credential["login"]
        )
        self.database.add(**credential)
        self.assertTrue(os.path.exists(os.path.dirname(credential_path)))
        self.database.remove(credential["name"], credential["login"])
        self.assertFalse(os.path.exists(os.path.dirname(credential_path)))

    @timethis
    def test_get_credential_by_name_returns_expected_credential(self):
        credential = some_credential(name="example.com")
        self.database.add(**credential._asdict())
        found = self.database.get(name=credential.name)

        self.assertIsInstance(found, list)
        self.assertTrue(all(True for c in found
                            if isinstance(c, pysswords.db.Credential)))
        self.assertTrue(found[0].name, credential.name)

    @timethis
    def test_get_returns_unique_credential_when_login_is_passed(self):
        pwd = "dummy"
        credential = some_credential(
            name="example.com",
            password=pwd
        )
        credential2 = some_credential(
            name="example.com",
            login="jonny.doe"
        )
        with patch("pysswords.db.Database.encrypt", return_value=pwd):
            self.database.add(**credential._asdict())
            self.database.add(**credential2._asdict())
        found = self.database.get(
            name=credential.name,
            login=credential.login
        )

        self.assertEqual(found[0], credential)

    @timethis
    def test_raises_credential_not_found_when_name_login_not_found(self):
        credential = some_credential(name="example.com")
        self.database.add(**credential._asdict())
        with self.assertRaises(CredentialNotFoundError):
            self.database.get(name="not added name")

    @timethis
    def test_search_database_returns_list_with_matched_credentials(self):
        credential1 = some_credential_dict(name="example.com")
        credential2 = some_credential_dict(name="github.com")
        credential3 = some_credential_dict(name="twitter.com")
        self.database.add(**credential1)
        self.database.add(**credential2)
        self.database.add(**credential3)

        self.assertEqual(len(self.database.search("it")), 2)
        self.assertEqual(len(self.database.search("github")), 1)
        self.assertEqual(len(self.database.search("not there")), 0)

    @timethis
    def test_encrypt_text_returns_valid_encryption_ascii_gpg(self):
        text = "secret"
        encrypted = self.database.encrypt(text)
        self.assertIn("-BEGIN PGP MESSAGE-", encrypted)
        self.assertIn("-END PGP MESSAGE-", encrypted)

    @timethis
    def test_key_returns_expected_key_fingerprint(self):
        self.assertEqual(
            self.database.key(),
            "2B88BF1F03FC2E3871894966F77B7A363E2EAE61")

    @timethis
    def test_key_returns_private_key_when_private_is_true(self):
        mock = Mock()
        mock.return_value = [
            {"fingerprint": "2B88BF1F03FC2E3871894966F77B7A363E2EAE61"}
        ]
        self.database.gpg.list_keys = mock
        self.database.key(private=True)
        self.database.gpg.list_keys.assert_any_call_with(secret=True)

    @timethis
    def test_key_raises_valueerror_when_key_not_found(self):
        with patch.object(self.database.gpg, "list_keys", return_value=[]):
            with self.assertRaises(ValueError):
                self.database.key()

    @timethis
    def test_decrypt_returns_plain_text_data(self):
        text = "secret"
        encrypted = self.database.encrypt(text)
        decrypted = self.database.decrypt(encrypted,
                                          passphrase=self.passphrase)
        self.assertEqual(decrypted, text)

    @timethis
    def test_update_credential_updates_credential_values(self):
        values = some_credential_dict()
        self.database.add(
            name=values["name"],
            login=values["login"],
            password=values["password"],
            comment=values["comment"]
        )
        name = values["name"]
        login = values["login"]
        new_values = values
        new_values["login"] = "doe.john"
        self.database.update(name, login, to_update=new_values)
        found = self.database.get(
            name=new_values["name"],
            login=new_values["login"]
        )

        self.assertEqual(found[0].login, new_values["login"])

    @timethis
    def test_update_credential_empty_passwords_keeps_default_password(self):
        values = some_credential_dict()
        self.database.add(
            name=values["name"],
            login=values["login"],
            password=values["password"],
            comment=values["comment"]
        )
        new_name = "NewService"
        new_values = {"name": new_name}
        name, login = values["name"], values["login"]
        self.database.update(name, login, to_update=new_values)
        found = self.database.get(
            name=new_values["name"],
            login=values["login"]
        )
        self.assertEqual(found[0].name, new_name)

    @timethis
    def test_update_raises_credential_not_found_error(self):
        self.database.get = Mock(side_effect=CredentialNotFoundError)
        with self.assertRaises(CredentialNotFoundError):
            self.database.update("not a name", None, {})

    @timethis
    def test_remove_raises_credentialnotfounderror(self):
        with self.assertRaises(CredentialNotFoundError):
            self.database.remove(name="none", login="none")

    @timethis
    def test_check_uses_gpg_sign_as_passphrase_checker(self):
        self.database.gpg.sign = Mock()
        self.database.check(self.passphrase)
        self.assertTrue(self.database.gpg.sign.called)
        self.database.gpg.sign.assert_called_once_with(
            "testing",
            default_key=self.database.key(True),
            passphrase=self.passphrase
        )

    @timethis
    def test_exportdb_creates_tar_with_database_at_given_dbfile_path(self):
        dbfile = "pysswords.db"
        with patch("pysswords.db.database.os"):
            with patch("pysswords.db.database.shutil") as mock_shutil:
                self.database.exportdb(dbfile=dbfile)
                mock_shutil.make_archive.assert_called_once_with(
                    dbfile,
                    "tar",
                    self.path)

    @timethis
    def test_importdb_unpacks_dbfile_tar_at_database_path(self):
        dbfile = "pysswords.db"
        with patch("pysswords.db.database.tarfile") as mocked:
            mocked_tar = Mock()
            mocked.open.return_value.__enter__.return_value = mocked_tar
            self.database.importdb(dbfile=dbfile)
            mocked.open.assert_called_once_with(dbfile)
            mocked_tar.extractall.assert_called_once_with(self.path)


class CredentialTests(unittest.TestCase):

    def setUp(self):
        self.path = os.path.join(TEST_DATA_DIR, "database")
        self.cleanup()

    def tearDown(self):
        self.cleanup()

    def cleanup(self):
        if os.path.exists(self.path):
            shutil.rmtree(self.path)

    @timethis
    def test_credential_expandpath_returns_expected_path_to_credential(self):
        credential = some_credential()
        credential_path = pysswords.db.credential.expandpath(
            self.path,
            name=credential.name,
            login=credential.login
        )
        expected_path = os.path.join(
            self.path,
            os.path.basename(credential.name),
            "{}.pyssword".format(credential.login)
        )
        self.assertEqual(credential_path, expected_path)

    @timethis
    def test_credential_content_returns_yaml_content_parseable_to_dict(self):
        content = pysswords.db.credential.content(some_credential())
        self.assertEqual(yaml.load(content), some_credential())

    @timethis
    def test_credential_asfullname_returns_expected_string(self):
        name = "example.com"
        login = "doe"
        fullname = pysswords.db.credential.asfullname(name, login)
        self.assertEqual(fullname, "{}@{}".format(login, name))

    @timethis
    def test_credential_splitname_returns_expected_name_login_tuple(self):
        name = "example.com"
        login = "doe"
        fullname = "{}@{}".format(login, name)
        name_login_tuple = pysswords.db.credential.splitname(fullname)
        self.assertEqual((name, login), name_login_tuple)

    @timethis
    def test_credential_splitname_raises_valueerror_when_invalid_name(self):
        invalid = ""
        with self.assertRaises(ValueError):
            pysswords.db.credential.splitname(invalid)


class UtilsTests(unittest.TestCase):

    @timethis
    def test_which_handle_windows_exe_extension_for_executables(self):
        with patch("pysswords.utils.os") as mocker:
            mocker.name = "nt"
            mocker.environ = {"PATH": "/"}
            mocker.pathsep = ":"
            mocked_join = Mock()
            mocker.path.join = mocked_join
            with patch("pysswords.utils.shutil") as mockshutil:
                mockshutil.which = Mock(side_effect=AttributeError)
                pysswords.utils.which("python")
            mocked_join.assert_any_call("/", "python.exe")




class MainTests(unittest.TestCase):

    def setUp(self):
        self.tempdb_path = os.path.join(TEST_DATA_DIR, "tmp")
        self.cleanup()
        self.passphrase = "dummy_passphrase"

    def tearDown(self):
        self.cleanup()

    def cleanup(self):
        if os.path.exists(self.tempdb_path):
            shutil.rmtree(self.tempdb_path)

    def create_database(self):
        with patch("pysswords.db.database.create_keyring",
                   new=mock_create_keyring):
            return Database.create(self.tempdb_path, self.passphrase)

    @timethis
    def test_logs_error_and_exit_application_when_gpg_is_not_installed(self):
        with patch("pysswords.__main__.which", return_value=None):
            with patch("pysswords.__main__.logging") as mock_logging:
                with self.assertRaises(SystemExit):
                    pysswords.__main__.main(["--init"])
                mock_logging.error.assert_called_once_with(
                    "GPG not installed: https://gnupg.org/download")

    @timethis
    def test_main_parse_args_returns_argparse_namespace(self):
        args = pysswords.__main__.parse_args(["--init"])
        self.assertIsInstance(args, argparse.Namespace)

    @timethis
    def test_main_default_pyssword_dir(self):
        pysswords_dir = os.path.join(os.path.expanduser("~"), ".pysswords")
        self.assertEqual(pysswords_dir, pysswords.__main__.default_db())

    @timethis
    def test_main_parse_args_has_init_arg(self):
        args = pysswords.__main__.parse_args(["--init"])
        self.assertIn("init", args.__dict__)
        args_short = pysswords.__main__.parse_args(["-I"])
        self.assertIn("init", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_database_arg(self):
        args = pysswords.__main__.parse_args(["--database", "/tmp/pysswords"])
        self.assertIn("database", args.__dict__)
        args_short = pysswords.__main__.parse_args(["-D", "/tmp/pysswords"])
        self.assertIn("database", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_database_default_value(self):
        args = pysswords.__main__.parse_args([])
        self.assertEqual(args.database, pysswords.__main__.default_db())

    @timethis
    def test_main_parse_args_has_add_arg(self):
        args = pysswords.__main__.parse_args(["--add"])
        self.assertIn("add", args.__dict__)
        args_short = pysswords.__main__.parse_args(["-a"])
        self.assertIn("add", args_short.__dict__)

    @timethis
    def test_main_parse_args_add_arg_is_true_when_passed(self):
        args = pysswords.__main__.parse_args(["--add"])
        self.assertTrue(args.add)

    @timethis
    def test_main_parse_args_has_remove_arg(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--remove", credential_name])
        args_short = pysswords.__main__.parse_args(["-r", credential_name])
        self.assertIn("remove", args.__dict__)
        self.assertIn("remove", args_short.__dict__)

    @timethis
    def test_main_parse_args_remove_arg_has_credential_name_passed(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--remove", credential_name])
        self.assertTrue(args.remove, credential_name)

    @timethis
    def test_main_parse_args_has_update_arg(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--update", credential_name])
        args_short = pysswords.__main__.parse_args(["-u", credential_name])
        self.assertIn("update", args.__dict__)
        self.assertIn("update", args_short.__dict__)

    @timethis
    def test_main_parse_args_update_arg_has_credential_name_passed(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--update", credential_name])
        self.assertEqual(args.update, credential_name)

    @timethis
    def test_main_parse_args_has_get_arg(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--get", credential_name])
        args_short = pysswords.__main__.parse_args(["-g", credential_name])
        self.assertIn("get", args.__dict__)
        self.assertIn("get", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_show_password_arg(self):
        args = pysswords.__main__.parse_args(["--show-password"])
        args_short = pysswords.__main__.parse_args(["-P"])
        self.assertIn("show_password", args.__dict__)
        self.assertIn("show_password", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_exportdb_arg(self):
        args = pysswords.__main__.parse_args(["--export", "pysswords.db"])
        self.assertIn("exportdb", args.__dict__)

    @timethis
    def test_main_parse_args_has_importdb_arg(self):
        args = pysswords.__main__.parse_args(["--import", "pysswords.db"])
        self.assertIn("importdb", args.__dict__)

    @timethis
    def test_main_parse_args_get_arg_has_credential_name_passed(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--get", credential_name])
        self.assertEqual(args.get, credential_name)

    @timethis
    def test_main_parse_args_has_search_arg(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--search", credential_name])
        args_short = pysswords.__main__.parse_args(["-s", credential_name])
        self.assertIn("search", args.__dict__)
        self.assertIn("search", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_verbose_arg(self):
        args = pysswords.__main__.parse_args(["--verbose"])
        args_short = pysswords.__main__.parse_args(["-v"])
        self.assertIn("verbose", args.__dict__)
        self.assertIn("verbose", args_short.__dict__)

    @timethis
    def test_main_parse_args_has_clean_arg(self):
        args = pysswords.__main__.parse_args(["--clean"])
        self.assertIn("clean", args.__dict__)

    @timethis
    def test_main_handles_verbose_option_setting_logger_level_to_info(self):
        with patch("pysswords.__main__.logging") as mock_logging:
            logger = mock_logging.getLogger()
            with patch("sys.stdout"):
                pysswords.__main__.main(["--verbose"])
            logger.setLevel.assert_called_once_with(mock_logging.INFO)

    @timethis
    def test_main_parse_args_search_arg_has_credential_name_passed(self):
        credential_name = "example.com"
        args = pysswords.__main__.parse_args(["--search", credential_name])
        self.assertEqual(args.search, credential_name)

    @timethis
    def test_main_raises_error_when_clipboard_passed_without_get_args(self):
        with open(os.devnull, 'w') as devnull:
            with patch("sys.stderr", devnull):
                with self.assertRaises(SystemExit):
                    pysswords.__main__.parse_args(["--clipboard"])
            with patch("sys.stderr", devnull):
                with self.assertRaises(SystemExit):
                    pysswords.__main__.parse_args(["-c"])

    @timethis
    def test_main_calls_cli_constructor_with_init_when_init_passed(self):
        tmp_path = "/tmp/.pysswords"
        args = ["-D", tmp_path, "--init"]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked.assert_called_once_with(
                database_path=tmp_path,
                show_password=False,
                init=True
            )

    @timethis
    def test_main_calls_cli_add_credential_when_add_passed(self):
        args = ["-D", "/tmp/pysswords", "--add"]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().add_credential.assert_called_once_with()

    @timethis
    def test_main_calls_interface_exportdb_when_export_arg_passed(self):
        dbfile = "pysswords.db"
        database = "/path/to/.pysswords"
        args = ["--export", dbfile, "-D", database]
        with patch("pysswords.__main__.CLI"):
            with patch("pysswords.__main__.CLI") as mocked_cli:
                pysswords.__main__.main(args)
                mocked_cli().exportdb.assert_called_once_with(
                    dbfile
                )

    @timethis
    def test_main_calls_interface_importdb_when_importb_arg_passed(self):
        dbfile = "pysswords.db"
        database = "/path/to/.pysswords"
        args = ["--import", dbfile, "-D", database]
        with patch("pysswords.__main__.CLI"):
            with patch("pysswords.__main__.CLI") as mocked_cli:
                pysswords.__main__.main(args)
                mocked_cli().importdb.assert_called_once_with(dbfile)

    @timethis
    def test_main_calls_cli_get_credentials_when_get_passed(self):
        fullname = "john.doe@example.com"
        args = ["-D", "/tmp/pysswords", "--get", fullname]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().get_credentials.assert_called_once_with(
                fullname=fullname
            )

    @timethis
    def test_main_calls_cli_search_credentials_when_search_passed(self):
        query = "example.com|org|net"
        args = ["-D", "/tmp/pysswords", "--search", query]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().search_credentials.assert_called_once_with(
                query=query
            )

    @timethis
    def test_main_calls_cli_update_credentials_when_update_passed(self):
        fullname = "john.doe@example.com"
        args = ["-D", "/tmp/pysswords", "--update", fullname]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().update_credentials.assert_called_once_with(
                fullname=fullname
            )

    @timethis
    def test_main_calls_cli_remove_credentials_when_remove_passed(self):
        fullname = "john.doe@example.com"
        args = ["-D", "/tmp/pysswords", "--remove", fullname]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().remove_credentials.assert_called_once_with(
                fullname=fullname
            )

    @timethis
    def test_main_calls_cli_show_when_nothing_passed(self):
        args = []
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().show.assert_called_once_with()

    @timethis
    def test_main_calls_cli_clean_database_when_clean_arg_passed(self):
        args = ["--clean"]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().clean_database.assert_called_once_with()

    @timethis
    def test_main_calls_copy_to_clipboard_when_clipboard_passed(self):
        fullname = "john.doe@example.com"
        args = ["-D", "/tmp/pysswords", "--clipboard", "--get", fullname]
        with patch("pysswords.__main__.CLI") as mocked:
            pysswords.__main__.main(args)
            mocked().copy_to_clipboard.assert_called_once_with(
                fullname=fullname
            )

    @timethis
    def test_main_handles_credential_not_found_error(self):
        fullname = "john@example.com"
        exception = CredentialNotFoundError(fullname)
        with patch("pysswords.__main__.CLI.get_credentials",
                   side_effect=exception):
            with patch("pysswords.__main__.logging") as mock_logging:
                pysswords.__main__.main(["-g", fullname])
                mock_logging.error.assert_called_once_with(
                    "Credential '{}' not found".format(fullname))

    @timethis
    def test_main_handles_credential_exists_error(self):
        fullname = "john@example.com"
        exception = CredentialExistsError(fullname)
        with patch("pysswords.__main__.CLI.add_credential",
                   side_effect=exception):
            with patch("pysswords.__main__.logging") as mock_logging:
                pysswords.__main__.main(["-a"])
                mock_logging.error.assert_called_once_with(
                    "Credential '{}' exists".format(fullname))

    @timethis
    def test_main_handles_database_exists_error(self):
        fullname = "john@example.com"
        with patch("pysswords.db.database.os.makedirs", side_effect=OSError):
            with patch("pysswords.cli.CLI.prompt"):
                with patch("pysswords.__main__.logging") as mock_logging:
                    pysswords.__main__.main(["--init"])
                    mock_logging.error.assert_called_once_with(
                        "Database exists".format(fullname))

    @timethis
    def test_main_handles_credential_valueerror(self):
        fullname = "john@example.com"
        exception = ValueError("Database key not found or corrupted")
        with patch("pysswords.__main__.CLI.get_credentials",
                   side_effect=exception):
            with patch("pysswords.__main__.logging") as mock_logging:
                pysswords.__main__.main(["--get", fullname])
                mock_logging.error.assert_called_once_with(
                    "Database key not found or corrupted")

    @timethis
    def test_main_handles_credential_keyboardinterrupt(self):
        exception = KeyboardInterrupt()
        with patch("pysswords.__main__.CLI.add_credential",
                   side_effect=exception):
            with patch("pysswords.__main__.logging") as mock_logging:
                pysswords.__main__.main(["--add"])
                mock_logging.info.assert_called_once_with("Keyboard interrupt")


@patch("pysswords.cli.Database")
class CLITests(unittest.TestCase):

    @timethis
    def test_cli_instances_has_expected_attributes(self, _):
        database_path = "/tmp/pysswords"
        interface = pysswords.cli.CLI(
            database_path=database_path,
            show_password=False,
            init=False,
        )
        self.assertIn("database", dir(interface))
        self.assertIn("headers", dir(interface))
        self.assertIn("tablefmt", dir(interface))
        self.assertIn("show_password", dir(interface))

    @timethis
    def test_cli_instances_calls_create_database_if_init_true(self, _):
        database_path = "/tmp/pysswords"
        with patch("pysswords.cli.CLI.create_database") as mocked:
            pysswords.cli.CLI(
                database_path=database_path,
                show_password=False,
                init=True,
            )
            mocked.assert_called_once_with(
                path=database_path
            )

    @timethis
    def test_cli_colored_returns_colored_text(self, _):
        text = "some text"
        color1 = pysswords.cli.colorama.Fore.YELLOW
        color2 = pysswords.cli.colorama.Fore.RED
        color_reset = pysswords.cli.colorama.Fore.RESET
        colored_text1 = "{}{}{}".format(
            color1,
            text,
            color_reset
        )
        colored_text2 = "{}{}{}".format(
            color2,
            text,
            color_reset
        )
        self.assertEqual(
            pysswords.cli.CLI.colored(text, "yellow"),
            colored_text1
        )
        self.assertEqual(
            pysswords.cli.CLI.colored(text, "red"),
            colored_text2
        )

    @timethis
    def test_cli_create_database_calls_prompt_password(self, _):
        with patch("pysswords.cli.CLI.prompt") as mocked_prompt:
            with patch("pysswords.cli.CLI.write"):
                pysswords.cli.CLI.create_database("some path")
                self.assertTrue(mocked_prompt.called)

    @timethis
    def test_cli_prompt_with_password_calls_prompt_password(self, _):
        with patch("pysswords.cli.CLI.prompt_password") as mocked:
            pysswords.cli.CLI.prompt("Pass:", password=True)
            self.assertTrue(mocked.called)

    @timethis
    def test_promt_password_returns_entered_password(self, _):
        with patch(BUILTINS_NAME + ".print"):
            with patch("pysswords.cli.getpass") as mocked:
                entry = "entry"
                mocked.return_value = entry
                ret = pysswords.cli.CLI.prompt_password("Pass:")
                self.assertEqual(entry, ret)

    @timethis
    def test_getpassphrase_raises_value_error_when_pwd_didnt_match(self, _):
        with patch("pysswords.cli.CLI.write"):
            with patch("pysswords.cli.getpass") as mocked:
                mocked.side_effect = ["pass", "wrong"] * 3
                with self.assertRaises(ValueError):
                    pysswords.cli.CLI.prompt_password("Password:")

    @timethis
    def test_cli_show_prints_credentials(self, _):
        credentials = [
            some_credential(),
            some_credential(login="mylogin")
        ]
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.credentials = credentials
        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
            interface.show()
            output = mock_stdout.getvalue()
        for credential in credentials:
            self.assertIn(credential.name, output)
            self.assertIn(credential.login, output)
            self.assertIn(credential.comment, output)

    @timethis
    def test_get_credentials_calls_db_get_credential(self, mocked_db):
        interface = pysswords.cli.CLI("some path", show_password=False)
        fullname = "doe@example.com"
        interface.get_credentials(fullname)
        name, login = pysswords.db.credential.splitname(fullname)
        mocked_db().get.assert_called_once_with(
            name=name,
            login=login
        )

    @timethis
    def test_cli_clean_removes_database_path_with_prompt_confirmation(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        with patch("pysswords.cli.CLI.prompt_confirmation") as mock_prompt:
            mock_prompt.return_value = True
            with patch("pysswords.cli.shutil.rmtree") as mock_rmtree:
                interface.clean_database()
                dbpath = interface.database.path
                mock_prompt.assert_called_once_with(
                    "Delete database at '{}'? ".format(dbpath))
                mock_rmtree.assert_called_once_with(dbpath)

            mock_prompt.return_value = False
            with patch("pysswords.cli.shutil.rmtree") as mock_rmtree:
                interface.clean_database()
                self.assertFalse(mock_rmtree.called)

    @timethis
    def test_cli_clean_logs_database_deleted_to_info(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        with patch("pysswords.cli.CLI.prompt_confirmation") as mock_prompt:
            mock_prompt.return_value = True
            with patch("pysswords.cli.shutil.rmtree") as mock_rmtree:
                with patch("pysswords.cli.logging") as mock_logging:
                    interface.clean_database()
                    dbpath = interface.database.path
                    mock_logging.info.assert_called_once_with(
                        "Database '{}' deleted.".format(dbpath))

    @timethis
    def test_remove_credentials_calls_db_get(self, mockdb):
        interface = pysswords.cli.CLI("some path", show_password=False)
        fullname = "doe@example.com"
        interface = pysswords.cli.CLI("some path", show_password=False)
        with patch("sys.stdout"):
            with patch("pysswords.cli.CLI.prompt_confirmation") as mocked:
                mocked.return_value = True
                interface.remove_credentials(fullname)

        name, login = pysswords.db.credential.splitname(fullname)
        mockdb().remove.assert_called_once_with(
            name=name,
            login=login
        )

    @timethis
    def test_remove_credentials_not_calls_db_remove_no_confirm(self, mockdb):
        interface = pysswords.cli.CLI("some path", show_password=False)
        fullname = "doe@example.com"
        interface = pysswords.cli.CLI("some path", show_password=False)
        with patch(BUILTINS_NAME + ".print"):
            with patch("pysswords.cli.CLI.prompt_confirmation") as mocked:
                mocked.return_value = False
                interface.remove_credentials(fullname)

        self.assertFalse(mockdb().remove.called)

    @timethis
    def test_remove_credentials_raises_credential_not_found(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        fullname = "doe@example.com"
        interface.database.get.side_effect = CredentialNotFoundError
        with self.assertRaises(CredentialNotFoundError):
            interface.remove_credentials(fullname)

    @timethis
    def test_remove_credentials_logs_removed_credentials_to_info(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        credentials = [some_credential(name="example", login="jonh"),
                       some_credential(name="example", login="jonh2")]
        interface.database.get = Mock(return_value=credentials)
        interface.write = Mock()
        interface.prompt_confirmation = Mock(return_value=True)
        with patch("pysswords.cli.logging") as mock_logging:
            fullname = "example"
            interface.remove_credentials(fullname)
            self.assertEqual(2, mock_logging.info.call_count)

    @timethis
    def test_update_credentials_logs_updated_credentials_to_info(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        credentials = [some_credential(name="example", login="jonh"),
                       some_credential(name="example", login="jonh2")]
        interface.database.get = Mock(return_value=credentials)
        interface.write = Mock()
        interface.prompt_confirmation = Mock(return_value=True)
        interface.prompt_credential = Mock(return_value={"comment": "dummy"})
        interface.database.update = Mock(return_value=credentials)
        with patch("pysswords.cli.logging") as mock_logging:
            fullname = "example"
            interface.update_credentials(fullname)
            self.assertEqual(2, mock_logging.info.call_count)

    @timethis
    def test_show_ask_passphrase_when_show_password_true(self, mockdb):
        interface = pysswords.cli.CLI("some path", show_password=True)
        interface.database.credentials = [some_credential(),
                                          some_credential(name="none")]
        with patch("pysswords.cli.CLI.get_passphrase") as mocked_getpass:
            with patch("sys.stdout"):
                interface.show()
            mocked_getpass.assert_called_once_with()

    @timethis
    def test_prompt_credential_returns_credential_dict(self, _):
        name, login, password, comment = "exmp.le", "_", 123, "Sans"
        with patch("pysswords.cli.CLI.prompt"):
            interface = pysswords.cli.CLI("some path", show_password=True)
            interface.prompt.side_effect = [
                name,
                login,
                password,
                comment
            ]
            credential_dict = interface.prompt_credential()
            self.assertIsInstance(credential_dict, dict)
            self.assertEqual(credential_dict["name"], name)
            self.assertEqual(credential_dict["login"], login)
            self.assertEqual(credential_dict["password"], password)
            self.assertEqual(credential_dict["comment"], comment)

    @timethis
    def test_prompt_confirmation_returns_expected_boolean_for_reply(self, _):
        with patch("pysswords.cli.CLI.prompt") as mockprompt:
            mockprompt.return_value = "y"
            confirmed = pysswords.cli.CLI.prompt_confirmation("")
            mockprompt.return_value = "n"
            not_confirmed = pysswords.cli.CLI.prompt_confirmation("")
            call_args, _ = mockprompt.call_args

            self.assertIn("y|n", call_args[0])
            self.assertTrue(confirmed)
            self.assertFalse(not_confirmed)

    @timethis
    def test_decrypt_credentials_is_called_for_every_credential(self, mockdb):
        credentials = [
            some_credential(),
            some_credential(name="something"),
        ]
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.decrypt_credentials(
            credentials=credentials,
            passphrase=Mock()
        )
        for credential in credentials:
            mockdb.decrypt.assert_any_call_with(credential.password)

    @timethis
    def test_show_prints_wrong_passphrase_bad_passphrase(self, _):
        interface = pysswords.cli.CLI("some path", show_password=True)
        interface.database.check = Mock(return_value=False)
        interface.database.credentials = [some_credential(),
                                          some_credential(name="none")]
        with patch("pysswords.cli.getpass"):
            with self.assertRaises(ValueError):
                interface.show()

    @timethis
    def test_show_calls_decrypt_credentials(self, _):
        interface = pysswords.cli.CLI("some path", show_password=True)
        interface.get_passphrase = Mock()
        interface.decrypt_credentials = Mock(return_value=[])
        interface.database.credentials = [some_credential(),
                                          some_credential(name="none")]
        with patch("sys.stdout"):
            interface.show()
        self.assertTrue(interface.decrypt_credentials.called)

    @timethis
    def test_add_credential_calls_db_add_with_credential_dict(self, mockdb):
        credential_dict = some_credential_dict()
        interface = pysswords.cli.CLI("some path", show_password=False)
        with patch("pysswords.cli.CLI.prompt_credential") as mockprompt:
            mockprompt.return_value = credential_dict
            interface.add_credential()
        mockdb().add.assert_called_once_with(**credential_dict)

    @timethis
    def test_search_credentials_calls_db_search_with_query(self, mockdb):
        query = "some query"
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.search_credentials(query)
        mockdb().search.assert_called_once_with(
            query=query
        )

    @timethis
    def test_write_prints_text_to_stdout(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        text = "some text"
        with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
            interface.write(text)
            self.assertIn(text, mock_stdout.getvalue())

    @timethis
    def test_uptade_credentials_raises_credential_not_found(self, mockdb):
        fullname = "doe@example.com"
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.get.side_effect = CredentialNotFoundError
        with self.assertRaises(CredentialNotFoundError):
            interface.update_credentials(fullname)

    @timethis
    def test_uptade_credentials_calls_prompt_confirmation(self, mockdb):
        fullname = "doe@example.com"
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.show = Mock()
        mockdb().get.return_value = [some_credential()]
        with patch("pysswords.cli.CLI.prompt_confirmation") as mockprompt:
            mockprompt.return_value = False
            interface.update_credentials(fullname)
            mockprompt.assert_called_once_with(
                "Edit these credentials?"
            )

    @timethis
    def test_update_credentials_calls_db_update_with_entries(self, mockdb):
        to_update = {"name": "example.com"}
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.show = Mock()
        interface.prompt_credential = Mock()
        interface.prompt_confirmation = Mock()
        interface.prompt_confirmation.return_value = True
        interface.prompt_credential.return_value = to_update
        mockdb().get.return_value = [some_credential()]

        interface.update_credentials("anything")
        _, call_args = mockdb().update.call_args
        self.assertEqual(call_args["to_update"], to_update)

    @timethis
    def test_copy_to_clipboard_logs_multiple_credentials_found(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.get.return_value = [
            some_credential(),
            some_credential(name="something_else")]
        with patch("pysswords.cli.logging.warning") as logger:
            with patch("pysswords.cli.getpass"):
                interface.copy_to_clipboard("fullname")
            self.assertTrue(logger.called)

    @timethis
    def test_copy_to_clipboard_calls_pyperclip_copy_with_pwd(self, mockdb):
        interface = pysswords.cli.CLI("some path", show_password=False)
        password = "password"
        mockdb().get.return_value = [some_credential()]
        mockdb().decrypt.return_value = password
        interface.write = Mock()
        interface.get_passphrase = Mock()
        with patch("pysswords.cli.pyperclip") as mockpyperclip:
            interface.copy_to_clipboard("fullname")
            mockpyperclip.copy.assert_called_once_with(password)

    @timethis
    def test_get_passphrase_returns_none_when_bad_passphrase(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.check = Mock(return_value=False)
        with patch("pysswords.cli.getpass"):
            with self.assertRaises(ValueError) as raised:
                interface.get_passphrase()
            self.assertEqual(str(raised.exception), "Wrong passphrase")

    @timethis
    def test_get_passphrase_returns_good_passphrase(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.check = Mock(return_value=True)
        interface.show = Mock()
        passphrase = "passphrase"
        with patch("pysswords.cli.getpass", return_value=passphrase):
            self.assertEqual(interface.get_passphrase(), passphrase)

    @timethis
    def test_copy_to_clipboard_raises_valueerror_when_bad_passphrase(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        interface.database.get.return_value = [some_credential()]
        interface.database.check = Mock(return_value=False)
        with patch("pysswords.cli.getpass"):
            with self.assertRaises(ValueError) as raised:
                interface.copy_to_clipboard("fullname")
            self.assertEqual(str(raised.exception), "Wrong passphrase")

    @timethis
    def test_cli_calls_database_exportdb_when_export_called(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        dbfile = "pysswords.db"
        interface.exportdb(dbfile)
        interface.database.exportdb.assert_called_once_with(
            dbfile)

    @timethis
    def test_cli_calls_database_importdb_when_importdb_called(self, _):
        interface = pysswords.cli.CLI("some path", show_password=False)
        dbfile = "pysswords.db"
        interface.importdb(dbfile)
        interface.database.importdb.assert_called_once_with(
            dbfile)


if __name__ == "__main__":
    if sys.version_info >= (3,):
        unittest.main(warnings=False)
    else:
        unittest.main()
