# Copyright (c) Aaron Gallagher <_@habnab.it>
# See COPYING for details.

from __future__ import unicode_literals

import pytest
import py.path
import unittest

from passacre import application
from passacre.test.util import excinfo_arg_0


datadir = py.path.local(__file__).dirpath('data')


def fake_open(path, mode):
    if 'fail' in path:
        raise IOError()
    return path, mode

def fake_expanduser(path):
    return path.replace('~', '.')

def read_out(capsys, app, *args):
    app.main(args)
    out, err = capsys.readouterr()
    assert not err
    return out

def read_err(capsys, app, *args):
    app.main(args)
    out, err = capsys.readouterr()
    assert not out
    return err


def test_open_first():
    assert application.open_first([
        'fail-spam',
        'fail-spam',
        'eggs',
        'spam',
    ], open=fake_open) == ('eggs', 'r')
    assert application.open_first([
        '~/fail-spam',
        '~/fail-spam',
        '~/eggs',
        '~/spam',
    ], open=fake_open, expanduser=fake_expanduser) == ('./eggs', 'r')
    assert application.open_first([
        'fail-spam',
        'fail-spam',
        'eggs',
        'spam',
    ], 'w', open=fake_open) == ('eggs', 'w')
    with pytest.raises(ValueError):
        application.open_first(['fail-spam', 'fail-eggs'], open=fake_open)
    with pytest.raises(ValueError):
        application.open_first([
            '~/fail-spam',
            '~/fail-eggs'
        ], open=fake_open, expanduser=fake_expanduser)
    with pytest.raises(ValueError):
        application.open_first([], open=fake_open)


def test_prompt(capsys):
    assert application.prompt('foo: ', input=lambda: 'bar') == 'bar'
    out, err = capsys.readouterr()
    assert not out
    assert err == 'foo: '


def fake_getpass(outputs):
    def getpass(prompt=None):
        assert prompt in outputs
        return outputs[prompt]
    return getpass

def test_prompt_password():
    assert 'bar' == application.prompt_password(False, getpass=fake_getpass({None: 'bar'}))
    assert 'bar' == application.prompt_password(
        True, getpass=fake_getpass({None: 'bar', 'Confirm password: ': 'bar'}))
    with pytest.raises(ValueError):
        application.prompt_password(
            True, getpass=fake_getpass({None: 'foo', 'Confirm password: ': 'bar'}))


class FakeXerox(object):
    def __init__(self):
        self.copied = []

    def copy(self, arg):
        self.copied.append(arg)

class FakeSleep(object):
    def __init__(self, interrupt=False):
        self.duration = None
        self.interrupt = interrupt

    def __call__(self, duration):
        self.duration = duration
        if self.interrupt:
            raise KeyboardInterrupt()

class FakeAtexit(object):
    def __init__(self):
        self.calls = []

    def register(self, f, *a, **kw):
        self.calls.append((f, a, kw))

    def call_all(self):
        for f, a, kw in self.calls:
            f(*a, **kw)


class ApplicationTestCaseMixin(object):
    config_dir = None
    hashed_site = None
    hashed_password = None

    @pytest.fixture(autouse=True)
    def init_capsys(self, capsys):
        self.capsys = capsys

    def setUp(self):
        datadir.join(self.config_dir).chdir()
        self.app = application.Passacre()
        _load_config = self.app.load_config
        self.app.load_config = lambda f=None: _load_config(f, expanduser=fake_expanduser)
        self.confirmed_password = False
        self.app._prompt_password = self.prompt_password
        self.password = 'passacre'

    def prompt_password(self, confirm):
        self.confirmed_password = confirm
        return self.password


    def test_generate_with_newline(self):
        out = read_out(self.capsys, self.app, 'generate', 'example.com')
        assert not self.confirmed_password
        assert out == self.hashed_password + '\n'

    def test_generate_no_newline(self):
        out = read_out(self.capsys, self.app, 'generate', '-n', 'example.com')
        assert out == self.hashed_password

    def test_generate_prompt_site(self):
        self.app.prompt = lambda _: 'example.com'
        out = read_out(self.capsys, self.app, 'generate')
        assert out == self.hashed_password + '\n'

    def test_generate_confirm_password(self):
        out = read_out(self.capsys, self.app, 'generate', 'example.com', '-c')
        assert self.confirmed_password
        assert out == self.hashed_password + '\n'

    def test_generate_copying(self):
        self.app.xerox = FakeXerox()
        err = read_err(self.capsys, self.app, 'generate', 'example.com', '-C')
        assert err == 'password copied.\n'
        assert self.app.xerox.copied == [self.hashed_password]

    def test_generate_copy_timeout(self):
        self.app.xerox = FakeXerox()
        self.app.sleep = FakeSleep()
        self.app.atexit = FakeAtexit()
        err = read_err(self.capsys, self.app, 'generate', 'example.com', '-C', '-w', '10')
        assert err == 'password copied.\n'
        assert self.app.xerox.copied == [self.hashed_password]
        assert self.app.sleep.duration == 10
        self.app.atexit.call_all()
        assert self.app.xerox.copied == [self.hashed_password, '']

    def test_generate_copy_timeout_interrupted(self):
        self.app.xerox = FakeXerox()
        self.app.sleep = FakeSleep(interrupt=True)
        self.app.atexit = FakeAtexit()
        err = read_err(self.capsys, self.app, 'generate', 'example.com', '-C', '-w', '10')
        assert err == 'password copied.\n'
        assert self.app.xerox.copied == [self.hashed_password]
        assert self.app.sleep.duration == 10
        self.app.atexit.call_all()
        assert self.app.xerox.copied == [self.hashed_password, '']


    def test_entropy(self):
        out = read_out(self.capsys, self.app, 'entropy')
        # stripping trailing whitespace is a bitch
        out = '\n'.join(line.rstrip() for line in out.splitlines())
        assert out == """
                      site                         entropy (bits)

default                                                209.75
becu.org                                               190.53
fidelity.com                                           101.72
schwab.com                                              47.63
still.further.example.com                               39.68
further.example.com                                     26.58
example.com                                             26.58
%s        13.29""" % (self.hashed_site,)


    def test_site_hash_with_newline(self):
        out = read_out(self.capsys, self.app, 'site', 'hash', 'hashed.example.com')
        assert not self.confirmed_password
        assert out == self.hashed_site + '\n'

    def test_site_hash_no_newline(self):
        out = read_out(self.capsys, self.app, 'site', 'hash', '-n', 'hashed.example.com')
        assert out == self.hashed_site

    def test_site_hash_prompt_site(self):
        self.app.prompt = lambda _: 'hashed.example.com'
        out = read_out(self.capsys, self.app, 'site', 'hash')
        assert out == self.hashed_site + '\n'

    def test_site_hash_confirm_password(self):
        out = read_out(self.capsys, self.app, 'site', 'hash', 'hashed.example.com', '-c')
        assert self.confirmed_password
        assert out == self.hashed_site + '\n'

    def test_site_hash_overridden_method_keccak(self):
        out = read_out(self.capsys, self.app, 'site', 'hash', 'hashed.example.com', '-m', 'keccak')
        assert out == 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4\n'

    @pytest.mark.skipif("sys.version_info < (3,)")
    def test_site_hash_overridden_method_skein(self):
        out = read_out(self.capsys, self.app, 'site', 'hash', 'hashed.example.com', '-m', 'skein')
        assert out == 'UYfDoAN9nYMdxCYtgKenzjhbc9eonu3w92ec3SAA5UbT1J3L\n'


    def test_no_args(self):
        with pytest.raises(SystemExit) as excinfo:
            self.app.main([])
        assert excinfo_arg_0(excinfo) == 2


class SqliteTestCaseMixin(object):
    def test_schema_entropy(self):
        out = read_out(self.capsys, self.app, 'entropy', '--schema')
        out = '\n'.join(line.rstrip() for line in out.splitlines())
        assert out == """
 schema    entropy (bits)

schema_5       209.75
schema_3       190.53
schema_6       101.72
schema_1        47.63
schema_4        39.68
schema_2        26.58
schema_0        26.58
schema_7        13.29"""


class KeccakTestCaseMixin(ApplicationTestCaseMixin):
    hashed_site = 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4'
    hashed_password = 'Abaris abaisance abashedness abarticulation'

class KeccakYAMLTestCase(KeccakTestCaseMixin, unittest.TestCase):
    config_dir = 'keccak-yaml'

class KeccakSqliteTestCase(KeccakTestCaseMixin, SqliteTestCaseMixin, unittest.TestCase):
    config_dir = 'keccak-sqlite'


@pytest.mark.skipif("sys.version_info < (3,)")
class SkeinTestCaseMixin(ApplicationTestCaseMixin):
    hashed_site = 'UYfDoAN9nYMdxCYtgKenzjhbc9eonu3w92ec3SAA5UbT1J3L'
    hashed_password = 'abactor abattoir abashedly abaca'

class SkeinYAMLTestCase(SkeinTestCaseMixin, unittest.TestCase):
    config_dir = 'skein-yaml'

class SkeinSqliteTestCase(SkeinTestCaseMixin, SqliteTestCaseMixin, unittest.TestCase):
    config_dir = 'skein-sqlite'


@pytest.fixture
def app():
    app = application.Passacre()
    app.load_config(datadir.join('keccak.sqlite').open('rb'))
    return app

@pytest.fixture
def init_app(app, tmpdir):
    dbpath = tmpdir.join('test.db').strpath
    app.main(['init', dbpath])
    return dbpath, application.Passacre()

def test_init_config(init_app, capsys):
    dbpath, app = init_app
    out = read_out(capsys, app, '-f', dbpath, 'config')
    assert out == ''

def test_init_schemata(init_app, capsys):
    dbpath, app = init_app
    out = read_out(capsys, app, '-f', dbpath, 'schema')
    assert out == '32-printable: [[32, "printable"]]\n'

def test_init_sites(init_app, capsys):
    dbpath, app = init_app
    out = read_out(capsys, app, '-f', dbpath, 'site')
    assert out == 'default: 32-printable\n'


@pytest.fixture
def yaml2sqlite_app(tmpdir):
    tmp_app = application.Passacre()
    dbpath = tmpdir.join('test.db')
    tmp_app.main(
        ['init', '-y', datadir.join('keccak.yaml').strpath, dbpath.strpath])
    app = application.Passacre()
    app.load_config(dbpath.open('rb'))
    return app

def test_init_from_yaml(yaml2sqlite_app, capsys):
    app = yaml2sqlite_app
    assert read_out(capsys, app, 'schema') == """12-alphanumeric: [[12, "alphanumeric"]]
32-alphanumeric: [[32, "alphanumeric"]]
32-printable: [[32, "printable"]]
8-alphanumeric: [[8, "alphanumeric"]]
schema_0: ["printable", [", ", 4, "word"], "printable"]
schema_1: [[" ", 2, "word"]]
schema_2: [[" ", 4, "word"]]
schema_3: [[", ", 4, "word"]]
schema_4: [[16, ["alphanumeric", "\\"%'()+,-/:;<=>?\\\\ ^_|"]]]
"""

def test_init_from_yaml_sites(yaml2sqlite_app, capsys):
    app = yaml2sqlite_app
    assert read_out(capsys, app, 'site') == """gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4: schema_1
becu.org: 32-alphanumeric
default: 32-printable
example.com: schema_2
fhcrc.org: 32-printable
fidelity.com: schema_4
further.example.com: schema_3
schwab.com: 8-alphanumeric
still.further.example.com: schema_0
"""

def test_init_from_yaml_config(yaml2sqlite_app, capsys):
    app = yaml2sqlite_app
    assert read_out(capsys, app, 'config') == """site-hashing.enabled: true
site-hashing.iterations: 10
words-file: "words"
"""

def test_init_from_yaml_site_config(yaml2sqlite_app, capsys):
    app = yaml2sqlite_app
    assert read_out(capsys, app, 'config', '-s', 'default') == """iterations: 10
method: "keccak"
"""


def test_site_yaml(app, capsys):
    app.load_config(datadir.join('keccak.yaml').open('rb'))
    out = read_out(capsys, app, 'site')
    assert out == """gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4
becu.org
default
example.com
fhcrc.org
fidelity.com
further.example.com
schwab.com
still.further.example.com
"""

def test_site_sqlite(app, capsys):
    out = read_out(capsys, app, 'site')
    assert out == """gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4: schema_7
becu.org: schema_3
default: schema_5
example.com: schema_0
fhcrc.org: schema_5
fidelity.com: schema_6
further.example.com: schema_2
schwab.com: schema_1
still.further.example.com: schema_4
"""

def test_site_sqlite_by_schema(app, capsys):
    out = read_out(capsys, app, 'site', '--by-schema')
    assert out == """schema_0: example.com
schema_1: schwab.com
schema_2: further.example.com
schema_3: becu.org
schema_4: still.further.example.com
schema_5: default, fhcrc.org
schema_6: fidelity.com
schema_7: gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4
"""


def copy_app(name, app, tmpdir):
    datadir.join(name).copy(tmpdir)
    db_copy = tmpdir.join(name)
    app.load_config(db_copy.open('rb'))
    app._prompt_password = lambda confirm: 'passacre'
    return app

@pytest.fixture
def mutable_app(app, tmpdir):
    return copy_app('keccak.sqlite', app, tmpdir)

def test_site_add(mutable_app, capsys):
    app = mutable_app
    assert '\nexample.org: schema_0\n' not in read_out(capsys, app, 'site')
    app.main(['site', 'add', 'example.org', 'schema_0'])
    assert '\nexample.org: schema_0\n' in read_out(capsys, app, 'site')

def test_site_add_hashed(mutable_app, capsys):
    app = mutable_app
    assert 'KE76ybZ-sO7o4iS944E_mo_jTQPCjzifFjyRELZS-RuDbcGu: schema_0\n' not in read_out(
        capsys, app, 'site')
    app.main(['site', '-a', 'add', 'example.org', 'schema_0'])
    assert 'KE76ybZ-sO7o4iS944E_mo_jTQPCjzifFjyRELZS-RuDbcGu: schema_0\n' in read_out(
        capsys, app, 'site')

def test_site_set_schema(mutable_app, capsys):
    app = mutable_app
    assert '\nexample.com: schema_0\n' in read_out(capsys, app, 'site')
    app.main(['site', 'set-schema', 'example.com', 'schema_7'])
    assert '\nexample.com: schema_7\n' in read_out(capsys, app, 'site')

def test_site_set_schema_hashed(mutable_app, capsys):
    app = mutable_app
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4: schema_7\n' in read_out(capsys, app, 'site')
    app.main(['site', '-a', 'set-schema', 'hashed.example.com', 'schema_0'])
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4: schema_0\n' in read_out(capsys, app, 'site')

def test_site_remove(mutable_app, capsys):
    app = mutable_app
    assert '\nexample.com:' in read_out(capsys, app, 'site')
    app.main(['site', 'remove', 'example.com'])
    assert '\nexample.com:' not in read_out(capsys, app, 'site')

def test_site_remove_hashed(mutable_app, capsys):
    app = mutable_app
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4:' in read_out(capsys, app, 'site')
    app.main(['site', '-a', 'remove', 'hashed.example.com'])
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4:' not in read_out(capsys, app, 'site')

def test_site_remove_default_fails(app):
    with pytest.raises(SystemExit) as excinfo:
        app.main(['site', 'remove', 'default'])
    assert excinfo_arg_0(excinfo)

def test_config(app, capsys):
    out = read_out(capsys, app, 'config')
    assert out == """site-hashing.enabled: true
site-hashing.iterations: 10
words-file: "words"
"""

    out = read_out(capsys, app, 'config', 'site-hashing.enabled')
    assert out == 'true\n'

    out = read_out(capsys, app, 'config', 'site-hashing')
    assert out == '{"enabled": true, "iterations": 10}\n'

    out = read_out(capsys, app, 'config', '-s', 'fhcrc.org')
    assert out == 'increment: 5\n'

    out = read_out(capsys, app, 'config', '-s', 'fhcrc.org', 'increment')
    assert out == '5\n'

def get_config_name(app, capsys, site, name):
    args = ['config']
    if site is not None:
        args.extend(['-s', site])
    args.append(name)
    app.main(args)
    out, err = capsys.readouterr()
    assert not err
    return out.rstrip('\n')

def test_config_set(mutable_app, capsys):
    app = mutable_app

    app.main(['config', 'words-file', 'foo'])
    assert get_config_name(app, capsys, None, 'words-file') == '"foo"'

    app.main(['config', 'words-file', 'null'])
    assert get_config_name(app, capsys, None, 'words-file') == ''

    app.main(['config', 'spam.spam.spam.eggs', 'spam'])
    assert get_config_name(app, capsys, None, 'spam') == '{"spam": {"spam": {"eggs": "spam"}}}'

    app.main(['config', 'spam.spam.spam.spam', 'spam'])
    assert get_config_name(app, capsys, None, 'spam') == (
        '{"spam": {"spam": {"eggs": "spam", "spam": "spam"}}}')

    app.main(['config', '-s', 'fhcrc.org', 'increment', '6'])
    assert get_config_name(app, capsys, 'fhcrc.org', 'increment') == '6'

    app.main(['config', '-s', 'example.com', 'increment', '7'])
    assert get_config_name(app, capsys, 'example.com', 'increment') == '7'

    app.main(['config', '-as', 'hashed.example.com', 'increment', '8'])
    assert get_config_name(
        app, capsys, 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4', 'increment') == '8'

    app.main(['config', '-cas', 'hashed.example.com', 'increment', '9'])
    assert get_config_name(
        app, capsys, 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4', 'increment') == '9'

def test_schema(app, capsys):
    app.main(['schema'])
    out, err = capsys.readouterr()
    assert not err
    assert out == r"""schema_0: [[" ", 4, "word"]]
schema_1: [[8, "alphanumeric"]]
schema_2: [[", ", 4, "word"]]
schema_3: [[32, "alphanumeric"]]
schema_4: ["printable", [", ", 4, "word"], "printable"]
schema_5: [[32, "printable"]]
schema_6: [[16, ["alphanumeric", "\"%'()+,-/:;<=>?\\ ^_|"]]]
schema_7: [[" ", 2, "word"]]
"""

def test_schema_add(mutable_app, capsys):
    app = mutable_app
    app.main(['schema', 'add', 'schema_10', '["alphanumeric", [10, "digit"]]'])
    assert '\nschema_10: ["alphanumeric", [10, "digit"]]\n' in read_out(capsys, app, 'schema')

def test_schema_set_value(mutable_app, capsys):
    app = mutable_app
    app.main(['schema', 'set-value', 'schema_0', '[[10, "digit"]]'])
    assert 'schema_0: [[10, "digit"]]\n' in read_out(capsys, app, 'schema')

def test_schema_set_name(mutable_app, capsys):
    app = mutable_app
    assert 'schema_10:' not in read_out(capsys, app, 'schema')
    app.main(['schema', 'set-name', 'schema_0', 'schema_10'])
    assert 'schema_10:' in read_out(capsys, app, 'schema')

def test_schema_remove(mutable_app, capsys):
    app = mutable_app
    app.main(['schema', 'add', 'schema_10', '["alphanumeric", [10, "digit"]]'])
    assert 'schema_10:' in read_out(capsys, app, 'schema')
    app.main(['schema', 'remove', 'schema_10'])
    assert 'schema_10:' not in read_out(capsys, app, 'schema')


@pytest.fixture
def always_hash_app(app, tmpdir):
    return copy_app('always-hash.sqlite', app, tmpdir)

def test_always_hash_site_add(always_hash_app, capsys):
    app = always_hash_app
    assert 'KE76ybZ-sO7o4iS944E_mo_jTQPCjzifFjyRELZS-RuDbcGu: schema_0\n' not in read_out(
        capsys, app, 'site')
    app.main(['site', 'add', 'example.org', 'schema_0'])
    assert 'KE76ybZ-sO7o4iS944E_mo_jTQPCjzifFjyRELZS-RuDbcGu: schema_0\n' in read_out(
        capsys, app, 'site')

def test_always_hash_site_remove(always_hash_app, capsys):
    app = always_hash_app
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4: schema_7\n' in read_out(
        capsys, app, 'site')
    app.main(['site', 'remove', 'hashed.example.com'])
    assert 'gN7y2jQ72IbdvQZxrZLNmC4hrlDmB-KZnGJiGpoB4VEcOCn4' not in read_out(
        capsys, app, 'site')

def test_always_hash_site_config(always_hash_app, capsys):
    app = always_hash_app
    assert read_out(capsys, app, 'config', '-s', 'hashed.example.com') == 'foo: "bar"\n'
    app.main(['config', '-s', 'hashed.example.com', 'foo', 'spam'])
    assert read_out(capsys, app, 'config', '-s', 'hashed.example.com') == 'foo: "spam"\n'


@pytest.fixture
def always_confirm_app(app, tmpdir):
    app = copy_app('always-confirm.sqlite', app, tmpdir)
    def prompt_password(confirm):
        app._confirmed_password = confirm
        return 'passacre'
    app._prompt_password = prompt_password
    return app

def test_always_confirm_site_add(always_confirm_app):
    app = always_confirm_app
    app.main(['site', '-a', 'add', 'example.org', 'schema_0'])
    assert app._confirmed_password

def test_always_confirm_site_remove(always_confirm_app):
    app = always_confirm_app
    app.main(['site', '-a', 'remove', 'hashed.example.com'])
    assert app._confirmed_password

def test_always_confirm_site_config(always_confirm_app):
    app = always_confirm_app
    app.main(['config', '-a', '-s', 'hashed.example.com'])
    assert app._confirmed_password


@pytest.fixture
def nonextant_words_app(app, tmpdir):
    return copy_app('nonextant-words.sqlite', app, tmpdir)

def test_nonextant_words_warns(nonextant_words_app):
    app = nonextant_words_app
    with pytest.raises(ValueError):
        app.main(['generate', 'example.com'])
