import os
import pytest
from unittest.mock import patch
from pathlib import Path
import argparse
import asyncio
import shlex

from gita import __main__
from gita import utils, info, common
from conftest import (
    PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, PATH_FNAME_MAIN,
    async_mock, TEST_DIR,
)


@patch('gita.utils.get_repos', return_value={'aa'})
def test_group_name(_):
    got = __main__._group_name('xx')
    assert got == 'xx'
    with pytest.raises(SystemExit):
        __main__._group_name('aa')


class TestAdd:

    @pytest.mark.parametrize('input, expected', [
        (['add', '.'], ''),
        ])
    @patch('gita.common.get_config_fname')
    def test_add(self, mock_path_fname, tmp_path, input, expected):
        def side_effect(input, _=None):
            return tmp_path / f'{input}.txt'
        mock_path_fname.side_effect = side_effect
        utils.get_repos.cache_clear()
        __main__.main(input)
        utils.get_repos.cache_clear()
        got = utils.get_repos()
        assert len(got) == 1
        assert got['gita']['type'] == expected


@pytest.mark.parametrize('path_fname, expected', [
    (PATH_FNAME, ''),
    (PATH_FNAME_CLASH, "repo2: ['--haha', '--pp']\n"),
    ])
@patch('gita.utils.is_git', return_value=True)
@patch('gita.utils.get_groups', return_value={})
@patch('gita.common.get_config_fname')
def test_flags(mock_path_fname, _, __, path_fname, expected, capfd):
    mock_path_fname.return_value = path_fname
    utils.get_repos.cache_clear()
    __main__.main(['flags'])
    out, err = capfd.readouterr()
    assert err == ''
    assert out == expected


class TestLsLl:
    @patch('gita.common.get_config_fname')
    def test_ll(self, mock_path_fname, capfd, tmp_path):
        """
        functional test
        """
        # avoid modifying the local configuration
        def side_effect(input, _=None):
            return tmp_path / f'{input}.txt'
        mock_path_fname.side_effect = side_effect
        utils.get_repos.cache_clear()
        __main__.main(['add', '.'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'Found 1 new repo(s).\n' == out

        # in production this is not needed
        utils.get_repos.cache_clear()

        __main__.main(['ls'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'gita\n' == out

        __main__.main(['ll'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'gita' in out
        assert info.Color.end in out

        # no color on branch name
        __main__.main(['ll', '-C'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'gita' in out
        assert info.Color.end not in out

        __main__.main(['ls', 'gita'])
        out, err = capfd.readouterr()
        assert err == ''
        assert out.strip() == utils.get_repos()['gita']['path']

    def test_ls(self, monkeypatch, capfd):
        monkeypatch.setattr(utils, 'get_repos',
                lambda: {'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}})
        monkeypatch.setattr(utils, 'describe', lambda x: x)
        __main__.main(['ls'])
        out, err = capfd.readouterr()
        assert err == ''
        assert out == "repo1 repo2\n"
        __main__.main(['ls', 'repo1'])
        out, err = capfd.readouterr()
        assert err == ''
        assert out == '/a/\n'

    @pytest.mark.parametrize('path_fname, expected', [
        (PATH_FNAME,
         "repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \nxxx   cmaster dsu\x1b[0m msg \n"),
        (PATH_FNAME_EMPTY, ""),
        (PATH_FNAME_MAIN,
         '\x1b[4mmain1\x1b[0m cmaster dsu\x1b[0m msg \nxx    cmaster dsu\x1b[0m msg \n'),
        (PATH_FNAME_CLASH,
         "repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \n"
         ),
    ])
    @patch('gita.utils.is_git', return_value=True)
    @patch('gita.info.get_head', return_value="master")
    @patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c"))
    @patch('gita.info.get_commit_msg', return_value="msg")
    @patch('gita.info.get_commit_time', return_value="")
    @patch('gita.common.get_config_fname')
    def test_with_path_files(self, mock_path_fname, _0, _1, _2, _3, _4, path_fname,
                          expected, capfd):
        def side_effect(input, _=None):
            if input == 'repos.csv':
                return path_fname
            return f'/{input}'
        mock_path_fname.side_effect = side_effect
        utils.get_repos.cache_clear()
        __main__.main(['ll'])
        out, err = capfd.readouterr()
        print(out)
        assert err == ''
        assert out == expected


@pytest.mark.parametrize('input, expected', [
    ({'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}}, ''),
    ])
@patch('subprocess.run')
@patch('gita.utils.get_repos')
def test_freeze(mock_repos, mock_run, input, expected, capfd):
    mock_repos.return_value = input
    __main__.main(['freeze'])
    assert mock_run.call_count == 2
    out, err = capfd.readouterr()
    assert err == ''
    assert out == expected


@patch('gita.utils.parse_clone_config', return_value=[
    ['git@github.com:user/repo.git', 'repo', '/a/repo']])
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
def test_clone(*_):
    asyncio.set_event_loop(asyncio.new_event_loop())
    args = argparse.Namespace()
    args.fname = ['freeze_filename']
    args.preserve_path = None
    __main__.f_clone(args)
    mock_run = utils.run_async.mock
    assert mock_run.call_count == 1
    cmds = ['git', 'clone', 'git@github.com:user/repo.git']
    mock_run.assert_called_once_with('repo', Path.cwd(), cmds)


@patch('gita.utils.parse_clone_config', return_value=[
    ['git@github.com:user/repo.git', 'repo', '/a/repo']])
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
def test_clone_with_preserve_path(*_):
    asyncio.set_event_loop(asyncio.new_event_loop())
    args = argparse.Namespace()
    args.fname = ['freeze_filename']
    args.preserve_path = True
    __main__.f_clone(args)
    mock_run = utils.run_async.mock
    assert mock_run.call_count == 1
    cmds = ['git', 'clone', 'git@github.com:user/repo.git', '/a/repo']
    mock_run.assert_called_once_with('repo', Path.cwd(), cmds)


@patch('os.path.isfile', return_value=True)
@patch('gita.common.get_config_fname', return_value='some path')
@patch('gita.utils.get_repos', return_value={'repo1': {'path': '/a/', 'type': ''},
                                             'repo2': {'path': '/b/', 'type': ''}})
@patch('gita.utils.write_to_repo_file')
def test_rm(mock_write, *_):
    args = argparse.Namespace()
    args.repo = ['repo1']
    __main__.f_rm(args)
    mock_write.assert_called_once_with(
            {'repo2': {'path': '/b/', 'type': ''}}, 'w')


def test_not_add():
    # this won't write to disk because the repo is not valid
    __main__.main(['add', '/home/some/repo/'])


@patch('gita.utils.get_repos', return_value={'repo2': {'path': '/d/efg',
                    'flags': []}})
@patch('subprocess.run')
def test_fetch(mock_run, *_):
    asyncio.set_event_loop(asyncio.new_event_loop())
    __main__.main(['fetch'])
    mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg', shell=False)


@patch(
    'gita.utils.get_repos', return_value={
        'repo1': {'path': '/a/bc', 'flags': []},
        'repo2': {'path': '/d/efg', 'flags': []}
    })
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
def test_async_fetch(*_):
    __main__.main(['fetch'])
    mock_run = utils.run_async.mock
    assert mock_run.call_count == 2
    cmds = ['git', 'fetch']
    # print(mock_run.call_args_list)
    mock_run.assert_any_call('repo1', '/a/bc', cmds)
    mock_run.assert_any_call('repo2', '/d/efg', cmds)


@pytest.mark.parametrize('input', [
    'diff --name-only --staged',
    "commit -am 'lala kaka'",
])
@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}})
@patch('subprocess.run')
def test_superman(mock_run, _, input):
    mock_run.reset_mock()
    args = ['super', 'repo7'] + shlex.split(input)
    __main__.main(args)
    expected_cmds = ['git'] + shlex.split(input)
    mock_run.assert_called_once_with(expected_cmds, cwd='path7', shell=False)


@pytest.mark.parametrize('input', [
    'diff --name-only --staged',
    "commit -am 'lala kaka'",
])
@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}})
@patch('subprocess.run')
def test_shell(mock_run, _, input):
    mock_run.reset_mock()
    args = ['shell', 'repo7', input]
    __main__.main(args)
    expected_cmds = input
    mock_run.assert_called_once_with(expected_cmds, cwd='path7', shell=True, stderr=-2, stdout=-1)


class TestContext:

    @patch('gita.utils.get_context', return_value=None)
    def test_display_no_context(self, _, capfd):
        __main__.main(['context'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'Context is not set\n' == out

    @patch('gita.utils.get_context', return_value=Path('gname.context'))
    @patch('gita.utils.get_groups', return_value={'gname': {'repos': ['a', 'b']}})
    def test_display_context(self, _, __, capfd):
        __main__.main(['context'])
        out, err = capfd.readouterr()
        assert err == ''
        assert 'gname: a b\n' == out

    @patch('gita.utils.get_context')
    def test_reset(self, mock_ctx):
        __main__.main(['context', 'none'])
        mock_ctx.return_value.unlink.assert_called()

    @patch('gita.utils.get_context', return_value=None)
    @patch('gita.common.get_config_dir', return_value=TEST_DIR)
    @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
    def test_set_first_time(self, *_):
        ctx = TEST_DIR / 'lala.context'
        assert not ctx.is_file()
        __main__.main(['context', 'lala'])
        assert ctx.is_file()
        ctx.unlink()

    @patch('gita.common.get_config_dir', return_value=TEST_DIR)
    @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
    @patch('gita.utils.get_context')
    def test_set_second_time(self, mock_ctx, *_):
        __main__.main(['context', 'kaka'])
        mock_ctx.return_value.rename.assert_called()


class TestGroupCmd:

    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    def test_ls(self, _, capfd):
        args = argparse.Namespace()
        args.to_group = None
        args.group_cmd = 'ls'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        out, err = capfd.readouterr()
        assert err == ''
        assert 'xx yy\n' == out

    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    def test_ll(self, _, capfd):
        args = argparse.Namespace()
        args.to_group = None
        args.group_cmd = None
        args.to_show = None
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        out, err = capfd.readouterr()
        assert err == ''
        assert out == '\x1b[4mxx\x1b[0m: \n  - a\n  - b\n\x1b[4myy\x1b[0m: \n  - a\n  - c\n  - d\n'

    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    def test_ll_with_group(self, _, capfd):
        args = argparse.Namespace()
        args.to_group = None
        args.group_cmd = None
        args.to_show = 'yy'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        out, err = capfd.readouterr()
        assert err == ''
        assert 'a c d\n' == out

    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    @patch('gita.utils.write_to_groups_file')
    def test_rename(self, mock_write, _):
        args = argparse.Namespace()
        args.gname = 'xx'
        args.new_name = 'zz'
        args.group_cmd = 'rename'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        expected = {'yy': {'repos': ['a', 'c', 'd'], 'path': ''},
                    'zz': {'repos': ['a', 'b'], 'path': ''}}
        mock_write.assert_called_once_with(expected, 'w')

    @patch('gita.info.get_color_encoding', return_value=info.default_colors)
    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    def test_rename_error(self, *_):
        utils.get_groups.cache_clear()
        with pytest.raises(SystemExit, match="1"):
            __main__.main('group rename xx yy'.split())

    @pytest.mark.parametrize('input, expected', [
        ('xx', {'yy': {'repos': ['a', 'c', 'd'], 'path': ''}}),
        ("xx yy", {}),
    ])
    @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    @patch('gita.utils.write_to_groups_file')
    def test_rm(self, mock_write, _, __, input, expected):
        utils.get_groups.cache_clear()
        args = ['group', 'rm'] + shlex.split(input)
        __main__.main(args)
        mock_write.assert_called_once_with(expected, 'w')

    @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    @patch('gita.utils.write_to_groups_file')
    def test_add(self, mock_write, *_):
        args = argparse.Namespace()
        args.to_group =  ['a', 'c']
        args.group_cmd = 'add'
        args.gname = 'zz'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        mock_write.assert_called_once_with(
                {'zz': {'repos': ['a', 'c'], 'path': ''}}, 'a+')

    @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    @patch('gita.utils.write_to_groups_file')
    def test_add_to_existing(self, mock_write, *_):
        args = argparse.Namespace()
        args.to_group =  ['a', 'c']
        args.group_cmd = 'add'
        args.gname = 'xx'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        mock_write.assert_called_once_with(
                {'xx': {'repos': ['a', 'b', 'c'], 'path': ''},
                 'yy': {'repos': ['a', 'c', 'd'], 'path': ''}}, 'w')

    @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
    @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
    @patch('gita.utils.write_to_groups_file')
    def test_rm_repo(self, mock_write, *_):
        args = argparse.Namespace()
        args.to_rm = ['a', 'c']
        args.group_cmd = 'rmrepo'
        args.gname = 'xx'
        utils.get_groups.cache_clear()
        __main__.f_group(args)
        mock_write.assert_called_once_with(
                {'xx': {'repos': ['b'], 'path': ''},
                 'yy': {'repos': ['a', 'c', 'd'], 'path': ''}}, 'w')

    @patch('gita.common.get_config_fname')
    def test_integration(self, mock_path_fname, tmp_path, capfd):
        def side_effect(input, _=None):
            return tmp_path / f'{input}.csv'
        mock_path_fname.side_effect = side_effect

        __main__.main('add .'.split())
        utils.get_repos.cache_clear()
        __main__.main('group add gita -n test'.split())
        utils.get_groups.cache_clear()
        __main__.main('ll test'.split())
        out, err = capfd.readouterr()
        assert err == ''
        assert 'gita' in out


@patch('gita.utils.is_git', return_value=True)
@patch('gita.common.get_config_fname', return_value=PATH_FNAME)
@patch('gita.utils.rename_repo')
def test_rename(mock_rename, _, __):
    utils.get_repos.cache_clear()
    args = ['rename', 'repo1', 'abc']
    __main__.main(args)
    mock_rename.assert_called_once_with(
            {'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []},
                'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []},
                'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}},
        'repo1', 'abc')


class TestInfo:

    @patch('gita.common.get_config_fname', return_value='')
    def test_ll(self, _, capfd):
        args = argparse.Namespace()
        args.info_cmd = None
        __main__.f_info(args)
        out, err = capfd.readouterr()
        assert 'In use: branch,commit_msg,commit_time\nUnused: path\n' == out
        assert err == ''

    @patch('gita.common.get_config_fname')
    def test_add(self, mock_get_fname, tmpdir):
        args = argparse.Namespace()
        args.info_cmd = 'add'
        args.info_item = 'path'
        with tmpdir.as_cwd():
            csv_config = Path.cwd() / 'info.csv'
            mock_get_fname.return_value = csv_config
            __main__.f_info(args)
            items = info.get_info_items()
        assert items == ['branch', 'commit_msg', 'commit_time', 'path']

    @patch('gita.common.get_config_fname')
    def test_rm(self, mock_get_fname, tmpdir):
        args = argparse.Namespace()
        args.info_cmd = 'rm'
        args.info_item = 'commit_msg'
        with tmpdir.as_cwd():
            csv_config = Path.cwd() / 'info.csv'
            mock_get_fname.return_value = csv_config
            __main__.f_info(args)
            items = info.get_info_items()
        assert items == ['branch', 'commit_time']


@patch('gita.common.get_config_fname')
def test_set_color(mock_get_fname, tmpdir):
        args = argparse.Namespace()
        args.color_cmd = 'set'
        args.color = 'b_white'
        args.situation = 'no-remote'
        with tmpdir.as_cwd():
            csv_config = Path.cwd() / 'colors.csv'
            mock_get_fname.return_value = csv_config
            __main__.f_color(args)

            info.get_color_encoding.cache_clear()  # avoid side effect
            items = info.get_color_encoding()
        info.get_color_encoding.cache_clear()  # avoid side effect
        assert items == {'no-remote': 'b_white', 'in-sync': 'green',
                'diverged': 'red', 'local-ahead': 'purple',
                'remote-ahead': 'yellow'}
