import re
import zipfile

import pytest

from hatchling.builders.custom import CustomBuilder
from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT


def test_target_config_not_table(isolation):
    config = {"tool": {"hatch": {"build": {"targets": {"custom": 9000}}}}}

    with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.custom` must be a table"):
        CustomBuilder(str(isolation), config=config)


def test_no_path(isolation):
    config = {
        "tool": {
            "hatch": {
                "build": {"targets": {"custom": {"path": ""}}},
            },
        },
    }

    with pytest.raises(ValueError, match="Option `path` for builder `custom` must not be empty if defined"):
        CustomBuilder(str(isolation), config=config)


def test_path_not_string(isolation):
    config = {"tool": {"hatch": {"build": {"targets": {"custom": {"path": 3}}}}}}

    with pytest.raises(TypeError, match="Option `path` for builder `custom` must be a string"):
        CustomBuilder(str(isolation), config=config)


def test_nonexistent(isolation):
    config = {"tool": {"hatch": {"build": {"targets": {"custom": {"path": "test.py"}}}}}}

    with pytest.raises(OSError, match="Build script does not exist: test.py"):
        CustomBuilder(str(isolation), config=config)


def test_default(hatch, helpers, temp_dir, config_file):
    config_file.model.template.plugins["default"]["src-layout"] = False
    config_file.save()

    project_name = "My.App"

    with temp_dir.as_cwd():
        result = hatch("new", project_name)

    assert result.exit_code == 0, result.output

    project_path = temp_dir / "my-app"

    config = {
        "project": {"name": project_name, "dynamic": ["version"]},
        "tool": {
            "hatch": {
                "version": {"path": "my_app/__about__.py"},
                "build": {"targets": {"custom": {}}},
            },
        },
    }

    file_path = project_path / DEFAULT_BUILD_SCRIPT
    file_path.write_text(
        helpers.dedent(
            """
            import os

            from hatchling.builders.wheel import WheelBuilder

            def get_builder():
                return CustomWheelBuilder

            class CustomWheelBuilder(WheelBuilder):
                def build(self, **kwargs):
                    for i, artifact in enumerate(super().build(**kwargs)):
                        build_dir = os.path.dirname(artifact)
                        new_path = os.path.join(build_dir, f'{self.PLUGIN_NAME}-{i}.whl')
                        os.replace(artifact, new_path)
                        yield new_path
            """
        )
    )
    builder = CustomBuilder(str(project_path), config=config)

    build_path = project_path / "dist"

    with project_path.as_cwd():
        artifacts = list(builder.build())

    assert len(artifacts) == 1
    expected_artifact = artifacts[0]

    build_artifacts = list(build_path.iterdir())
    assert len(build_artifacts) == 1
    assert expected_artifact == str(build_artifacts[0])
    assert expected_artifact == str(build_path / "custom-0.whl")

    extraction_directory = temp_dir / "_archive"
    extraction_directory.mkdir()

    with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
        zip_archive.extractall(str(extraction_directory))

    metadata_directory = f"{builder.project_id}.dist-info"
    expected_files = helpers.get_template_files(
        "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
    )
    helpers.assert_files(extraction_directory, expected_files)

    # Inspect the archive rather than the extracted files because on Windows they lose their metadata
    # https://stackoverflow.com/q/9813243
    with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
        zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
        assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)


def test_explicit_path(hatch, helpers, temp_dir, config_file):
    config_file.model.template.plugins["default"]["src-layout"] = False
    config_file.save()

    project_name = "My.App"

    with temp_dir.as_cwd():
        result = hatch("new", project_name)

    assert result.exit_code == 0, result.output

    project_path = temp_dir / "my-app"

    config = {
        "project": {"name": project_name, "dynamic": ["version"]},
        "tool": {
            "hatch": {
                "version": {"path": "my_app/__about__.py"},
                "build": {"targets": {"custom": {"path": f"foo/{DEFAULT_BUILD_SCRIPT}"}}},
            },
        },
    }

    file_path = project_path / "foo" / DEFAULT_BUILD_SCRIPT
    file_path.ensure_parent_dir_exists()
    file_path.write_text(
        helpers.dedent(
            """
            import os

            from hatchling.builders.wheel import WheelBuilder

            def get_builder():
                return CustomWheelBuilder

            class CustomWheelBuilder(WheelBuilder):
                def build(self, **kwargs):
                    for i, artifact in enumerate(super().build(**kwargs)):
                        build_dir = os.path.dirname(artifact)
                        new_path = os.path.join(build_dir, f'{self.PLUGIN_NAME}-{i}.whl')
                        os.replace(artifact, new_path)
                        yield new_path
            """
        )
    )
    builder = CustomBuilder(str(project_path), config=config)

    build_path = project_path / "dist"

    with project_path.as_cwd():
        artifacts = list(builder.build())

    assert len(artifacts) == 1
    expected_artifact = artifacts[0]

    build_artifacts = list(build_path.iterdir())
    assert len(build_artifacts) == 1
    assert expected_artifact == str(build_artifacts[0])
    assert expected_artifact == str(build_path / "custom-0.whl")

    extraction_directory = temp_dir / "_archive"
    extraction_directory.mkdir()

    with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
        zip_archive.extractall(str(extraction_directory))

    metadata_directory = f"{builder.project_id}.dist-info"
    expected_files = helpers.get_template_files(
        "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
    )
    helpers.assert_files(extraction_directory, expected_files)

    # Inspect the archive rather than the extracted files because on Windows they lose their metadata
    # https://stackoverflow.com/q/9813243
    with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
        zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
        assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)


def test_no_subclass(hatch, helpers, temp_dir):
    project_name = "My.App"

    with temp_dir.as_cwd():
        result = hatch("new", project_name)

    assert result.exit_code == 0, result.output

    project_path = temp_dir / "my-app"

    config = {
        "project": {"name": project_name, "dynamic": ["version"]},
        "tool": {
            "hatch": {
                "version": {"path": "my_app/__about__.py"},
                "build": {"targets": {"custom": {"path": f"foo/{DEFAULT_BUILD_SCRIPT}"}}},
            },
        },
    }

    file_path = project_path / "foo" / DEFAULT_BUILD_SCRIPT
    file_path.ensure_parent_dir_exists()
    file_path.write_text(
        helpers.dedent(
            """
            from hatchling.builders.plugin.interface import BuilderInterface

            foo = None
            bar = 'baz'

            class CustomBuilder:
                pass
            """
        )
    )

    with (
        pytest.raises(
            ValueError,
            match=re.escape(
                f"Unable to find a subclass of `BuilderInterface` in `foo/{DEFAULT_BUILD_SCRIPT}`: {temp_dir}"
            ),
        ),
        project_path.as_cwd(),
    ):
        CustomBuilder(str(project_path), config=config)


def test_multiple_subclasses(hatch, helpers, temp_dir):
    project_name = "My.App"

    with temp_dir.as_cwd():
        result = hatch("new", project_name)

    assert result.exit_code == 0, result.output

    project_path = temp_dir / "my-app"

    config = {
        "project": {"name": project_name, "dynamic": ["version"]},
        "tool": {
            "hatch": {
                "version": {"path": "my_app/__about__.py"},
                "build": {"targets": {"custom": {"path": f"foo/{DEFAULT_BUILD_SCRIPT}"}}},
            },
        },
    }

    file_path = project_path / "foo" / DEFAULT_BUILD_SCRIPT
    file_path.ensure_parent_dir_exists()
    file_path.write_text(
        helpers.dedent(
            """
            import os

            from hatchling.builders.wheel import WheelBuilder

            class CustomWheelBuilder(WheelBuilder):
                pass
            """
        )
    )

    with (
        pytest.raises(
            ValueError,
            match=re.escape(
                f"Multiple subclasses of `BuilderInterface` found in `foo/{DEFAULT_BUILD_SCRIPT}`, select "
                f"one by defining a function named `get_builder`: {temp_dir}"
            ),
        ),
        project_path.as_cwd(),
    ):
        CustomBuilder(str(project_path), config=config)


def test_dynamic_dependencies(hatch, helpers, temp_dir, config_file):
    config_file.model.template.plugins["default"]["src-layout"] = False
    config_file.save()

    project_name = "My.App"

    with temp_dir.as_cwd():
        result = hatch("new", project_name)

    assert result.exit_code == 0, result.output

    project_path = temp_dir / "my-app"

    config = {
        "project": {"name": project_name, "dynamic": ["version"]},
        "tool": {
            "hatch": {
                "version": {"path": "my_app/__about__.py"},
                "build": {
                    "targets": {"custom": {"dependencies": ["foo"], "hooks": {"custom": {"dependencies": ["bar"]}}}}
                },
            },
        },
    }

    file_path = project_path / DEFAULT_BUILD_SCRIPT
    file_path.write_text(
        helpers.dedent(
            """
            from hatchling.builders.hooks.plugin.interface import BuildHookInterface
            from hatchling.builders.wheel import WheelBuilder

            def get_builder():
                return CustomWheelBuilder

            class CustomWheelBuilder(WheelBuilder):
                pass

            class CustomHook(BuildHookInterface):
                def dependencies(self):
                    return ['baz']
            """
        )
    )
    builder = CustomBuilder(str(project_path), config=config)

    assert builder.config.dependencies == ["foo", "bar", "baz"]
