# coding=utf-8
from __future__ import unicode_literals

import os
import time
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

import mock
import pytest
import yaml
from ship_it.manifest import Manifest, get_manifest_from_path


@pytest.fixture
def manifest_file():
    return """
version: 0.1.0
name: ship_it
"""



def test_sanity(tmpdir, manifest_file, manifest):
    """
    Does our mock manifest contents evaluate the same as a file?
    """
    _file = tmpdir.join('manifest.yaml')
    _file.write(manifest_file)
    assert get_manifest_from_path(str(_file)).contents == manifest.contents


@mock.patch('yaml.load')
@mock.patch('ship_it.manifest.Manifest.get_manifest_fobj')
def test_open_for_yaml(mock_fobj, mock_load):
    assert not mock_fobj.called
    assert not mock_load.called
    Manifest('some path')
    mock_fobj.assert_called_once_with(os.path.abspath('some path'))
    mock_load.assert_called_once_with(mock_fobj.return_value,
                                      Loader=yaml.BaseLoader)


@pytest.mark.parametrize('path', [
    '/home/test/manifest.yaml',
    '~/manifest.yaml',
    'manifest.yaml',
])
@mock.patch('ship_it.manifest.Manifest.get_manifest_content_from_path')
def test_manifest_normalizes_path(mock_content, path):
    """
    Test that the resulting manifest has the path it came from
    """
    assert not mock_content.called
    _man = Manifest(path)
    mock_content.assert_called_once_with(Manifest.normalize_path(path))
    assert _man.path
    assert os.path.isabs(_man.path)
    assert _man.manifest_dir
    assert os.path.isabs(_man.manifest_dir)
    assert '~' not in _man.path
    assert '~' not in _man.manifest_dir


float_version1 = """
version: 0.2
name: ship_it
"""

float_version2 = """
version: 0.20
name: ship_it
"""

string_version = """
version: '0.20'
name: ship_it
"""

non_number_version = """
version: abc
name: ship_it
"""

three_part_version = """
version: 0.2.30
name: ship_it
"""

@pytest.mark.parametrize('manifest_content,expected_version', [
    (float_version1, '0.2'),
    (float_version2, '0.20'),
    (string_version, '0.20'),
    (non_number_version, 'abc'),
    (three_part_version, '0.2.30'),
])
def test_manifest_content_from_path(manifest_content, expected_version):
    with mock.patch('ship_it.manifest.Manifest.get_manifest_fobj') as get:
        get.return_value = StringIO(manifest_content)
        manifest_output = Manifest.get_manifest_content_from_path(None)
        assert manifest_output['version'] == expected_version


def test_override_virtualenv_name():
    """
    Test that there's a distinction between name and virtualenv name
    """
    test_man = Manifest('manifest.yaml', manifest_contents=dict(
        name='prefix-ship_it',
        virtualenv_name='ship_it'
    ))
    assert test_man.virtualenv_name == 'ship_it'
    assert test_man.remote_virtualenv_path == '/opt/ship_it'
    assert test_man.name == 'prefix-ship_it'


@pytest.mark.parametrize('val,expected', [
    ('true', True),
    ('yes', True),
    ('on', True),
    ('y', True),
    ('', False),
    ('false', False),
])
def test_upgrade_pip(val, expected):
    """
    Test that upgrade pip is available
    """
    test_man = Manifest('manifest.yaml', manifest_contents=dict(
        upgrade_pip=val
    ))
    assert test_man.upgrade_pip is expected

@pytest.mark.parametrize('val,expected', [
    ('true', True),
    ('yes', True),
    ('on', True),
    ('y', True),
    ('', False),
    ('false', False),
])
def test_upgrade_wheel(val, expected):
    """
    Test that upgrade wheel is available
    """
    test_man = Manifest('manifest.yaml', manifest_contents=dict(
        upgrade_wheel=val
    ))
    assert test_man.upgrade_wheel is expected

class TestPackagePath(object):
    """
    Test things related to figuring out the package path
    """
    def test_default_to_virtualenv_name(self):
        """
        Test that if unspecified, it uses the virtualenv name and the manifest
        directory
        """
        test_man = Manifest('/path/manifest.yaml', manifest_contents=dict(
            name='prefix-ship_it',
            virtualenv_name='ship_it'
        ))
        assert test_man.virtualenv_name == 'ship_it'
        assert test_man.local_package_path == '/path/ship_it'

    @pytest.mark.parametrize('pkg_path,expected', [
        # we respect absolute paths
        ('/abs/path/to/pkg', '/abs/path/to/pkg'),
        # paths are relative to manifest directory
        ('pkg', '/path/pkg'),
        ('../pkg', '/pkg'),
    ])
    def test_override(self, pkg_path, expected):
        """
        Test that if unspecified, it uses the virtualenv name and the manifest
        directory
        """
        test_man = Manifest('/path/manifest.yaml', manifest_contents=dict(
            name='ship_it',
            local_package_path=pkg_path
        ))

        assert test_man.local_package_path == expected


class TestRemotePackagePath(object):
    """
    Test things related to figuring out the remote package path
    """
    def test_default_to_virtualenv_name(self):
        """
        Test that if unspecified, it uses the virtualenv name and /opt
        """
        test_man = Manifest('/path/manifest.yaml', manifest_contents=dict(
            name='prefix-ship_it',
            virtualenv_name='ship_it'
        ))
        assert test_man.virtualenv_name == 'ship_it'
        assert test_man.remote_package_path == '/opt'
        assert test_man.remote_virtualenv_path == '/opt/ship_it'

    @pytest.mark.parametrize('pkg_path,expected', [
        # we respect absolute paths
        ('/usr/local/share', '/usr/local/share/pkg'),
        ('/usr/local/share/', '/usr/local/share/pkg'),
        # relative paths are relative to pkg_location
        ('company_name', '/opt/company_name/pkg'),
        ('company_name/', '/opt/company_name/pkg'),
    ])
    def test_override(self, pkg_path, expected):
        """
        Test that if unspecified, it uses the virtualenv name and the manifest
        directory
        """
        test_man = Manifest('/path/manifest.yaml', manifest_contents=dict(
            name='ship_it',
            virtualenv_name='pkg',
            remote_package_path=pkg_path
        ))

        assert test_man.virtualenv_name == 'pkg'
        assert test_man.remote_virtualenv_path == expected


class TestGettingArgsAndFlags(object):
    """
    Test the specifics of getting command line flags and args from a manifest
    """
    @pytest.mark.parametrize('test_man, expected', [
        # We just set up a bunch of flags with whatever we get
        (dict(name='test'), [('name', 'test')]),
        (dict(version='test'), [('version', 'test')]),
        (dict(iteration='test'), [('iteration', 'test')]),
        (dict(description='test'), [('description', 'test')]),
        (dict(epoch=1234), [('epoch', 1234)]),
        # We do do some normalization on some of them
        (dict(before_install='test'), [('before-install', '/test_dir/test')]),
        (dict(after_install='test'), [('after-install', '/test_dir/test')]),
        (dict(before_install='/test'), [('before-install', '/test')]),
        (dict(after_install='/test'), [('after-install', '/test')]),
        # # mix and match
        (dict(after_install='/test', name='test'),
        [('after-install', '/test'), ('name', 'test')]),
    ])
    def test_single_flags(self, manifest, test_man, expected):
        # just test the subset of things we care about
        manifest.contents = test_man
        assert sorted(manifest.get_single_flags()) == sorted(expected)

    @pytest.mark.parametrize('epoch_value', [
        'TIMESTAMP',
        'timestamp',
        'TimeStamp',
    ])
    def test_epoch_flag(self, manifest, epoch_value):
        """
        Test generating a timestamp for the epoch value.
        """
        manifest.contents = {'epoch': epoch_value}
        curtime = int(time.time())
        assert int(manifest.get_single_flags()[0][1]) in range(curtime, curtime + 10)

    @pytest.mark.parametrize('test_cfg, expected_args, expected_flags', [
        # we don't have to return config files
        ({}, [], []),
        # some paths relative to the resulting venv
        ({'etc/settings.py': 'examples/settings.py'},
         ['/test_dir/examples/settings.py=/opt/ship_it/etc/'],
         [('config-files', 'opt/ship_it/etc/settings.py')]),
        # some absolute paths
        ({'/etc/ship_it/settings.cfg': 'packaging/rpm/settings.cfg'},
         ['/test_dir/packaging/rpm/settings.cfg=/etc/ship_it/'],
         [('config-files', 'etc/ship_it/settings.cfg')]),
        # more than one config file
        ({'/etc/ship_it/settings.cfg': 'packaging/rpm/settings.cfg',
          'etc/settings.py': 'examples/settings.py'},
         ['/test_dir/packaging/rpm/settings.cfg=/etc/ship_it/',
          '/test_dir/examples/settings.py=/opt/ship_it/etc/'],
         [('config-files', 'etc/ship_it/settings.cfg'),
          ('config-files', 'opt/ship_it/etc/settings.py')]),
    ])
    def test_config(self, manifest, test_cfg, expected_args, expected_flags):
        """
        Test getting the right arguments and flags from config files.
        """
        manifest.contents.update({'config_files': test_cfg})
        actual_args, actual_flags = manifest.get_config_args_and_flags()
        assert sorted(actual_args) == sorted(expected_args)
        assert sorted(actual_flags) == sorted(expected_flags)

    @pytest.mark.parametrize('test_extra_files, expected_args', [
        # we don't have to have extra files
        ({}, []),
        # relative path files
        ({'content/important.txt': 'src/important.txt'},
         ['/test_dir/src/important.txt=/opt/ship_it/content/']),
        # relative path directories
        ({'content': 'content/'},
         ['/test_dir/content=/opt/ship_it/']),
        # absolute path files
        ({'/etc/ship_it/important.txt': 'external/important.txt'},
         ['/test_dir/external/important.txt=/etc/ship_it/']),
        # absolute path directores
        ({'/etc/ship_it/content': 'external/content/'},
         ['/test_dir/external/content=/etc/ship_it/']),
        # more than one extra file
        ({'/etc/ship_it/important.txt': 'external/important.txt',
          'content/important.txt': 'src/important.txt'},
         ['/test_dir/external/important.txt=/etc/ship_it/',
          '/test_dir/src/important.txt=/opt/ship_it/content/']),
    ])
    def test_extra_files(self, manifest, test_extra_files, expected_args):
        """
        Test getting the right arguments from extra files/
        """
        manifest.contents.update({'extra_files': test_extra_files})
        actual_args = manifest.get_extra_files_args()
        assert sorted(actual_args) == sorted(expected_args)

    @pytest.mark.parametrize('dependency_type,value,expected', [
        # you can specify nothing, and it's fine
        ('depends', [], []),
        # depends uses the `--depends` flag and respects versions
        ('depends', ['foo', 'bar == 2.0'], [
            ('depends', 'foo'),
            ('depends', 'bar == 2.0'),
        ]),
    ])
    def test_requirements(self, manifest, dependency_type, value, expected):
        """
        Test that we correctly set the dependency flags for requirements
        """
        manifest.contents[dependency_type] = value

        assert sorted(manifest.get_dependency_flags()) == sorted(expected)


    @pytest.mark.parametrize('value,exclude_compiled,expected', [
        # you can specify nothing, and it's fine
        ([], 'false', []),
        (['**.pyc','*.pyo'], 'false', [
            ('exclude', '**.pyc'),
            ('exclude', '*.pyo'),
        ]),
        (['**/__pycache__', '*.py[co]'], 'true', [
            ('exclude', '**/__pycache__'),
            ('exclude', '*.py[co]'),
            ('exclude', '__pycache__'),
        ]),
        ([], 'true', [
            ('exclude', '*.py[co]'),
            ('exclude', '__pycache__'),
        ])
    ])
    def test_excludes(self, manifest, value, exclude_compiled, expected):
        """
        Test that we correctly get exclude flags
        """
        manifest.contents['exclude'] = value
        manifest.contents['exclude_compiled'] = exclude_compiled

        assert sorted(manifest.get_exclude_flags()) == sorted(expected)

    @pytest.mark.parametrize('value,expected', [
        ('YES', True),
        ('y', True),
        ('Y', True),
        ('True', True),
        ('true', True),
        ('On', True),
        ('ON', True),
        ('NO', False),
        ('n', False),
        ('False', False),
        ('false', False),
        ('Off', False),
        ('OFF', False),
        ('off', False),
    ])
    def test_get_bool_value(self, manifest, value, expected):
        manifest.contents['test'] = value
        assert manifest.get_bool_value('test') is expected

    @mock.patch('ship_it.manifest.Manifest.remote_package_path', new_callable=mock.PropertyMock)
    def test_remote_pkg_path_arg(self, mock_remote_pkg_path, manifest):
        """
        Test that the value returned from remote_package_path is set in fpm
        """
        destination = '/usr/local/share'
        mock_remote_pkg_path.return_value = destination
        assert manifest.get_args_and_flags()[0][0] == '/test_dir/build/ship_it={}'.format(destination)

    @pytest.mark.parametrize('cfg_update,expected_user,expected_group', [
        ({},                'ship_it', 'ship_it'),
        ({'user': 'root'},  'root',    'ship_it'),
        ({'group': 'root'}, 'ship_it', 'root'),
        ({'user': 'root',
          'group': 'root'}, 'root', 'root'),
    ])
    @mock.patch('ship_it.manifest.Manifest.get_config_args_and_flags')
    @mock.patch('ship_it.manifest.Manifest.get_extra_files_args')
    @mock.patch('ship_it.manifest.Manifest.get_single_flags')
    @mock.patch('ship_it.manifest.Manifest.get_dependency_flags')
    @mock.patch('ship_it.manifest.Manifest.get_exclude_flags')
    def test_get_overall_args(self, mock_excludes, mock_depends, mock_single,
                              mock_extra, mock_cfg,  manifest, cfg_update, expected_user, expected_group):
        # Return values must be set here, rather than in the decorator,
        # to reset their state for each iteration.
        mock_excludes.return_value = [('exclude', '**.pyc'), ('exclude', '**.pyo')]
        mock_depends.return_value = [('depends', 'weird-dependency == 0.1')]
        mock_single.return_value = [('single', 'flag')]
        mock_cfg.return_value = (['cfg arg'], [('cfg', 'flag')])
        mock_extra.return_value = ['extra=arg']

        expected_args = [
            'cfg arg',
            'extra=arg',
            '/test_dir/build/ship_it=/opt'
        ]
        expected_flags = [
            ('single', 'flag'),
            ('cfg', 'flag'),
            ('rpm-user', expected_user),
            ('rpm-group', expected_group),
            ('directories', '/opt/ship_it'),
            ('depends', 'weird-dependency == 0.1'),
            ('exclude', '**.pyc'),
            ('exclude', '**.pyo'),
        ]
        manifest.contents.update(cfg_update)
        actual_args, actual_flags = manifest.get_args_and_flags()
        assert sorted(actual_args) == sorted(expected_args)
        assert sorted(actual_flags) == sorted(expected_flags)
