from __future__ import annotations

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

import pytest
import yaml

from commitizen import cmd, config, defaults, git
from commitizen.config.json_config import JsonConfig
from commitizen.config.toml_config import TomlConfig
from commitizen.config.yaml_config import YAMLConfig
from commitizen.exceptions import ConfigFileIsEmpty, InvalidConfigurationError

TOML_STR = """
[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"]
"""

PYPROJECT = f"""
{TOML_STR}

[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!",
        "amend!",
    ],
    "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": {},
    "breaking_change_exclamation_in_title": False,
    "message_length_limit": 0,
}

_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!",
        "amend!",
    ],
    "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": {},
    "breaking_change_exclamation_in_title": False,
    "message_length_limit": 0,
}


@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


@pytest.mark.usefixtures("in_repo_root")
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, 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, indirect=True
    )
    def test_load_conf(self, config_files_manager):
        cfg = config.read_cfg()
        assert cfg.settings == _settings

    def test_conf_returns_default_when_no_files(self, 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(self, tmpdir):
        with tmpdir.as_cwd():
            p = tmpdir.join("pyproject.toml")
            p.write("")
            p = tmpdir.join(".cz.toml")
            p.write(TOML_STR)

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

    def test_load_pyproject_toml_from_config_argument(self, 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(self, 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 = 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(self, 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 = 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(self, 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")

    def test_load_empty_json_from_config_argument(self, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.json")
            _not_root_path.write("")

            with pytest.raises(ConfigFileIsEmpty):
                config.read_cfg(filepath="./not_in_root/.cz.json")

    def test_load_empty_yaml_from_config_argument(self, tmpdir):
        with tmpdir.as_cwd():
            _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml")
            _not_root_path.write("")

            with pytest.raises(ConfigFileIsEmpty):
                config.read_cfg(filepath="./not_in_root/.cz.yaml")


class TestWarnMultipleConfigFiles:
    @pytest.mark.parametrize(
        ("files", "expected_path"),
        [
            # Same directory, different file types
            ([(".cz.toml", TOML_STR), (".cz.json", JSON_STR)], ".cz.toml"),
            ([(".cz.json", JSON_STR), (".cz.yaml", YAML_STR)], ".cz.json"),
            ([(".cz.toml", TOML_STR), (".cz.yaml", YAML_STR)], ".cz.toml"),
            # With pyproject.toml
            (
                [("pyproject.toml", PYPROJECT), (".cz.json", JSON_STR)],
                ".cz.json",
            ),
            (
                [("pyproject.toml", PYPROJECT), (".cz.toml", TOML_STR)],
                ".cz.toml",
            ),
        ],
    )
    def test_warn_multiple_config_files_same_dir(
        self, tmpdir, capsys, files, expected_path
    ):
        """Test warning when multiple config files exist in same directory."""
        with tmpdir.as_cwd():
            for filename, content in files:
                tmpdir.join(filename).write(content)

            cfg = config.read_cfg()
            captured = capsys.readouterr()

            assert "Multiple config files detected" in captured.err
            for filename, _ in files:
                assert filename in captured.err
            assert f"Using config file: '{expected_path}'" in captured.err

            assert cfg.path == Path(expected_path)

    @pytest.mark.parametrize(
        ("config_file", "content"),
        [
            (".cz.json", JSON_STR),
            (".cz.toml", TOML_STR),
            (".cz.yaml", YAML_STR),
            ("cz.toml", TOML_STR),
            ("cz.json", JSON_STR),
            ("cz.yaml", YAML_STR),
        ],
    )
    def test_warn_same_filename_different_directories_with_git(
        self, tmpdir, capsys, config_file, content
    ):
        """Test warning when same config filename exists in the current directory and in the git root."""
        with tmpdir.as_cwd():
            cmd.run("git init")

            # Create config in git root
            tmpdir.join(config_file).write(content)

            # Create same filename in subdirectory
            subdir = tmpdir.mkdir("subdir")
            subdir.join(config_file).write(content)

            with subdir.as_cwd():
                cfg = config.read_cfg()
                captured = capsys.readouterr()

                assert "Multiple config files detected" in captured.err
                assert f"Using config file: '{config_file}'" in captured.err
                assert cfg.path == Path(config_file)

    def test_no_warn_with_explicit_config_path(self, tmpdir, capsys):
        """Test that no warning is issued when user explicitly specifies config."""
        with tmpdir.as_cwd():
            # Create multiple config files
            tmpdir.join(".cz.toml").write(PYPROJECT)
            tmpdir.join(".cz.json").write(JSON_STR)

            # Read config with explicit path
            cfg = config.read_cfg(filepath=".cz.json")

            # No warning should be issued
            captured = capsys.readouterr()
            assert "Multiple config files detected" not in captured.err

            # Verify the explicitly specified config is loaded (compare to expected JSON config)
            json_cfg_expected = JsonConfig(data=JSON_STR, path=Path(".cz.json"))
            assert cfg.settings == json_cfg_expected.settings

    @pytest.mark.parametrize(
        ("config_file", "content", "with_git"),
        [
            (file, content, with_git)
            for file, content in [
                (".cz.toml", TOML_STR),
                (".cz.json", JSON_STR),
                (".cz.yaml", YAML_STR),
                ("pyproject.toml", PYPROJECT),
                ("cz.toml", TOML_STR),
                ("cz.json", JSON_STR),
                ("cz.yaml", YAML_STR),
            ]
            for with_git in [True, False]
        ],
    )
    def test_no_warn_with_single_config_file(
        self, tmpdir, capsys, config_file, content, with_git
    ):
        """Test that no warning is issued when user explicitly specifies config."""
        with tmpdir.as_cwd():
            if with_git:
                cmd.run("git init")

            tmpdir.join(config_file).write(content)

            cfg = config.read_cfg()
            captured = capsys.readouterr()

            # No warning should be issued
            assert "Multiple config files detected" not in captured.err
            assert cfg.path == Path(config_file)

    def test_no_warn_with_no_commitizen_section_in_pyproject_toml_and_cz_toml(
        self, tmpdir, capsys
    ):
        with tmpdir.as_cwd():
            tmpdir.join("pyproject.toml").write("[tool.foo]\nbar = 'baz'")
            tmpdir.join(".cz.toml").write(TOML_STR)

            cfg = config.read_cfg()
            captured = capsys.readouterr()
            assert "Multiple config files detected" not in captured.err
            assert cfg.path == Path(".cz.toml")


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

        assert Path(path).read_text(encoding="utf-8") == "[tool.commitizen]\n"

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

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

        assert (
            Path(path).read_text(encoding="utf-8")
            == existing_content + "\n[tool.commitizen]\n"
        )

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

        with pytest.raises(InvalidConfigurationError) as excinfo:
            TomlConfig(data=existing_content, path=path)
        assert config_file in str(excinfo.value)


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

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

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

        with pytest.raises(InvalidConfigurationError) as excinfo:
            JsonConfig(data=existing_content, path=path)
        assert config_file in str(excinfo.value)


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

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

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

        with pytest.raises(InvalidConfigurationError) as excinfo:
            YAMLConfig(data=existing_content, path=path)
        assert config_file in str(excinfo.value)
