from __future__ import annotations

import argparse
import os.path
from unittest import mock

import pytest

import pre_commit.constants as C
from pre_commit import main
from pre_commit.errors import FatalError
from pre_commit.util import cmd_output
from testing.auto_namedtuple import auto_namedtuple
from testing.util import cwd


def _args(**kwargs):
    kwargs.setdefault('command', 'help')
    kwargs.setdefault('config', C.CONFIG_FILE)
    if kwargs['command'] in {'run', 'try-repo'}:
        kwargs.setdefault('commit_msg_filename', None)
    return argparse.Namespace(**kwargs)


def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir):
    with pytest.raises(FatalError):
        main._adjust_args_and_chdir(_args())


def test_adjust_args_and_chdir_noop(in_git_dir):
    args = _args(command='run', files=['f1', 'f2'])
    main._adjust_args_and_chdir(args)
    assert os.getcwd() == in_git_dir
    assert args.config == C.CONFIG_FILE
    assert args.files == ['f1', 'f2']


def test_adjust_args_and_chdir_relative_things(in_git_dir):
    in_git_dir.join('foo/cfg.yaml').ensure()
    with in_git_dir.join('foo').as_cwd():
        args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml')
        main._adjust_args_and_chdir(args)
        assert os.getcwd() == in_git_dir
        assert args.config == os.path.join('foo', 'cfg.yaml')
        assert args.files == [
            os.path.join('foo', 'f1'),
            os.path.join('foo', 'f2'),
        ]


def test_adjust_args_and_chdir_relative_commit_msg(in_git_dir):
    in_git_dir.join('foo/cfg.yaml').ensure()
    with in_git_dir.join('foo').as_cwd():
        args = _args(command='run', files=[], commit_msg_filename='t.txt')
        main._adjust_args_and_chdir(args)
        assert os.getcwd() == in_git_dir
        assert args.commit_msg_filename == os.path.join('foo', 't.txt')


@pytest.mark.skipif(os.name != 'nt', reason='windows feature')
def test_install_on_subst(in_git_dir, store):  # pragma: posix no cover
    assert not os.path.exists('Z:')
    cmd_output('subst', 'Z:', str(in_git_dir))
    try:
        with cwd('Z:'):
            test_adjust_args_and_chdir_noop('Z:\\')
    finally:
        cmd_output('subst', '/d', 'Z:')


def test_adjust_args_and_chdir_non_relative_config(in_git_dir):
    with in_git_dir.join('foo').ensure_dir().as_cwd():
        args = _args()
        main._adjust_args_and_chdir(args)
        assert os.getcwd() == in_git_dir
        assert args.config == C.CONFIG_FILE


def test_adjust_args_try_repo_repo_relative(in_git_dir):
    with in_git_dir.join('foo').ensure_dir().as_cwd():
        args = _args(command='try-repo', repo='../foo', files=[])
        assert args.repo is not None
        assert os.path.exists(args.repo)
        main._adjust_args_and_chdir(args)
        assert os.getcwd() == in_git_dir
        assert os.path.exists(args.repo)
        assert args.repo == 'foo'


FNS = (
    'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks',
    'migrate_config', 'run', 'sample_config', 'uninstall',
    'validate_config', 'validate_manifest',
)
CMDS = tuple(fn.replace('_', '-') for fn in FNS)


@pytest.fixture
def mock_commands():
    mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS}
    ret = auto_namedtuple(**mcks)
    yield ret
    for mck in ret:
        mck.stop()


@pytest.fixture
def argparse_parse_args_spy():
    parse_args_mock = mock.Mock()

    original_parse_args = argparse.ArgumentParser.parse_args

    def fake_parse_args(self, args):
        # call our spy object
        parse_args_mock(args)
        return original_parse_args(self, args)

    with mock.patch.object(
        argparse.ArgumentParser, 'parse_args', fake_parse_args,
    ):
        yield parse_args_mock


def assert_only_one_mock_called(mock_objs):
    total_call_count = sum(mock_obj.call_count for mock_obj in mock_objs)
    assert total_call_count == 1


def test_overall_help(mock_commands):
    with pytest.raises(SystemExit):
        main.main(['--help'])


def test_help_command(mock_commands, argparse_parse_args_spy):
    with pytest.raises(SystemExit):
        main.main(['help'])

    argparse_parse_args_spy.assert_has_calls([
        mock.call(['help']),
        mock.call(['--help']),
    ])


def test_help_other_command(mock_commands, argparse_parse_args_spy):
    with pytest.raises(SystemExit):
        main.main(['help', 'run'])

    argparse_parse_args_spy.assert_has_calls([
        mock.call(['help', 'run']),
        mock.call(['run', '--help']),
    ])


@pytest.mark.parametrize('command', CMDS)
def test_all_cmds(command, mock_commands, mock_store_dir):
    main.main((command,))
    assert getattr(mock_commands, command.replace('-', '_')).call_count == 1
    assert_only_one_mock_called(mock_commands)


def test_try_repo(mock_store_dir):
    with mock.patch.object(main, 'try_repo') as patch:
        main.main(('try-repo', '.'))
    assert patch.call_count == 1


def test_init_templatedir(mock_store_dir):
    with mock.patch.object(main, 'init_templatedir') as patch:
        main.main(('init-templatedir', 'tdir'))

    assert patch.call_count == 1
    assert 'tdir' in patch.call_args[0]
    assert patch.call_args[1]['hook_types'] is None
    assert patch.call_args[1]['skip_on_missing_config'] is True


def test_init_templatedir_options(mock_store_dir):
    args = (
        'init-templatedir',
        'tdir',
        '--hook-type',
        'commit-msg',
        '--no-allow-missing-config',
    )
    with mock.patch.object(main, 'init_templatedir') as patch:
        main.main(args)

    assert patch.call_count == 1
    assert 'tdir' in patch.call_args[0]
    assert patch.call_args[1]['hook_types'] == ['commit-msg']
    assert patch.call_args[1]['skip_on_missing_config'] is False


def test_help_cmd_in_empty_directory(
        in_tmpdir,
        mock_commands,
        argparse_parse_args_spy,
):
    with pytest.raises(SystemExit):
        main.main(['help', 'run'])

    argparse_parse_args_spy.assert_has_calls([
        mock.call(['help', 'run']),
        mock.call(['run', '--help']),
    ])


def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir):
    with pytest.raises(SystemExit):
        main.main([])
    log_file = os.path.join(mock_store_dir, 'pre-commit.log')
    cap_out_lines = cap_out.get().splitlines()
    assert (
        cap_out_lines[-2] ==
        'An error has occurred: FatalError: git failed. '
        'Is it installed, and are you in a Git repository directory?'
    )
    assert cap_out_lines[-1] == f'Check the log at {log_file}'


def test_hook_stage_migration(mock_store_dir):
    with mock.patch.object(main, 'run') as mck:
        main.main(('run', '--hook-stage', 'commit'))
    assert mck.call_args[0][2].hook_stage == 'pre-commit'