import pytest
import tomli

from ini2toml.plugins.profile_independent_tasks import remove_empty_table_headers
from ini2toml.plugins.setuptools_pep621 import (
    Directive,
    SetuptoolsPEP621,
    _ConfusingPackagesConfig,
    activate,
)
from ini2toml.translator import Translator


@pytest.fixture
def plugin():
    return SetuptoolsPEP621()


@pytest.fixture
def translator(plugin):
    return Translator(plugins=[activate], ini_parser_opts=dict(delimiters=("=", ": ")))


@pytest.fixture
def parse(translator):
    return lambda text: translator.loads(text)


@pytest.fixture
def convert(translator):
    return lambda irepr: translator.dumps(irepr)


example_normalise_keys = """\
[metadata]
summary = Automatically translates .cfg/.ini files into TOML
author_email = example@example
license-file = LICENSE.txt
long_description_content_type = text/x-rst; charset=UTF-8
home_page = https://github.com/abravalheri/ini2toml/
classifier = Development Status :: 4 - Beta
platform = any
"""
expected_normalise_keys = """\
[metadata]
description = "Automatically translates .cfg/.ini files into TOML"
author-email = "example@example"
license-files = "LICENSE.txt"
long-description-content-type = "text/x-rst; charset=UTF-8"
url = "https://github.com/abravalheri/ini2toml/"
classifiers = "Development Status :: 4 - Beta"
platforms = "any"
"""


def test_normalise_keys(plugin, parse, convert):
    obj = parse(example_normalise_keys)
    with pytest.warns(DeprecationWarning):
        obj = plugin.normalise_keys(obj)
    assert convert(obj) == expected_normalise_keys


# ----


example_convert_directives = """\
[metadata]
version = attr: mymodule.myfunc
classifiers = file: CLASSIFIERS.txt
description = file: README.txt

[options]
entry-points = file: ENTRYPOINTS.txt
packages = find_namespace:
"""
expected_convert_directives = """\
[metadata]
version = {attr = "mymodule.myfunc"}
classifiers = {file = ["CLASSIFIERS.txt"]}
description = {file = ["README.txt"]}

[options]
entry-points = {file = ["ENTRYPOINTS.txt"]}
packages = {find_namespace = ""}
"""


def test_convert_directives(plugin, parse, convert):
    doc = parse(example_convert_directives)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.apply_value_processing(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc) == expected_convert_directives


# ----


example_apply_value_processing = """\
[metadata]
version = 5.3  # comment
classifiers =
    Development Status :: 4 - Beta
    Programming Language :: Python
keywords = python, module
[options]
zip-safe = False  # comment
package-dir =
    =src
install-requires =
    importlib-metadata; python_version<"3.8"
    configupdater>=3,<=4
cmdclass =
    customcmd = custom_build.CustomCmd
    other = custom_build.OtherCmd  # Some command
[options.entry-points]
# For example:
console-scripts =
    putup = pyscaffold.cli:run  # CLI exec
pyscaffold.cli =
    # comment
    fibonacci = ini2toml.skeleton:run # comment
    awesome = pyscaffoldext.awesome.extension:AwesomeExtension
"""
expected_apply_value_processing = """\
[metadata]
version = "5.3" # comment
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python",
]
keywords = ["python", "module"]

[options]
zip-safe = false # comment
package-dir = {"" = "src"}
install-requires = [
    'importlib-metadata; python_version<"3.8"',
    "configupdater>=3,<=4",
]

[options.cmdclass]
customcmd = "custom_build.CustomCmd"
other = "custom_build.OtherCmd" # Some command

["project:entry-points"]
# For example:

["project:entry-points"."pyscaffold.cli"]
# comment
fibonacci = "ini2toml.skeleton:run" # comment
awesome = "pyscaffoldext.awesome.extension:AwesomeExtension"

["project:scripts"]
putup = "pyscaffold.cli:run" # CLI exec
"""


def test_move_entry_points_and_apply_value_processing(plugin, parse, convert):
    doc = parse(example_apply_value_processing)
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.move_and_split_entrypoints(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    text = convert(doc).strip()
    assert text == expected_apply_value_processing.strip()


# ----

example_split_subtables = """\
[options.packages.find]
where = src
[project:entry-points]
# For example:
[project:scripts]
django-admin = django.core.management:execute_from_command_line
"""

expected_split_subtables = """\
[tool.setuptools.packages.find]
where = "src"

[project.entry-points]
# For example:

[project.scripts]
django-admin = "django.core.management:execute_from_command_line"
"""


def test_split_subtables(plugin, parse, convert):
    doc = parse(example_split_subtables.strip())
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.split_subtables(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    converted = convert(doc).strip()
    assert remove_empty_table_headers(converted) == expected_split_subtables.strip()


# ----

example_entrypoints_and_split_subtables = """\
[options.packages.find]
where = src
[options.entry-points]
# For example:
console-scripts =
    django-admin = django.core.management:execute_from_command_line
gui-scripts =
    project = my.module:function [extra-dep]
pyscaffold.cli =
    config = pyscaffold.extensions.config:Config
    interactive = pyscaffold.extensions.interactive:Interactive
"""

expected_entrypoints_and_split_subtables = """\
[tool.setuptools.packages.find]
where = "src"

[project.entry-points]
# For example:

[project.entry-points."pyscaffold.cli"]
config = "pyscaffold.extensions.config:Config"
interactive = "pyscaffold.extensions.interactive:Interactive"

[project.scripts]
django-admin = "django.core.management:execute_from_command_line"

[project.gui-scripts]
project = "my.module:function [extra-dep]"
"""


def test_entrypoints_and_split_subtables(plugin, parse, convert):
    doc = parse(example_entrypoints_and_split_subtables.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.move_and_split_entrypoints(doc)
    doc = plugin.split_subtables(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    converted = convert(doc).strip()
    expected = expected_entrypoints_and_split_subtables.strip()
    assert remove_empty_table_headers(converted) == expected


# ----


example_handle_license_files = """\
[metadata]
license-files = LICENSE.txt
"""

expected_handle_license_files = """\
[metadata]

[options]
license-files = ["LICENSE.txt"]
"""


def test_handle_license_files(plugin, parse, convert):
    doc = parse(example_handle_license_files.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_license(doc)
    doc = plugin.remove_metadata_not_in_pep621(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_handle_license_files.strip()


example_handle_multiple_license_files = """\
[metadata]
license-files = LICENSE.txt, NOTICE.txt
"""

expected_handle_multiple_license_files = """\
[metadata]

[options]
license-files = ["LICENSE.txt", "NOTICE.txt"]
"""


def test_handle_multiple_license_files(plugin, parse, convert):
    doc = parse(example_handle_multiple_license_files.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_license(doc)
    doc = plugin.remove_metadata_not_in_pep621(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_handle_multiple_license_files.strip()


example_handle_license = """\
[metadata]
license = MPL-2.0
"""

expected_handle_license = """\
[metadata]
license = {text = "MPL-2.0"}
"""


def test_handle_license(plugin, parse, convert):
    doc = parse(example_handle_license.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_license(doc)
    doc = plugin.remove_metadata_not_in_pep621(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_handle_license.strip()


example_handle_license_and_files = """\
[metadata]
license = MPL-2.0  # comment
license-files = LICENSE.txt
"""

expected_handle_license_and_files = """\
[metadata]
license = {text = "MPL-2.0"} # comment

[options]
license-files = ["LICENSE.txt"]
"""


def test_handle_license_and_files(plugin, parse, convert):
    doc = parse(example_handle_license_and_files.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_license(doc)
    doc = plugin.remove_metadata_not_in_pep621(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_handle_license_and_files.strip()


# ----


example_handle_packages_find = """\
[options]
packages = find_namespace:
[options.packages.find]
where = src
exclude =
    tests
"""

expected_handle_packages_find = """\
[options]

["options.packages.find"]
where = ["src"]
exclude = ["tests"]
namespaces = true
"""


def test_handle_packages_find(plugin, parse, convert):
    doc = parse(example_handle_packages_find.strip())
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_packages_find(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_handle_packages_find.strip()


# ----

example_fix_extras_require = """\
[options.extras-require]
pyarrow:python_version>'3.4' =
    pyarray>=1.0.0,<2.0dev; os_name=='posix'
    colorama; os_name=='nt'
"""

expected_fix_extras_require = """\
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

["project:optional-dependencies"]
pyarrow = [
    "pyarray>=1.0.0,<2.0dev; os_name=='posix' and python_version>'3.4'",
    "colorama; os_name=='nt' and python_version>'3.4'",
]
"""


def test_fix_extras_require(plugin, parse, convert):
    doc = plugin.template()
    doc.update(parse(example_fix_extras_require.strip()))
    doc = plugin.apply_value_processing(doc)
    doc = plugin.move_options_missing_in_pep621(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.fix_extras_require(doc)
    doc.pop("tool", None)
    doc.pop("options", None)
    doc.pop("metadata", None)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    text = convert(doc).strip()
    assert text == expected_fix_extras_require.strip()


# ----


example_move_setup_requires = """\
[options]
setup-requires =
    setuptools>=46.1.0
    setuptools_scm>=5
"""

expected_move_setup_requires = """\
[build-system]
requires = [
    "setuptools>=46.1.0",
    "setuptools_scm>=5",
]
build-backend = "setuptools.build_meta"
"""


def test_move_setup_requires(plugin, parse, convert):
    doc = plugin.template()
    doc.update(parse(example_move_setup_requires.strip()))
    doc = plugin.apply_value_processing(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    with pytest.warns(DeprecationWarning, match="'setup_requires' is deprecated"):
        doc = plugin.move_setup_requires(doc)
    doc.pop("tool", None)
    doc.pop("options", None)
    doc.pop("metadata", None)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    text = convert(doc).strip()
    assert text == expected_move_setup_requires.strip()


# ----


example_move_tests_require1 = """\
[options]
tests-require =
    pytest
    pytest-cov
"""

expected_move_tests_require1 = """\
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

["project:optional-dependencies"]
testing = [
    "pytest",
    "pytest-cov",
]
"""

example_move_tests_require2 = """\
[options]
tests-require =
    pytest
    pytest-cov

[options.extras-require]
testing =
    pytest>=6.2.5
"""

expected_move_tests_require2 = """\
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

["project:optional-dependencies"]
testing = [
    "pytest>=6.2.5",
    "pytest-cov",
]
"""


def test_move_tests_require(plugin, parse, convert):
    for example, expected in [
        (example_move_tests_require1, expected_move_tests_require1),
        (example_move_tests_require2, expected_move_tests_require2),
    ]:
        doc = plugin.template()
        doc.update(parse(example.strip()))
        doc = plugin.apply_value_processing(doc)
        doc = plugin.move_options_missing_in_pep621(doc)
        print(doc)
        print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
        with pytest.warns(DeprecationWarning, match="'tests_require' is deprecated"):
            doc = plugin.move_tests_require(doc)
        doc.pop("tool", None)
        doc.pop("options", None)
        doc.pop("metadata", None)
        print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
        print(doc)
        text = convert(doc).strip()
        assert text == expected.strip()


# ----


example_dynamic = """\
[metadata]
version = attr: django.__version__
classifiers = file: classifiers.txt
description = file: readme.txt
long-description = file: readme.txt, desc.txt
long-description-content-type: plain/text

[options]
entry-points = file: entry-points.txt
"""

expected_dynamic = """\
[metadata]

dynamic = [
    "readme",
    "version",
    "classifiers",
    "description",
    "entry-points",
    "scripts",
    "gui-scripts",
]

["options.dynamic"]
readme = {file = ["readme.txt", "desc.txt"], content-type = "plain/text"}
version = {attr = "django.__version__"}
classifiers = {file = ["classifiers.txt"]}
description = {file = ["readme.txt"]}
entry-points = {file = ["entry-points.txt"]}
"""  # noqa


def test_directives(plugin, parse, convert):
    doc = parse(example_dynamic.strip())
    doc = plugin.apply_value_processing(doc)
    doc = plugin.move_options_missing_in_pep621(doc)
    assert isinstance(doc["metadata"]["version"], Directive)
    assert doc["metadata"]["version"].kind == "attr"

    assert isinstance(doc["metadata"]["classifiers"], Directive)

    assert isinstance(doc["metadata"]["description"], Directive)
    assert doc["metadata"]["description"].kind == "file"

    assert isinstance(doc["metadata"]["entry-points"], Directive)


def test_handle_dynamic(plugin, parse, convert):
    doc = parse(example_dynamic.strip())
    doc = plugin.apply_value_processing(doc)
    doc = plugin.merge_and_rename_long_description_and_content_type(doc)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.handle_dynamic(doc)
    doc.pop("tool", None)
    doc.pop("options", None)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc).strip() == expected_dynamic.strip()


# ----

example_dynamic_dependencies = """\
[options]
install_requires = file: requirements.txt
[options.extras_require]
dev = file: dev-requirements.txt
"""

expected_dynamic_dependencies = """\
[project]
dynamic = ["version", "dependencies", "optional-dependencies"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies.dev = {file = ["dev-requirements.txt"]}
"""


def test_dynamic_dependecies(plugin, parse, convert):
    doc = parse(example_dynamic_dependencies)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.normalise_keys(doc)
    doc = plugin.pep621_transform(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    converted = tomli.loads(convert(doc).strip())
    converted.pop("build-system", None)
    converted["tool"]["setuptools"].pop("include-package-data", None)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(converted)
    assert converted == tomli.loads(expected_dynamic_dependencies)


# ----

expected_empty = """\
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]

[tool.setuptools]
include-package-data = false
"""


def test_empty(translator, plugin, parse, convert):
    doc = parse("")
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.normalise_keys(doc)
    doc = plugin.pep621_transform(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    converted = convert(doc).strip()
    assert remove_empty_table_headers(converted) == expected_empty.strip()

    # Same thing but with the higher level API:
    text = translator.translate("", profile_name="setup.cfg")
    assert text.strip() == expected_empty.strip()


# ----


example_data_files = """
[options]
data-files =
    a = b
"""

expected_data_files = """\
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]

[tool.setuptools]
data-files = {a = ["b"]}
include-package-data = false
"""


def test_data_files(translator):
    # Same thing but with the higher level API:
    with pytest.warns(DeprecationWarning, match="'data-files' is deprecated"):
        text = translator.translate(example_data_files, profile_name="setup.cfg")
        assert remove_empty_table_headers(text.strip()) == expected_data_files.strip()


# ----

# Example taken from https://github.com/jaraco/zipp
example_deps_with_line_continuation = """
[options.extras_require]
testing =
    # upstream
    pytest >= 6
    pytest-checkdocs >= 2.4
    pytest-flake8
    pytest-black >= 0.3.7; \
        # workaround for jaraco/skeleton#22
        python_implementation != "PyPy"
    pytest-cov
    pytest-mypy; \
        # workaround for jaraco/skeleton#22
        # comment have several lines
        python_implementation != "PyPy" and \
        # another comment
        os_name == "unix"
    pytest-enabler >= 1.0.1
"""

expected_deps_with_line_continuation = """
["options.extras_require"]
testing = [
    # upstream
    "pytest >= 6",
    "pytest-checkdocs >= 2.4"
    "pytest-flake8",
    "pytest-black >= 0.3.7; python_implementation != \"PyPy\"", # workaround for jaraco/skeleton#22
    "pytest-cov",
    "pytest-mypy; python_implementation != \"PyPy\" and os_name == \"unix\"",
    # workaround for jaraco/skeleton#22
    # comment have several lines
    # another comment
    pytest-enabler >= 1.0.1
"""  # noqa


def test_deps_with_line_continuation(plugin, parse, convert):
    doc = parse(example_convert_directives)
    print(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    doc = plugin.apply_value_processing(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc) == expected_convert_directives


# ----

# Example taken from https://github.com/aio-libs/aiohttp/
# In general, you should either list packages explicit or use find,
# but some configs change and `options.packages.find` is left behind
example_explicit_packages_with_find = """\
[options]
packages = aiohttp
[options.packages.find]
exclude =
  examples
"""

# In those cases, `ini2toml` should remove  `[options.packages.find]`
# And warn about it
expected_explicit_packages_with_find = """\
[tool.setuptools]
packages = ["aiohttp"]
"""


def test_explicit_packages_with_find(plugin, parse, convert):
    doc = parse(example_explicit_packages_with_find)
    print(doc)
    doc = plugin.apply_value_processing(doc)
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    with pytest.warns(_ConfusingPackagesConfig, match="[options.packages.find]"):
        doc = plugin.handle_packages_find(doc)
    doc.rename("options", ("tool", "setuptools"))
    print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
    print(doc)
    assert convert(doc) == expected_explicit_packages_with_find
