import os
import sys
from pathlib import Path

import pytest

from isort import exceptions, settings
from isort.settings import Config
from isort.wrap_modes import WrapModes


class TestConfig:
    instance = Config()

    def test_init(self):
        assert Config()

    def test_init_unsupported_settings_fails_gracefully(self):
        with pytest.raises(exceptions.UnsupportedSettings):
            Config(apply=True)
        with pytest.raises(exceptions.UnsupportedSettings) as error:
            Config(apply=True)
        assert error.value.unsupported_settings == {"apply": {"value": True, "source": "runtime"}}

    def test_known_settings(self):
        assert Config(known_third_party=["one"]).known_third_party == frozenset({"one"})
        assert Config(known_thirdparty=["two"]).known_third_party == frozenset({"two"})
        assert Config(
            known_third_party=["one"], known_thirdparty=["two"]
        ).known_third_party == frozenset({"one"})

    def test_invalid_settings_path(self):
        with pytest.raises(exceptions.InvalidSettingsPath):
            Config(settings_path="this_couldnt_possibly_actually_exists/could_it")

    def test_invalid_pyversion(self):
        with pytest.raises(ValueError, match="The python version 10 is not supported."):
            Config(py_version=10)

    def test_invalid_profile(self):
        with pytest.raises(exceptions.ProfileDoesNotExist):
            Config(profile="blackandwhitestylemixedwithpep8")

    def test_is_skipped(self):
        assert Config().is_skipped(Path("C:\\path\\isort.py"))
        assert Config(skip=["/path/isort.py"]).is_skipped(Path("C:\\path\\isort.py"))

    def test_is_supported_filetype(self):
        assert self.instance.is_supported_filetype("file.py")
        assert self.instance.is_supported_filetype("file.pyi")
        assert self.instance.is_supported_filetype("file.pyx")
        assert self.instance.is_supported_filetype("file.pxd")
        assert not self.instance.is_supported_filetype("file.pyc")
        assert not self.instance.is_supported_filetype("file.txt")
        assert not self.instance.is_supported_filetype("file.pex")

    def test_is_supported_filetype_ioerror(self, tmpdir):
        does_not_exist = tmpdir.join("fake.txt")
        assert not self.instance.is_supported_filetype(str(does_not_exist))

    def test_is_supported_filetype_shebang(self, tmpdir):
        path = tmpdir.join("myscript")
        path.write("#!/usr/bin/env python\n")
        assert self.instance.is_supported_filetype(str(path))

    def test_is_supported_filetype_editor_backup(self, tmpdir):
        path = tmpdir.join("myscript~")
        path.write("#!/usr/bin/env python\n")
        assert not self.instance.is_supported_filetype(str(path))

    def test_is_supported_filetype_defaults(self, tmpdir):
        assert self.instance.is_supported_filetype(str(tmpdir.join("stub.pyi")))
        assert self.instance.is_supported_filetype(str(tmpdir.join("source.py")))
        assert self.instance.is_supported_filetype(str(tmpdir.join("source.pyx")))

    def test_is_supported_filetype_configuration(self, tmpdir):
        config = Config(supported_extensions=("pyx",), blocked_extensions=("py",))
        assert config.is_supported_filetype(str(tmpdir.join("stub.pyx")))
        assert not config.is_supported_filetype(str(tmpdir.join("stub.py")))

    @pytest.mark.skipif(
        sys.platform == "win32", reason="cannot create fifo file on Windows platform"
    )
    def test_is_supported_filetype_fifo(self, tmpdir):
        fifo_file = os.path.join(tmpdir, "fifo_file")
        os.mkfifo(fifo_file)
        assert not self.instance.is_supported_filetype(fifo_file)

    def test_src_paths_are_combined_and_deduplicated(self):
        src_paths = ["src", "tests"]
        src_full_paths = (Path(os.getcwd()) / f for f in src_paths)
        assert sorted(Config(src_paths=src_paths * 2).src_paths) == sorted(src_full_paths)

    def test_src_paths_supports_glob_expansion(self, tmp_path):
        libs = tmp_path / "libs"
        libs.mkdir()
        requests = libs / "requests"
        requests.mkdir()
        beautifulpasta = libs / "beautifulpasta"
        beautifulpasta.mkdir()
        assert sorted(Config(directory=tmp_path, src_paths=["libs/*"]).src_paths) == sorted(
            (beautifulpasta, requests)
        )

    def test_deprecated_multi_line_output(self):
        assert Config(multi_line_output=6).multi_line_output == WrapModes.VERTICAL_GRID_GROUPED  # type: ignore # noqa


def test_as_list():
    assert settings._as_list([" one "]) == ["one"]  # type: ignore
    assert settings._as_list("one,two") == ["one", "two"]


def _write_simple_settings(tmp_file):
    tmp_file.write_text(
        """
[isort]
force_grid_wrap=true
""",
        "utf8",
    )


def test_find_config(tmpdir):
    tmp_config = tmpdir.join(".isort.cfg")

    # can't find config if it has no relevant section
    tmp_config.write_text(
        """
[section]
force_grid_wrap=true
""",
        "utf8",
    )
    assert not settings._find_config(str(tmpdir))[1]

    # or if it is malformed
    tmp_config.write_text("""arstoyrsyan arienrsaeinrastyngpuywnlguyn354q^%$)(%_)@$""", "utf8")
    assert not settings._find_config(str(tmpdir))[1]

    # can when it has either a file format, or generic relevant section
    _write_simple_settings(tmp_config)
    assert settings._find_config(str(tmpdir))[1]


def test_find_config_deep(tmpdir):
    # can't find config if it is further up than MAX_CONFIG_SEARCH_DEPTH
    dirs = [f"dir{i}" for i in range(settings.MAX_CONFIG_SEARCH_DEPTH + 1)]
    tmp_dirs = tmpdir.ensure(*dirs, dirs=True)
    tmp_config = tmpdir.join("dir0", ".isort.cfg")
    _write_simple_settings(tmp_config)
    assert not settings._find_config(str(tmp_dirs))[1]
    # but can find config if it is MAX_CONFIG_SEARCH_DEPTH up
    one_parent_up = os.path.split(str(tmp_dirs))[0]
    assert settings._find_config(one_parent_up)[1]


def test_get_config_data(tmpdir):
    test_config = tmpdir.join("test_config.editorconfig")
    test_config.write_text(
        """
root = true

[*.{js,py}]
indent_style=tab
indent_size=tab

[*.py]
force_grid_wrap=false
comment_prefix="text"

[*.{java}]
indent_style = space
""",
        "utf8",
    )
    loaded_settings = settings._get_config_data(
        str(test_config), sections=settings.CONFIG_SECTIONS[".editorconfig"]
    )
    assert loaded_settings
    assert loaded_settings["comment_prefix"] == "text"
    assert loaded_settings["force_grid_wrap"] == 0
    assert loaded_settings["indent"] == "\t"
    assert str(tmpdir) in loaded_settings["source"]


def test_editorconfig_without_sections(tmpdir):
    test_config = tmpdir.join("test_config.editorconfig")
    test_config.write_text("\nroot = true\n", "utf8")
    loaded_settings = settings._get_config_data(str(test_config), sections=("*.py",))
    assert not loaded_settings


def test_get_config_data_with_toml_and_utf8(tmpdir):
    test_config = tmpdir.join("pyproject.toml")
    # Exception: UnicodeDecodeError: 'gbk' codec can't decode byte 0x84 in position 57
    test_config.write_text(
        """
[project]
description = "基于FastAPI + Mysql的 TodoList"  # Exception: UnicodeDecodeError
name = "TodoList"
version = "0.1.0"

[tool.isort]
multi_line_output = 3

""",
        "utf8",
    )
    loaded_settings = settings._get_config_data(
        str(test_config), sections=settings.CONFIG_SECTIONS["pyproject.toml"]
    )
    assert loaded_settings
    assert str(tmpdir) in loaded_settings["source"]


def test_as_bool():
    assert settings._as_bool("TrUe") is True
    assert settings._as_bool("true") is True
    assert settings._as_bool("t") is True
    assert settings._as_bool("FALSE") is False
    assert settings._as_bool("faLSE") is False
    assert settings._as_bool("f") is False
    with pytest.raises(ValueError, match="invalid truth value"):
        settings._as_bool("")
    with pytest.raises(ValueError, match="invalid truth value falsey"):
        settings._as_bool("falsey")
    with pytest.raises(ValueError, match="invalid truth value truthy"):
        settings._as_bool("truthy")


def test_find_all_configs(tmpdir):
    setup_cfg = """
[isort]
profile=django
"""

    pyproject_toml = """
[tool.isort]
profile = "hug"
"""

    isort_cfg = """
[settings]
profile=black
"""

    pyproject_toml_broken = """
[tool.isorts]
something = nothing
"""

    dir1 = tmpdir / "subdir1"
    dir2 = tmpdir / "subdir2"
    dir3 = tmpdir / "subdir3"
    dir4 = tmpdir / "subdir4"

    dir1.mkdir()
    dir2.mkdir()
    dir3.mkdir()
    dir4.mkdir()

    setup_cfg_file = dir1 / "setup.cfg"
    setup_cfg_file.write_text(setup_cfg, "utf-8")

    pyproject_toml_file = dir2 / "pyproject.toml"
    pyproject_toml_file.write_text(pyproject_toml, "utf-8")

    isort_cfg_file = dir3 / ".isort.cfg"
    isort_cfg_file.write_text(isort_cfg, "utf-8")

    pyproject_toml_file_broken = dir4 / "pyproject.toml"
    pyproject_toml_file_broken.write_text(pyproject_toml_broken, "utf-8")

    config_trie = settings.find_all_configs(str(tmpdir))

    config_info_1 = config_trie.search(str(dir1 / "test1.py"))
    assert config_info_1[0] == str(setup_cfg_file)
    assert config_info_1[0] == str(setup_cfg_file)
    assert config_info_1[1]["profile"] == "django"

    config_info_2 = config_trie.search(str(dir2 / "test2.py"))
    assert config_info_2[0] == str(pyproject_toml_file)
    assert config_info_2[0] == str(pyproject_toml_file)
    assert config_info_2[1]["profile"] == "hug"

    config_info_3 = config_trie.search(str(dir3 / "test3.py"))
    assert config_info_3[0] == str(isort_cfg_file)
    assert config_info_3[0] == str(isort_cfg_file)
    assert config_info_3[1]["profile"] == "black"

    config_info_4 = config_trie.search(str(tmpdir / "file4.py"))
    assert config_info_4[0] == "default"
