"""Tests for `bumpversion` package."""
import shutil
import subprocess
from pathlib import Path

import pytest
from click.testing import CliRunner, Result
from pytest import param

from bumpversion import __version__, cli
from tests.conftest import inside_dir


def test_version_displays_library_version():
    """
    Arrange/Act: Run the `version` subcommand.

    Assert: The output matches the library version.
    """
    runner: CliRunner = CliRunner()
    result: Result = runner.invoke(cli.cli, ["--version"])
    assert __version__ in result.output.strip(), "Version number should match library version."


def test_bump_no_configured_files(mocker, tmp_path):
    """
    Arrange/Act: Run the `bump` subcommand with --no-configured-files.

    Assert: There is no configured files specified to modify
    """
    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
    runner: CliRunner = CliRunner()
    with inside_dir(tmp_path):
        result: Result = runner.invoke(cli.cli, ["--current-version", "1.0.0", "--no-configured-files", "patch"])

    if result.exit_code != 0:
        print(result.output)

    assert result.exit_code == 0

    call_args = mocked_do_bump.call_args[0]
    assert len(call_args[2].files) == 0


def test_no_configured_files_still_file_args_work(mocker, tmp_path):
    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
    runner: CliRunner = CliRunner()
    with inside_dir(tmp_path):
        result: Result = runner.invoke(
            cli.cli, ["--current-version", "1.0.0", "--no-configured-files", "patch", "do-this-file.txt"]
        )

    if result.exit_code != 0:
        print(result.output)

    assert result.exit_code == 0

    call_args = mocked_do_bump.call_args[0]
    assert len(call_args[2].files) == 1
    assert call_args[2].files[0].filename == "do-this-file.txt"


def test_missing_explicit_config_file(tmp_path: Path):
    """The command-line processor should raise an exception if the config file is missing."""
    with inside_dir(tmp_path):
        runner: CliRunner = CliRunner()
        with inside_dir(tmp_path):
            result: Result = runner.invoke(cli.cli, ["--config-file", "missing-file.cfg"])
        assert result.exit_code != 0
        assert "'missing-file.cfg' does not exist." in result.output


def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker):
    """The command-line processor should override the config file."""
    # Arrange
    config_path = tmp_path / "this_config.toml"
    fixture_toml = fixtures_path / "basic_cfg.toml"
    shutil.copy(fixture_toml, config_path)
    runner: CliRunner = CliRunner()
    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")

    # Act
    with inside_dir(tmp_path):
        result: Result = runner.invoke(
            cli.cli,
            [
                "--config-file",
                str(config_path),
                "--current-version",
                "1.1.0",
                "--new-version",
                "1.2.0",
                "--allow-dirty",
                "--parse",
                r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)",
                "--serialize",
                "XXX{spam};{blob};{slurp}",
                "--search",
                "my-search",
                "--replace",
                "my-replace",
                "--no-commit",
                "--no-tag",
                "patch",
                "do-this-file.txt",
            ],
        )

    # Assert
    assert result.exit_code == 0
    assert mocked_do_bump.call_count == 1
    assert mocked_do_bump.call_args[0][0] == "patch"
    assert mocked_do_bump.call_args[0][1] == "1.2.0"
    the_config = mocked_do_bump.call_args[0][2]
    assert the_config.current_version == "1.1.0"
    assert the_config.allow_dirty
    assert the_config.parse == r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)"
    assert the_config.serialize == ["XXX{spam};{blob};{slurp}"]
    assert the_config.search == "my-search"
    assert the_config.replace == "my-replace"
    assert the_config.commit is False
    assert the_config.tag is False
    assert len(the_config.files) == 4
    assert {f.filename for f in the_config.files} == {
        "setup.py",
        "bumpversion/__init__.py",
        "CHANGELOG.md",
        "do-this-file.txt",
    }
    assert mocked_do_bump.call_args[0][3] == config_path


@pytest.mark.parametrize(
    ["repo", "scm_command"],
    [
        param("git_repo", "git", id="git"),
        param("hg_repo", "hg", id="hg"),
    ],
)
def test_dirty_work_dir_raises_error(repo: str, scm_command: str, request):
    repo_path: Path = request.getfixturevalue(repo)
    with inside_dir(repo_path):
        # Arrange
        repo_path.joinpath("dirty2").write_text("i'm dirty! 1.1.1")
        subprocess.run([scm_command, "add", "dirty2"], check=True)
        runner: CliRunner = CliRunner()

        # Act
        result: Result = runner.invoke(cli.cli, ["patch", "--current-version", "1.1.1", "--no-allow-dirty", "dirty2"])

    # Assert
    assert result.exit_code != 0
    assert "working directory is not clean" in result.output


def test_listing_outputs_correctly_and_stops(tmp_path: Path, fixtures_path: Path):
    """The --list option should list the configuration and nothing else."""
    # Arrange
    config_path = tmp_path / "pyproject.toml"
    toml_path = fixtures_path / "basic_cfg.toml"
    shutil.copy(toml_path, config_path)
    runner: CliRunner = CliRunner()

    with inside_dir(tmp_path):
        result: Result = runner.invoke(cli.cli, ["--list", "patch"])

    if result.exit_code != 0:
        print(result.output)
        print(result.exception)

    assert result.exit_code == 0
    assert set(result.output.splitlines(keepends=False)) == {
        "new_version=1.0.1-dev",
        "current_version=1.0.0",
        "parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
        "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
        "search={current_version}",
        "replace={new_version}",
        "tag=True",
        "sign_tags=False",
        "tag_name=v{new_version}",
        "tag_message=Bump version: {current_version} → {new_version}",
        "allow_dirty=False",
        "commit=True",
        "message=Bump version: {current_version} → {new_version}",
        "commit_args=None",
        "files=[{'filename': 'setup.py', 'glob': None, 'parse': None, 'serialize': "
        "None, 'search': None, 'replace': None}, {'filename': "
        "'bumpversion/__init__.py', 'glob': None, 'parse': None, 'serialize': None, "
        "'search': None, 'replace': None}, {'filename': 'CHANGELOG.md', 'glob': None, "
        "'parse': None, 'serialize': None, 'search': '**unreleased**', 'replace': "
        "'**unreleased**\\n**v{new_version}**'}]",
    }


def test_non_scm_operations_if_scm_not_installed(tmp_path: Path, monkeypatch):
    """Everything works except SCM commands if the SCM is unusable."""
    # Arrange
    monkeypatch.setenv("PATH", "")

    with inside_dir(tmp_path):
        version_path = tmp_path / "VERSION"
        version_path.write_text("31.0.3")

        runner: CliRunner = CliRunner()

        # Act
        runner.invoke(cli.cli, ["major", "--current-version", "31.0.3", "VERSION"])

    # Assert
    assert version_path.read_text() == "32.0.0"
