from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

import pytest
import yaml

from commitizen import config, defaults, git
from commitizen.exceptions import ConfigFileIsEmpty, InvalidConfigurationError

PYPROJECT = """
[tool.commitizen]
name = "cz_jira"
version = "1.0.0"
version_files = [
    "commitizen/__version__.py",
    "pyproject.toml"
]
style = [
    ["pointer", "reverse"],
    ["question", "underline"]
]
pre_bump_hooks = [
    "scripts/generate_documentation.sh"
]
post_bump_hooks = ["scripts/slack_notification.sh"]

[tool.black]
line-length = 88
target-version = ['py36', 'py37', 'py38']
"""

DICT_CONFIG = {
    "commitizen": {
        "name": "cz_jira",
        "version": "1.0.0",
        "version_files": ["commitizen/__version__.py", "pyproject.toml"],
        "style": [["pointer", "reverse"], ["question", "underline"]],
        "pre_bump_hooks": ["scripts/generate_documentation.sh"],
        "post_bump_hooks": ["scripts/slack_notification.sh"],
    }
}

JSON_STR = r"""
    {
        "commitizen": {
            "name": "cz_jira",
            "version": "1.0.0",
            "version_files": [
                "commitizen/__version__.py",
                "pyproject.toml"
            ]
        }
    }
"""

YAML_STR = """
commitizen:
  name: cz_jira
  version: 1.0.0
  version_files:
  - commitizen/__version__.py
  - pyproject.toml
"""

_settings: dict[str, Any] = {
    "name": "cz_jira",
    "version": "1.0.0",
    "version_provider": "commitizen",
    "version_scheme": None,
    "tag_format": "$version",
    "legacy_tag_formats": [],
    "ignored_tag_formats": [],
    "bump_message": None,
    "retry_after_failure": False,
    "allow_abort": False,
    "allowed_prefixes": ["Merge", "Revert", "Pull request", "fixup!", "squash!"],
    "version_files": ["commitizen/__version__.py", "pyproject.toml"],
    "style": [["pointer", "reverse"], ["question", "underline"]],
    "changelog_file": "CHANGELOG.md",
    "changelog_format": None,
    "changelog_incremental": False,
    "changelog_start_rev": None,
    "changelog_merge_prerelease": False,
    "update_changelog_on_bump": False,
    "use_shortcuts": False,
    "major_version_zero": False,
    "pre_bump_hooks": ["scripts/generate_documentation.sh"],
    "post_bump_hooks": ["scripts/slack_notification.sh"],
    "prerelease_offset": 0,
    "encoding": "utf-8",
    "always_signoff": False,
    "template": None,
    "extras": {},
}

_new_settings: dict[str, Any] = {
    "name": "cz_jira",
    "version": "2.0.0",
    "version_provider": "commitizen",
    "version_scheme": None,
    "tag_format": "$version",
    "legacy_tag_formats": [],
    "ignored_tag_formats": [],
    "bump_message": None,
    "retry_after_failure": False,
    "allow_abort": False,
    "allowed_prefixes": ["Merge", "Revert", "Pull request", "fixup!", "squash!"],
    "version_files": ["commitizen/__version__.py", "pyproject.toml"],
    "style": [["pointer", "reverse"], ["question", "underline"]],
    "changelog_file": "CHANGELOG.md",
    "changelog_format": None,
    "changelog_incremental": False,
    "changelog_start_rev": None,
    "changelog_merge_prerelease": False,
    "update_changelog_on_bump": False,
    "use_shortcuts": False,
    "major_version_zero": False,
    "pre_bump_hooks": ["scripts/generate_documentation.sh"],
    "post_bump_hooks": ["scripts/slack_notification.sh"],
    "prerelease_offset": 0,
    "encoding": "utf-8",
    "always_signoff": False,
    "template": None,
    "extras": {},
}


@pytest.fixture
def config_files_manager(request, tmpdir):
    with tmpdir.as_cwd():
        filename = request.param
        with open(filename, "w", encoding="utf-8") as f:
            if "toml" in filename:
                f.write(PYPROJECT)
            elif "json" in filename:
                json.dump(DICT_CONFIG, f)
            elif "yaml" in filename:
                yaml.dump(DICT_CONFIG, f)
        yield


def test_find_git_project_root(tmpdir):
    assert git.find_git_project_root() == Path(os.getcwd())

    with tmpdir.as_cwd() as _:
        assert git.find_git_project_root() is None


@pytest.mark.parametrize(
    "config_files_manager", defaults.config_files.copy(), indirect=True
)
def test_set_key(config_files_manager):
    _conf = config.read_cfg()
    _conf.set_key("version", "2.0.0")
    cfg = config.read_cfg()
    assert cfg.settings == _new_settings


class TestReadCfg:
    @pytest.mark.parametrize(
        "config_files_manager", defaults.config_files.copy(), indirect=True
    )
    def test_load_conf(_, config_files_manager):
        cfg = config.read_cfg()
        assert cfg.settings == _settings

    def test_conf_returns_default_when_no_files(_, tmpdir):
        with tmpdir.as_cwd():
            cfg = config.read_cfg()
            assert cfg.settings == defaults.DEFAULT_SETTINGS

    def test_load_empty_pyproject_toml_and_cz_toml_with_config(_, tmpdir):
        with tmpdir.as_cwd():
            p = tmpdir.join("pyproject.toml")
            p.write("")
            p = tmpdir.join(".cz.toml")
            p.write(PYPROJECT)

            cfg = config.read_cfg()
            assert cfg.settings == _settings

    def test_load_pyproject_toml_from_config_argument(_, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml")
            _not_root_path.write(PYPROJECT)

            cfg = config.read_cfg(filepath="./not_in_root/pyproject.toml")
            assert cfg.settings == _settings

    def test_load_cz_json_not_from_config_argument(_, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.json")
            _not_root_path.write(JSON_STR)

            cfg = config.read_cfg(filepath="./not_in_root/.cz.json")
            json_cfg_by_class = config.JsonConfig(data=JSON_STR, path=_not_root_path)
            assert cfg.settings == json_cfg_by_class.settings

    def test_load_cz_yaml_not_from_config_argument(_, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml")
            _not_root_path.write(YAML_STR)

            cfg = config.read_cfg(filepath="./not_in_root/.cz.yaml")
            yaml_cfg_by_class = config.YAMLConfig(data=YAML_STR, path=_not_root_path)
            assert cfg.settings == yaml_cfg_by_class._settings

    def test_load_empty_pyproject_toml_from_config_argument(_, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml")
            _not_root_path.write("")

            with pytest.raises(ConfigFileIsEmpty):
                config.read_cfg(filepath="./not_in_root/pyproject.toml")


@pytest.mark.parametrize(
    "config_file, exception_string",
    [
        (".cz.toml", r"\.cz\.toml"),
        ("cz.toml", r"cz\.toml"),
        ("pyproject.toml", r"pyproject\.toml"),
    ],
    ids=[".cz.toml", "cz.toml", "pyproject.toml"],
)
class TestTomlConfig:
    def test_init_empty_config_content(self, tmpdir, config_file, exception_string):
        path = tmpdir.mkdir("commitizen").join(config_file)
        toml_config = config.TomlConfig(data="", path=path)
        toml_config.init_empty_config_content()

        with open(path, encoding="utf-8") as toml_file:
            assert toml_file.read() == "[tool.commitizen]\n"

    def test_init_empty_config_content_with_existing_content(
        self, tmpdir, config_file, exception_string
    ):
        existing_content = "[tool.black]\nline-length = 88\n"

        path = tmpdir.mkdir("commitizen").join(config_file)
        path.write(existing_content)
        toml_config = config.TomlConfig(data="", path=path)
        toml_config.init_empty_config_content()

        with open(path, encoding="utf-8") as toml_file:
            assert toml_file.read() == existing_content + "\n[tool.commitizen]\n"

    def test_init_with_invalid_config_content(
        self, tmpdir, config_file, exception_string
    ):
        existing_content = "invalid toml content"
        path = tmpdir.mkdir("commitizen").join(config_file)

        with pytest.raises(InvalidConfigurationError, match=exception_string):
            config.TomlConfig(data=existing_content, path=path)


@pytest.mark.parametrize(
    "config_file, exception_string",
    [
        (".cz.json", r"\.cz\.json"),
        ("cz.json", r"cz\.json"),
    ],
    ids=[".cz.json", "cz.json"],
)
class TestJsonConfig:
    def test_init_empty_config_content(self, tmpdir, config_file, exception_string):
        path = tmpdir.mkdir("commitizen").join(config_file)
        json_config = config.JsonConfig(data="{}", path=path)
        json_config.init_empty_config_content()

        with open(path, encoding="utf-8") as json_file:
            assert json.load(json_file) == {"commitizen": {}}

    def test_init_with_invalid_config_content(
        self, tmpdir, config_file, exception_string
    ):
        existing_content = "invalid json content"
        path = tmpdir.mkdir("commitizen").join(config_file)

        with pytest.raises(InvalidConfigurationError, match=exception_string):
            config.JsonConfig(data=existing_content, path=path)


@pytest.mark.parametrize(
    "config_file, exception_string",
    [
        (".cz.yaml", r"\.cz\.yaml"),
        ("cz.yaml", r"cz\.yaml"),
    ],
    ids=[".cz.yaml", "cz.yaml"],
)
class TestYamlConfig:
    def test_init_empty_config_content(self, tmpdir, config_file, exception_string):
        path = tmpdir.mkdir("commitizen").join(config_file)
        yaml_config = config.YAMLConfig(data="{}", path=path)
        yaml_config.init_empty_config_content()

        with open(path) as yaml_file:
            assert yaml.safe_load(yaml_file) == {"commitizen": {}}

    def test_init_with_invalid_content(self, tmpdir, config_file, exception_string):
        existing_content = "invalid: .cz.yaml: content: maybe?"
        path = tmpdir.mkdir("commitizen").join(config_file)

        with pytest.raises(InvalidConfigurationError, match=exception_string):
            config.YAMLConfig(data=existing_content, path=path)