"""Collection of tests around cookiecutter's command-line interface."""

import json
import os
import re
from pathlib import Path

import pytest
from click.testing import CliRunner

from cookiecutter import utils
from cookiecutter.__main__ import main
from cookiecutter.environment import StrictEnvironment
from cookiecutter.exceptions import UnknownExtension
from cookiecutter.main import cookiecutter


@pytest.fixture(scope='session')
def cli_runner():
    """Fixture that returns a helper function to run the cookiecutter cli."""
    runner = CliRunner()

    def cli_main(*cli_args, **cli_kwargs):
        """Run cookiecutter cli main with the given args."""
        return runner.invoke(main, cli_args, **cli_kwargs)

    return cli_main


@pytest.fixture
def remove_fake_project_dir(request):
    """Remove the fake project directory created during the tests."""

    def fin_remove_fake_project_dir():
        for prefix in ('', 'input'):
            dir_name = f'{prefix}fake-project'
            if os.path.isdir(dir_name):
                utils.rmtree(dir_name)

    request.addfinalizer(fin_remove_fake_project_dir)


@pytest.fixture
def remove_tmp_dir(request):
    """Remove the fake project directory created during the tests."""
    if os.path.isdir('tests/tmp'):
        utils.rmtree('tests/tmp')

    def fin_remove_tmp_dir():
        if os.path.isdir('tests/tmp'):
            utils.rmtree('tests/tmp')

    request.addfinalizer(fin_remove_tmp_dir)


@pytest.fixture
def make_fake_project_dir(request):
    """Create a fake project to be overwritten in the according tests."""
    os.makedirs('fake-project')


@pytest.fixture(params=['-V', '--version'])
def version_cli_flag(request):
    """Pytest fixture return both version invocation options."""
    return request.param


def test_cli_version(cli_runner, version_cli_flag):
    """Verify Cookiecutter version output by `cookiecutter` on cli invocation."""
    result = cli_runner(version_cli_flag)
    assert result.exit_code == 0
    assert result.output.startswith('Cookiecutter')


@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
def test_cli_error_on_existing_output_directory(cli_runner):
    """Test cli invocation without `overwrite-if-exists` fail if dir exist."""
    result = cli_runner('tests/fake-repo-pre/', '--no-input')
    assert result.exit_code != 0
    expected_error_msg = 'Error: "fake-project" directory already exists\n'
    assert result.output == expected_error_msg


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli(cli_runner):
    """Test cli invocation work without flags if directory not exist."""
    result = cli_runner('tests/fake-repo-pre/', '--no-input')
    assert result.exit_code == 0
    assert os.path.isdir('fake-project')
    content = Path("fake-project", "README.rst").read_text()
    assert 'Project name: **Fake Project**' in content


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_verbose(cli_runner):
    """Test cli invocation display log if called with `verbose` flag."""
    result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v')
    assert result.exit_code == 0
    assert os.path.isdir('fake-project')
    content = Path("fake-project", "README.rst").read_text()
    assert 'Project name: **Fake Project**' in content


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_replay(mocker, cli_runner):
    """Test cli invocation display log with `verbose` and `replay` flags."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--replay', '-v')

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=True,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_replay_file(mocker, cli_runner):
    """Test cli invocation correctly pass --replay-file option."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--replay-file', '~/custom-replay-file', '-v')

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay='~/custom-replay-file',
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_tmp_dir')
def test_cli_replay_generated(mocker, cli_runner):
    """Test cli invocation correctly generates a project with replay."""
    template_path = 'tests/fake-repo-replay/'
    result = cli_runner(
        template_path,
        '--replay-file',
        'tests/test-replay/valid_replay.json',
        '-o',
        'tests/tmp/',
        '-v',
    )
    assert result.exit_code == 0
    assert open('tests/tmp/replay-project/README.md').read().strip() == 'replayed'


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_exit_on_noinput_and_replay(mocker, cli_runner):
    """Test cli invocation fail if both `no-input` and `replay` flags passed."""
    mock_cookiecutter = mocker.patch(
        'cookiecutter.cli.cookiecutter', side_effect=cookiecutter
    )

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--no-input', '--replay', '-v')

    assert result.exit_code == 1

    expected_error_msg = (
        "You can not use both replay and no_input or extra_context at the same time."
    )

    assert expected_error_msg in result.output

    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        True,
        replay=True,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.fixture(params=['-f', '--overwrite-if-exists'])
def overwrite_cli_flag(request):
    """Pytest fixture return all `overwrite-if-exists` invocation options."""
    return request.param


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_run_cookiecutter_on_overwrite_if_exists_and_replay(
    mocker, cli_runner, overwrite_cli_flag
):
    """Test cli invocation with `overwrite-if-exists` and `replay` flags."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--replay', '-v', overwrite_cli_flag)

    assert result.exit_code == 0

    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=True,
        overwrite_if_exists=True,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_overwrite_if_exists_when_output_dir_does_not_exist(
    cli_runner, overwrite_cli_flag
):
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.

    Case when output dir not exist.
    """
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)

    assert result.exit_code == 0
    assert os.path.isdir('fake-project')


@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
def test_cli_overwrite_if_exists_when_output_dir_exists(cli_runner, overwrite_cli_flag):
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.

    Case when output dir already exist.
    """
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)
    assert result.exit_code == 0
    assert os.path.isdir('fake-project')


@pytest.fixture(params=['-o', '--output-dir'])
def output_dir_flag(request):
    """Pytest fixture return all output-dir invocation options."""
    return request.param


def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir):
    """Test cli invocation with `output-dir` flag changes output directory."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, output_dir_flag, output_dir)

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir=output_dir,
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.fixture(params=['-h', '--help', 'help'])
def help_cli_flag(request):
    """Pytest fixture return all help invocation options."""
    return request.param


def test_cli_help(cli_runner, help_cli_flag):
    """Test cli invocation display help message with `help` flag."""
    result = cli_runner(help_cli_flag)
    assert result.exit_code == 0
    assert result.output.startswith('Usage')


@pytest.fixture
def user_config_path(tmp_path):
    """Pytest fixture return `user_config` argument as string."""
    return str(tmp_path.joinpath("tests", "config.yaml"))


def test_user_config(mocker, cli_runner, user_config_path):
    """Test cli invocation works with `config-file` option."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--config-file', user_config_path)

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=user_config_path,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_default_user_config_overwrite(mocker, cli_runner, user_config_path):
    """Test cli invocation ignores `config-file` if `default-config` passed."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(
        template_path,
        '--config-file',
        user_config_path,
        '--default-config',
    )

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=user_config_path,
        default_config=True,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_default_user_config(mocker, cli_runner):
    """Test cli invocation accepts `default-config` flag correctly."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--default-config')

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=True,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_echo_undefined_variable_error(output_dir, cli_runner):
    """Cli invocation return error if variable undefined in template."""
    template_path = 'tests/undefined-variable/file-name/'

    result = cli_runner(
        '--no-input',
        '--default-config',
        '--output-dir',
        output_dir,
        template_path,
    )

    assert result.exit_code == 1

    error = "Unable to create file '{{cookiecutter.foobar}}'"
    assert error in result.output

    message = (
        "Error message: 'collections.OrderedDict object' has no attribute 'foobar'"
    )
    assert message in result.output

    context = {
        '_cookiecutter': {
            'github_username': 'hackebrot',
            'project_slug': 'testproject',
        },
        'cookiecutter': {
            'github_username': 'hackebrot',
            'project_slug': 'testproject',
            '_template': template_path,
            '_repo_dir': template_path,
            '_output_dir': output_dir,
            '_checkout': None,
        },
    }
    context_str = json.dumps(context, indent=4, sort_keys=True)
    assert context_str in result.output


def test_echo_unknown_extension_error(output_dir, cli_runner):
    """Cli return error if extension incorrectly defined in template."""
    template_path = 'tests/test-extensions/unknown/'

    result = cli_runner(
        '--no-input',
        '--default-config',
        '--output-dir',
        output_dir,
        template_path,
    )

    assert result.exit_code == 1

    assert 'Unable to load extension: ' in result.output


def test_local_extension(tmpdir, cli_runner):
    """Test to verify correct work of extension, included in template."""
    output_dir = str(tmpdir.mkdir('output'))
    template_path = 'tests/test-extensions/local_extension/'

    result = cli_runner(
        '--no-input',
        '--default-config',
        '--output-dir',
        output_dir,
        template_path,
    )
    assert result.exit_code == 0
    content = Path(output_dir, 'Foobar', 'HISTORY.rst').read_text()
    assert 'FoobarFoobar' in content
    assert 'FOOBAR' in content


def test_local_extension_not_available(tmpdir, cli_runner):
    """Test handling of included but unavailable local extension."""
    context = {'cookiecutter': {'_extensions': ['foobar']}}

    with pytest.raises(UnknownExtension) as err:
        StrictEnvironment(context=context, keep_trailing_newline=True)

    assert 'Unable to load extension: ' in str(err.value)


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_extra_context(cli_runner):
    """Cli invocation replace content if called with replacement pairs."""
    result = cli_runner(
        'tests/fake-repo-pre/',
        '--no-input',
        '-v',
        'project_name=Awesomez',
    )
    assert result.exit_code == 0
    assert os.path.isdir('fake-project')
    content = Path('fake-project', 'README.rst').read_text()
    assert 'Project name: **Awesomez**' in content


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_extra_context_invalid_format(cli_runner):
    """Cli invocation raise error if called with unknown argument."""
    result = cli_runner(
        'tests/fake-repo-pre/',
        '--no-input',
        '-v',
        'ExtraContextWithNoEqualsSoInvalid',
    )
    assert result.exit_code == 2
    assert "Error: Invalid value for '[EXTRA_CONTEXT]...'" in result.output
    assert 'should contain items of the form key=value' in result.output


@pytest.fixture
def debug_file(tmp_path):
    """Pytest fixture return `debug_file` argument as path object."""
    return tmp_path.joinpath('fake-repo.log')


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_debug_file_non_verbose(cli_runner, debug_file):
    """Test cli invocation writes log to `debug-file` if flag enabled.

    Case for normal log output.
    """
    assert not debug_file.exists()

    result = cli_runner(
        '--no-input',
        '--debug-file',
        str(debug_file),
        'tests/fake-repo-pre/',
    )
    assert result.exit_code == 0

    assert debug_file.exists()

    context_log = (
        "DEBUG cookiecutter.main: context_file is "
        "tests/fake-repo-pre/cookiecutter.json"
    )
    assert context_log in debug_file.read_text()
    assert context_log not in result.output


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_debug_file_verbose(cli_runner, debug_file):
    """Test cli invocation writes log to `debug-file` if flag enabled.

    Case for verbose log output.
    """
    assert not debug_file.exists()

    result = cli_runner(
        '--verbose',
        '--no-input',
        '--debug-file',
        str(debug_file),
        'tests/fake-repo-pre/',
    )
    assert result.exit_code == 0

    assert debug_file.exists()

    context_log = (
        "DEBUG cookiecutter.main: context_file is "
        "tests/fake-repo-pre/cookiecutter.json"
    )
    assert context_log in debug_file.read_text()
    assert context_log in result.output


@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path):
    """Verify --list-installed command correct invocation."""
    fake_template_dir = os.path.dirname(os.path.abspath('fake-project'))
    os.makedirs(os.path.dirname(user_config_path))
    # Single quotes in YAML will not parse escape codes (\).
    Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'")
    Path("fake-project", "cookiecutter.json").write_text('{}')

    result = cli_runner(
        '--list-installed',
        '--config-file',
        user_config_path,
        str(debug_file),
    )

    assert "1 installed templates:" in result.output
    assert result.exit_code == 0


def test_debug_list_installed_templates_failure(
    cli_runner, debug_file, user_config_path
):
    """Verify --list-installed command error on invocation."""
    os.makedirs(os.path.dirname(user_config_path))
    Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"')

    result = cli_runner(
        '--list-installed', '--config-file', user_config_path, str(debug_file)
    )

    assert "Error: Cannot list installed templates." in result.output
    assert result.exit_code == -1


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_directory_repo(cli_runner):
    """Test cli invocation works with `directory` option."""
    result = cli_runner(
        'tests/fake-repo-dir/',
        '--no-input',
        '-v',
        '--directory=my-dir',
    )
    assert result.exit_code == 0
    assert os.path.isdir("fake-project")
    content = Path("fake-project", "README.rst").read_text()
    assert "Project name: **Fake Project**" in content


cli_accept_hook_arg_testdata = [
    ("--accept-hooks=yes", None, True),
    ("--accept-hooks=no", None, False),
    ("--accept-hooks=ask", "yes", True),
    ("--accept-hooks=ask", "no", False),
]


@pytest.mark.parametrize(
    "accept_hooks_arg,user_input,expected", cli_accept_hook_arg_testdata
)
def test_cli_accept_hooks(
    mocker,
    cli_runner,
    output_dir_flag,
    output_dir,
    accept_hooks_arg,
    user_input,
    expected,
):
    """Test cli invocation works with `accept-hooks` option."""
    mock_cookiecutter = mocker.patch("cookiecutter.cli.cookiecutter")

    template_path = "tests/fake-repo-pre/"
    result = cli_runner(
        template_path, output_dir_flag, output_dir, accept_hooks_arg, input=user_input
    )

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        output_dir=output_dir,
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        skip_if_file_exists=False,
        accept_hooks=expected,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_with_json_decoding_error(cli_runner):
    """Test cli invocation with a malformed JSON file."""
    template_path = 'tests/fake-repo-bad-json/'
    result = cli_runner(template_path, '--no-input')
    assert result.exit_code != 0

    # Validate the error message.
    # original message from json module should be included
    pattern = 'Expecting \'{0,1}:\'{0,1} delimiter: line 1 column (19|20) \\(char 19\\)'
    assert re.search(pattern, result.output)
    # File name should be included too...for testing purposes, just test the
    # last part of the file. If we wanted to test the absolute path, we'd have
    # to do some additional work in the test which doesn't seem that needed at
    # this point.
    path = os.path.sep.join(['tests', 'fake-repo-bad-json', 'cookiecutter.json'])
    assert path in result.output


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_with_pre_prompt_hook(cli_runner):
    """Test cli invocation in a template with pre_prompt hook."""
    template_path = 'tests/test-pyhooks/'
    result = cli_runner(template_path, '--no-input')
    assert result.exit_code == 0
    dir_name = 'inputfake-project'
    assert os.path.isdir(dir_name)
    content = Path(dir_name, "README.rst").read_text()
    assert 'foo' in content


def test_cli_with_pre_prompt_hook_fail(cli_runner, monkeypatch):
    """Test cli invocation will fail when a given env var is present."""
    template_path = 'tests/test-pyhooks/'
    with monkeypatch.context() as m:
        m.setenv('COOKIECUTTER_FAIL_PRE_PROMPT', '1')
        result = cli_runner(template_path, '--no-input')
    assert result.exit_code == 1
    dir_name = 'inputfake-project'
    assert not Path(dir_name).exists()
