import logging
from pathlib import Path

import pytest
from click import UsageError
from pytest import LogCaptureFixture, param

from bumpversion.config.models import VersionPartConfig
from bumpversion import exceptions
from bumpversion.utils import get_context
from bumpversion.version_part import VersionPart
from tests.conftest import get_config_data, inside_dir


@pytest.fixture(
    params=[
        None,
        ("0", "1", "2"),
        ("0", "3"),
    ]
)
def version_part_config(request):
    """Return a three-part and a two-part version part configuration."""
    if request.param is None:
        return VersionPartConfig()
    else:
        return VersionPartConfig(values=request.param)


def test_version_part_init(version_part_config):
    vp = VersionPart(version_part_config, version_part_config.first_value)
    assert vp.value is not None


def test_version_part_copy(version_part_config):
    vp = VersionPart(version_part_config, version_part_config.first_value)
    vc = vp.copy()
    assert vp.value == vc.value
    assert id(vp) != id(vc)


def test_version_part_bump(version_part_config):
    vp = VersionPart(version_part_config, version_part_config.first_value)
    vc = vp.bump()
    if version_part_config.values:
        assert vc.value == str(version_part_config.values[1])
    else:
        assert vc.value == "1"


def test_version_part_check_optional_false(version_part_config):
    assert not VersionPart(version_part_config, version_part_config.first_value).bump().is_optional


def test_version_part_check_optional_true(version_part_config):
    assert VersionPart(version_part_config, version_part_config.first_value).is_optional


def test_version_part_equality(version_part_config):
    assert VersionPart(version_part_config, version_part_config.first_value) == VersionPart(
        version_part_config, version_part_config.first_value
    )


def test_version_part_null(version_part_config):
    assert VersionPart(version_part_config, version_part_config.first_value).null() == VersionPart(
        version_part_config, version_part_config.first_value
    )


def test_bump_version_missing_part():
    overrides = {
        "current_version": "1.0.0",
    }
    conf, version_config, current_version = get_config_data(overrides)
    with pytest.raises(exceptions.InvalidVersionPartError, match="No part named 'bugfix'"):
        current_version.bump("bugfix", version_config.order)


def test_build_number_configuration():
    overrides = {
        "current_version": "2.1.6-5123",
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<build>\d+)",
        "serialize": ["{major}.{minor}.{patch}-{build}"],
        "parts": {
            "build": {
                "independent": True,
            }
        },
    }
    conf, version_config, current_version = get_config_data(overrides)

    build_version_bump = current_version.bump("build", version_config.order)
    assert build_version_bump.values["build"].value == "5124"
    assert build_version_bump.values["build"].is_independent

    major_version_bump = build_version_bump.bump("major", version_config.order)
    assert major_version_bump.values["build"].value == "5124"
    assert build_version_bump.values["build"].is_independent
    assert major_version_bump.values["major"].value == "3"

    build_version_bump = major_version_bump.bump("build", version_config.order)
    assert build_version_bump.values["build"].value == "5125"
    assert build_version_bump.values["build"].is_independent


def test_serialize_with_distance_to_latest_tag():
    """Using ``distance_to_latest_tag`` in the serialization string outputs correctly."""
    from bumpversion.scm import Git, SCMInfo

    overrides = {
        "current_version": "19.6.0",
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+).*",
        "serialize": ["{major}.{minor}.{patch}-pre{distance_to_latest_tag}"],
    }
    conf, version_config, current_version = get_config_data(overrides)
    conf.scm_info = SCMInfo(
        tool=Git, commit_sha="1234123412341234", distance_to_latest_tag=3, current_version="19.6.0", dirty=False
    )
    assert version_config.serialize(current_version, get_context(conf)) == "19.6.0-pre3"


def test_serialize_with_environment_var():
    import os

    os.environ["BUILD_NUMBER"] = "567"
    overrides = {
        "current_version": "2.3.4",
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+).*",
        "serialize": ["{major}.{minor}.{patch}-pre{$BUILD_NUMBER}"],
    }
    conf, version_config, current_version = get_config_data(overrides)
    assert version_config.serialize(current_version, get_context(conf)) == "2.3.4-pre567"
    del os.environ["BUILD_NUMBER"]


def test_serialize_with_newlines():
    overrides = {
        "current_version": "MAJOR=31\nMINOR=0\nPATCH=3\n",
        "parse": r"MAJOR=(?P<major>\d+)\nMINOR=(?P<minor>\d+)\nPATCH=(?P<patch>\d+)\n",
        "serialize": ["MAJOR={major}\nMINOR={minor}\nPATCH={patch}\n"],
    }
    conf, version_config, current_version = get_config_data(overrides)
    new_version = current_version.bump("major", version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == "MAJOR=32\nMINOR=0\nPATCH=0\n"


@pytest.mark.parametrize(
    ["bump_type", "expected"],
    [
        param("patch", "0.9.1", id="patch"),
        param("minor", "0.10", id="minor"),
        param("major", "1", id="major"),
    ],
)
def test_serialize_three_part(bump_type: str, expected: str):
    overrides = {
        "current_version": "0.9",
        "parse": r"(?P<major>\d+)(\.(?P<minor>\d+)(\.(?P<patch>\d+))?)?",
        "serialize": ["{major}.{minor}.{patch}", "{major}.{minor}", "{major}"],
    }
    conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump(bump_type, version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == expected


@pytest.mark.parametrize(
    ["initial", "bump_type", "expected"],
    [
        param("1.5.dev", "release", "1.5", id="release"),
        param("1.5", "minor", "1.6.dev", id="minor"),
    ],
)
def test_bump_non_numeric_parts(initial: str, bump_type: str, expected: str):
    overrides = {
        "current_version": initial,
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<release>[a-z]+))?",
        "serialize": ["{major}.{minor}.{release}", "{major}.{minor}"],
        "parts": {
            "release": {
                "optional_value": "gamma",
                "values": ["dev", "gamma"],
            },
        },
    }
    conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump(bump_type, version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == expected


@pytest.mark.parametrize(
    ["initial", "bump_type", "expected"],
    [
        param("1.alpha", "release", "1.beta", id="alpha-to-beta-release"),
        param("1.beta", "release", "1", id="beta-to-release"),
    ],
)
def test_optional_value(initial: str, bump_type: str, expected: str):
    overrides = {
        "current_version": initial,
        "parse": r"(?P<num>\d+)(\.(?P<release>.*))?(\.)?",
        "serialize": ["{num}.{release}", "{num}"],
        "parts": {
            "release": {
                "optional_value": "gamma",
                "values": ["alpha", "beta", "gamma"],
            },
        },
    }
    conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump(bump_type, version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == expected


@pytest.mark.parametrize(
    ["initial", "bump_type", "expected"],
    [
        param("1.0a", "prerel", "1.0b", id="a-to-b-release"),
        param("1.0b", "prerelversion", "1.0b1", id="prerelease-version"),
        param("1.0b1", "prerelversion", "1.0b2", id="prerelease-version2"),
        param("1.0b2", "prerel", "1.0c", id="b2-to-c-release"),
        param("1.0c", "prerel", "1.0rc", id="c-to-rc-release"),
        param("1.0rc", "prerel", "1.0", id="rc-to-d-release"),
        param("1.0", "minor", "1.1dev", id="minor-release"),
        param("1.1dev", "prerel", "1.1a", id="dev-to-a-release"),
    ],
)
def test_python_pre_release_release_post_release(initial: str, bump_type: str, expected: str):
    # adapted from http://legacy.python.org/dev/peps/pep-0386/#the-new-versioning-algorithm
    overrides = {
        "current_version": initial,
        "parse": r"""^
            (?P<major>\d+)\.(?P<minor>\d+)   # minimum 'N.N'
            (?:
                (?P<prerel>[abc]|rc|dev)     # 'a' = alpha, 'b' = beta
                                             # 'c' or 'rc' = release candidate
                (?:
                    (?P<prerelversion>\d+(?:\.\d+)*)
                )?
            )?
            (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?""",
        "serialize": [
            "{major}.{minor}{prerel}{prerelversion}",
            "{major}.{minor}{prerel}",
            "{major}.{minor}",
        ],
        "parts": {
            "prerel": {
                "optional_value": "d",
                "values": ["dev", "a", "b", "c", "rc", "d"],
            },
        },
    }
    conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump(bump_type, version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == expected


@pytest.mark.parametrize(
    ["initial", "bump_type", "expected"],
    [
        param("0.9.4", "major", "1.1.0", id="first-value-1"),
    ],
)
def test_part_first_value(initial: str, bump_type: str, expected: str):
    overrides = {
        "current_version": initial,
        "parts": {
            "minor": {
                "first_value": "1",
            },
        },
    }
    conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump(bump_type, version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == expected


def test_version_part_invalid_regex_exit(tmp_path: Path) -> None:
    """A version part with an invalid regex should raise an exception."""
    # Arrange
    overrides = {
        "current_version": "12",
        "parse": "*kittens*",
    }
    with inside_dir(tmp_path):
        with pytest.raises(UsageError):
            get_config_data(overrides)


def test_parse_doesnt_parse_current_version(tmp_path: Path, caplog: LogCaptureFixture) -> None:
    """A warning should be output when the parse regex doesn't parse the version."""
    overrides = {"current_version": "12", "parse": "xxx"}
    with inside_dir(tmp_path):
        get_config_data(overrides)

    assert "    Evaluating 'parse' option: 'xxx' does not parse current version '12'" in caplog.messages


def test_part_does_not_revert_to_zero_if_optional(tmp_path: Path) -> None:
    """A non-numeric part with the optional value should not revert to zero."""
    # From https://github.com/c4urself/bump2version/issues/248
    # Arrange
    overrides = {
        "current_version": "0.3.1",
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>\D+)(?P<build>\d*))?",
        "serialize": [
            "{major}.{minor}.{patch}{release}{build}",
            "{major}.{minor}.{patch}{release}",
            "{major}.{minor}.{patch}",
        ],
        "parts": {
            "release": {
                "optional_value": "g",
                "first_value": "g",
                "values": [
                    "dev",
                    "a",
                    "b",
                    "g",
                ],
            },
        },
    }
    with inside_dir(tmp_path):
        conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump("build", version_config.order)
    assert version_config.serialize(new_version, get_context(conf)) == "0.3.1g1"


def test_order_of_serialization(tmp_path: Path, caplog: LogCaptureFixture) -> None:
    """The order of serialization should be as specified in the config."""
    caplog.set_level(logging.DEBUG)
    overrides = {
        "current_version": "3.16.dev1",
        "parse": r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<release>[a-z]+)?(?P<patch>\d+)?",
        "serialize": [
            "{major}.{minor}.{release}{patch}",
            "{major}.{minor}.{release}",
            "{major}.{minor}.{patch}",
        ],
        "parts": {
            "release": {
                "optional_value": "prod",
                "first_value": "dev",
                "values": [
                    "dev",
                    "prod",
                ],
            },
        },
    }
    with inside_dir(tmp_path):
        conf, version_config, current_version = get_config_data(overrides)

    new_version = current_version.bump("release", version_config.order)
    new_version_str = version_config.serialize(new_version, get_context(conf))
    for msg in caplog.messages:
        print(msg)
    assert new_version_str == "3.16.prod"
