import os
import re
from os.path import join as pjoin

import pathspec
import pytest

from hatch.utils.structures import EnvVars
from hatchling.builders.constants import BuildEnvVars

from .utils import MockBuilder


class TestDirectory:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.directory == builder.config.directory == str(isolation / "dist")

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"directory": "bar"}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.directory == str(isolation / "bar")

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"directory": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.directory` must be a string"):
            _ = builder.config.directory

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"directory": "bar"}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.directory == str(isolation / "bar")

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"directory": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.directory` must be a string"):
            _ = builder.config.directory

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"directory": "bar", "targets": {"foo": {"directory": "baz"}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.directory == str(isolation / "baz")

    def test_absolute_path(self, isolation):
        absolute_path = str(isolation)
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"directory": absolute_path}}}}}}
        builder = MockBuilder(absolute_path, config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.directory == absolute_path


class TestSkipExcludedDirs:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.skip_excluded_dirs is builder.config.skip_excluded_dirs is False

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"skip-excluded-dirs": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.skip_excluded_dirs is True

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"skip-excluded-dirs": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.foo.skip-excluded-dirs` must be a boolean"
        ):
            _ = builder.config.skip_excluded_dirs

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"skip-excluded-dirs": True}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.skip_excluded_dirs is True

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"skip-excluded-dirs": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.skip-excluded-dirs` must be a boolean"):
            _ = builder.config.skip_excluded_dirs

    def test_target_overrides_global(self, isolation):
        config = {
            "tool": {
                "hatch": {"build": {"skip-excluded-dirs": True, "targets": {"foo": {"skip-excluded-dirs": False}}}}
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.skip_excluded_dirs is False


class TestIgnoreVCS:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.ignore_vcs is builder.config.ignore_vcs is False

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"ignore-vcs": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.ignore_vcs is True

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"ignore-vcs": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.ignore-vcs` must be a boolean"):
            _ = builder.config.ignore_vcs

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"ignore-vcs": True}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.ignore_vcs is True

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"ignore-vcs": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.ignore-vcs` must be a boolean"):
            _ = builder.config.ignore_vcs

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"ignore-vcs": True, "targets": {"foo": {"ignore-vcs": False}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.ignore_vcs is False


class TestRequireRuntimeDependencies:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.require_runtime_dependencies is builder.config.require_runtime_dependencies is False

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-dependencies": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.require_runtime_dependencies is True

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-dependencies": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError,
            match="Field `tool.hatch.build.targets.foo.require-runtime-dependencies` must be a boolean",
        ):
            _ = builder.config.require_runtime_dependencies

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"require-runtime-dependencies": True}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.require_runtime_dependencies is True

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"require-runtime-dependencies": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.require-runtime-dependencies` must be a boolean"):
            _ = builder.config.require_runtime_dependencies

    def test_target_overrides_global(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "require-runtime-dependencies": True,
                        "targets": {"foo": {"require-runtime-dependencies": False}},
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.require_runtime_dependencies is False


class TestRequireRuntimeFeatures:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.require_runtime_features == builder.config.require_runtime_features == []

    def test_target(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo": [], "bar": []}},
            "tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-features": ["foo", "bar"]}}}}},
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.require_runtime_features == ["foo", "bar"]

    def test_target_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-features": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.foo.require-runtime-features` must be an array"
        ):
            _ = builder.config.require_runtime_features

    def test_target_feature_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-features": [9000]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError,
            match="Feature #1 of field `tool.hatch.build.targets.foo.require-runtime-features` must be a string",
        ):
            _ = builder.config.require_runtime_features

    def test_target_feature_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-features": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match=(
                "Feature #1 of field `tool.hatch.build.targets.foo.require-runtime-features` cannot be an empty string"
            ),
        ):
            _ = builder.config.require_runtime_features

    def test_target_feature_unknown(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"foo": {"require-runtime-features": ["foo_bar"]}}}}},
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match=(
                "Feature `foo-bar` of field `tool.hatch.build.targets.foo.require-runtime-features` is not defined in "
                "field `project.optional-dependencies`"
            ),
        ):
            _ = builder.config.require_runtime_features

    def test_global(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo": [], "bar": []}},
            "tool": {"hatch": {"build": {"require-runtime-features": ["foo", "bar"]}}},
        }
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.require_runtime_features == ["foo", "bar"]

    def test_global_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"require-runtime-features": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.require-runtime-features` must be an array"):
            _ = builder.config.require_runtime_features

    def test_global_feature_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"require-runtime-features": [9000]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Feature #1 of field `tool.hatch.build.require-runtime-features` must be a string"
        ):
            _ = builder.config.require_runtime_features

    def test_global_feature_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"require-runtime-features": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match="Feature #1 of field `tool.hatch.build.require-runtime-features` cannot be an empty string",
        ):
            _ = builder.config.require_runtime_features

    def test_global_feature_unknown(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"require-runtime-features": ["foo_bar"]}}},
        }
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match=(
                "Feature `foo-bar` of field `tool.hatch.build.require-runtime-features` is not defined in "
                "field `project.optional-dependencies`"
            ),
        ):
            _ = builder.config.require_runtime_features

    def test_target_overrides_global(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo_bar": [], "bar_baz": []}},
            "tool": {
                "hatch": {
                    "build": {
                        "require-runtime-features": ["bar_baz"],
                        "targets": {"foo": {"require-runtime-features": ["foo_bar"]}},
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.require_runtime_features == ["foo-bar"]


class TestOnlyPackages:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.only_packages is builder.config.only_packages is False

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-packages": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.only_packages is True

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-packages": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.only-packages` must be a boolean"):
            _ = builder.config.only_packages

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-packages": True}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.only_packages is True

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-packages": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.only-packages` must be a boolean"):
            _ = builder.config.only_packages

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-packages": True, "targets": {"foo": {"only-packages": False}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.only_packages is False


class TestReproducible:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.reproducible is builder.config.reproducible is True

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"reproducible": False}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.reproducible is False

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"reproducible": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.reproducible` must be a boolean"):
            _ = builder.config.reproducible

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"reproducible": False}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.reproducible is False

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"reproducible": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.reproducible` must be a boolean"):
            _ = builder.config.reproducible

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"reproducible": False, "targets": {"foo": {"reproducible": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.reproducible is True


class TestDevModeDirs:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.dev_mode_dirs == builder.config.dev_mode_dirs == []

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-dirs": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.dev-mode-dirs` must be an array of strings"):
            _ = builder.config.dev_mode_dirs

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-dirs": ["foo", "bar/baz"]}}}}
        builder = MockBuilder(str(isolation), config=config)

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

    def test_global_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-dirs": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Directory #1 in field `tool.hatch.build.dev-mode-dirs` must be a string"):
            _ = builder.config.dev_mode_dirs

    def test_global_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-dirs": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Directory #1 in field `tool.hatch.build.dev-mode-dirs` cannot be an empty string"
        ):
            _ = builder.config.dev_mode_dirs

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dev-mode-dirs": ["foo", "bar/baz"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_target_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dev-mode-dirs": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Directory #1 in field `tool.hatch.build.targets.foo.dev-mode-dirs` must be a string"
        ):
            _ = builder.config.dev_mode_dirs

    def test_target_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dev-mode-dirs": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match="Directory #1 in field `tool.hatch.build.targets.foo.dev-mode-dirs` cannot be an empty string",
        ):
            _ = builder.config.dev_mode_dirs

    def test_target_overrides_global(self, isolation):
        config = {
            "tool": {"hatch": {"build": {"dev-mode-dirs": ["foo"], "targets": {"foo": {"dev-mode-dirs": ["bar"]}}}}}
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dev_mode_dirs == ["bar"]


class TestDevModeExact:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.dev_mode_exact is builder.config.dev_mode_exact is False

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dev-mode-exact": True}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dev_mode_exact is True

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dev-mode-exact": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.dev-mode-exact` must be a boolean"):
            _ = builder.config.dev_mode_exact

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-exact": True}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dev_mode_exact is True

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-exact": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.dev-mode-exact` must be a boolean"):
            _ = builder.config.dev_mode_exact

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"dev-mode-exact": True, "targets": {"foo": {"dev-mode-exact": False}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dev_mode_exact is False


class TestPackages:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.packages == builder.config.packages == []

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"packages": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.packages` must be an array of strings"):
            _ = builder.config.packages

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"packages": ["src/foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.packages) == 1
        assert builder.config.packages[0] == pjoin("src", "foo")

    def test_global_package_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"packages": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Package #1 in field `tool.hatch.build.packages` must be a string"):
            _ = builder.config.packages

    def test_global_package_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"packages": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Package #1 in field `tool.hatch.build.packages` cannot be an empty string"
        ):
            _ = builder.config.packages

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"packages": ["src/foo"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.packages) == 1
        assert builder.config.packages[0] == pjoin("src", "foo")

    def test_target_package_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"packages": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Package #1 in field `tool.hatch.build.targets.foo.packages` must be a string"
        ):
            _ = builder.config.packages

    def test_target_package_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"packages": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Package #1 in field `tool.hatch.build.targets.foo.packages` cannot be an empty string"
        ):
            _ = builder.config.packages

    def test_target_overrides_global(self, isolation):
        config = {
            "tool": {"hatch": {"build": {"packages": ["src/foo"], "targets": {"foo": {"packages": ["pkg/foo"]}}}}}
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.packages) == 1
        assert builder.config.packages[0] == pjoin("pkg", "foo")

    def test_no_source(self, isolation):
        config = {"tool": {"hatch": {"build": {"packages": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.packages) == 1
        assert builder.config.packages[0] == pjoin("foo")


class TestSources:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.sources == builder.config.sources == {}
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("src", "foo", "bar.py")

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.sources` must be a mapping or array of strings"):
            _ = builder.config.sources

    def test_global_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": ["src"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "")] == ""
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("foo", "bar.py")

    def test_global_array_source_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Source #1 in field `tool.hatch.build.sources` must be a string"):
            _ = builder.config.sources

    def test_global_array_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(ValueError, match="Source #1 in field `tool.hatch.build.sources` cannot be an empty string"):
            _ = builder.config.sources

    def test_global_mapping(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": {"src/foo": "renamed"}}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "foo", "")] == pjoin("renamed", "")
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("renamed", "bar.py")

    def test_global_mapping_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": {"": "renamed"}}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[""] == pjoin("renamed", "")
        assert builder.config.get_distribution_path("bar.py") == pjoin("renamed", "bar.py")
        assert builder.config.get_distribution_path(pjoin("foo", "bar.py")) == pjoin("renamed", "foo", "bar.py")

    def test_global_mapping_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": {"src/foo": ""}}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "foo", "")] == ""
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == "bar.py"

    def test_global_mapping_replacement_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": {"src/foo": 0}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Path for source `src/foo` in field `tool.hatch.build.sources` must be a string"
        ):
            _ = builder.config.sources

    def test_target_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": ""}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.foo.sources` must be a mapping or array of strings"
        ):
            _ = builder.config.sources

    def test_target_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": ["src"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "")] == ""
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("foo", "bar.py")

    def test_target_array_source_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Source #1 in field `tool.hatch.build.targets.foo.sources` must be a string"
        ):
            _ = builder.config.sources

    def test_target_array_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Source #1 in field `tool.hatch.build.targets.foo.sources` cannot be an empty string"
        ):
            _ = builder.config.sources

    def test_target_mapping(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": {"src/foo": "renamed"}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "foo", "")] == pjoin("renamed", "")
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("renamed", "bar.py")

    def test_target_mapping_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": {"": "renamed"}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.sources) == 1
        assert builder.config.sources[""] == pjoin("renamed", "")
        assert builder.config.get_distribution_path(pjoin("bar.py")) == pjoin("renamed", "bar.py")

    def test_target_mapping_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": {"src/foo": ""}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "foo", "")] == ""
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == "bar.py"

    def test_target_mapping_replacement_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"sources": {"src/foo": 0}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError,
            match="Path for source `src/foo` in field `tool.hatch.build.targets.foo.sources` must be a string",
        ):
            _ = builder.config.sources

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": ["src"], "targets": {"foo": {"sources": ["pkg"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("pkg", "")] == ""
        assert builder.config.get_distribution_path(pjoin("pkg", "foo", "bar.py")) == pjoin("foo", "bar.py")
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("src", "foo", "bar.py")

    def test_no_source(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": ["bar"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("bar", "")] == ""
        assert builder.config.get_distribution_path(pjoin("foo", "bar.py")) == pjoin("foo", "bar.py")

    def test_compatible_with_packages(self, isolation):
        config = {"tool": {"hatch": {"build": {"sources": {"src/foo": "renamed"}, "packages": ["src/foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert len(builder.config.sources) == 1
        assert builder.config.sources[pjoin("src", "foo", "")] == pjoin("renamed", "")
        assert builder.config.get_distribution_path(pjoin("src", "foo", "bar.py")) == pjoin("renamed", "bar.py")


class TestForceInclude:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.force_include == builder.config.force_include == {}

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.force-include` must be a mapping"):
            _ = builder.config.force_include

    def test_global_absolute(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": {str(isolation / "source"): "/target/"}}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.force_include == {str(isolation / "source"): "target"}

    def test_global_relative(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": {"../source": "/target/"}}}}}
        builder = MockBuilder(str(isolation / "foo"), config=config)

        assert builder.config.force_include == {str(isolation / "source"): "target"}

    def test_global_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": {"": "/target/"}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Source #1 in field `tool.hatch.build.force-include` cannot be an empty string"
        ):
            _ = builder.config.force_include

    def test_global_relative_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": {"source": 0}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Path for source `source` in field `tool.hatch.build.force-include` must be a string"
        ):
            _ = builder.config.force_include

    def test_global_relative_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"force-include": {"source": ""}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match="Path for source `source` in field `tool.hatch.build.force-include` cannot be an empty string",
        ):
            _ = builder.config.force_include

    def test_target_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"force-include": ""}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.force-include` must be a mapping"):
            _ = builder.config.force_include

    def test_target_absolute(self, isolation):
        config = {
            "tool": {
                "hatch": {"build": {"targets": {"foo": {"force-include": {str(isolation / "source"): "/target/"}}}}}
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.force_include == {str(isolation / "source"): "target"}

    def test_target_relative(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"force-include": {"../source": "/target/"}}}}}}}
        builder = MockBuilder(str(isolation / "foo"), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.force_include == {str(isolation / "source"): "target"}

    def test_target_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"force-include": {"": "/target/"}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match="Source #1 in field `tool.hatch.build.targets.foo.force-include` cannot be an empty string",
        ):
            _ = builder.config.force_include

    def test_target_relative_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"force-include": {"source": 0}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError,
            match="Path for source `source` in field `tool.hatch.build.targets.foo.force-include` must be a string",
        ):
            _ = builder.config.force_include

    def test_target_relative_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"force-include": {"source": ""}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match=(
                "Path for source `source` in field `tool.hatch.build.targets.foo.force-include` "
                "cannot be an empty string"
            ),
        ):
            _ = builder.config.force_include

    def test_order(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "force-include": {
                            "../very-nested": "target1/embedded",
                            "../source1": "/target2/",
                            "../source2": "/target1/",
                        }
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation / "foo"), config=config)

        assert builder.config.force_include == {
            str(isolation / "source2"): "target1",
            str(isolation / "very-nested"): f"target1{os.sep}embedded",
            str(isolation / "source1"): "target2",
        }


class TestOnlyInclude:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.only_include == builder.config.only_include == {}

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-include": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.only-include` must be an array"):
            _ = builder.config.only_include

    def test_global_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-include": [9000]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Path #1 in field `tool.hatch.build.only-include` must be a string"):
            _ = builder.config.only_include

    @pytest.mark.parametrize("path", ["/", "~/foo", "../foo"])
    def test_global_not_relative(self, isolation, path):
        config = {"tool": {"hatch": {"build": {"only-include": [path]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match=f"Path #1 in field `tool.hatch.build.only-include` must be relative: {path}"
        ):
            _ = builder.config.only_include

    def test_global_duplicate(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-include": ["/foo//bar", "foo//bar/"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match=re.escape(f"Duplicate path in field `tool.hatch.build.only-include`: foo{os.sep}bar")
        ):
            _ = builder.config.only_include

    def test_global_correct(self, isolation):
        config = {"tool": {"hatch": {"build": {"only-include": ["/foo//bar/"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.only_include == {f"{isolation}{os.sep}foo{os.sep}bar": f"foo{os.sep}bar"}

    def test_target_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-include": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.only-include` must be an array"):
            _ = builder.config.only_include

    def test_target_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-include": [9000]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Path #1 in field `tool.hatch.build.targets.foo.only-include` must be a string"
        ):
            _ = builder.config.only_include

    @pytest.mark.parametrize("path", ["/", "~/foo", "../foo"])
    def test_target_not_relative(self, isolation, path):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-include": [path]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match=f"Path #1 in field `tool.hatch.build.targets.foo.only-include` must be relative: {path}"
        ):
            _ = builder.config.only_include

    def test_target_duplicate(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-include": ["/foo//bar", "foo//bar/"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match=re.escape(f"Duplicate path in field `tool.hatch.build.targets.foo.only-include`: foo{os.sep}bar"),
        ):
            _ = builder.config.only_include

    def test_target_correct(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"only-include": ["/foo//bar/"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.only_include == {f"{isolation}{os.sep}foo{os.sep}bar": f"foo{os.sep}bar"}


class TestVersions:
    def test_default_known(self, isolation):
        builder = MockBuilder(str(isolation))
        builder.PLUGIN_NAME = "foo"
        builder.get_version_api = lambda: {"2": str, "1": str}

        assert builder.config.versions == builder.config.versions == ["2", "1"]

    def test_default_override(self, isolation):
        builder = MockBuilder(str(isolation))
        builder.PLUGIN_NAME = "foo"
        builder.get_default_versions = lambda: ["old", "new", "new"]

        assert builder.config.versions == builder.config.versions == ["old", "new"]

    def test_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": ""}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.foo.versions` must be an array of strings"
        ):
            _ = builder.config.versions

    def test_correct(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": ["3.14", "1", "3.14"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"
        builder.get_version_api = lambda: {"3.14": str, "42": str, "1": str}

        assert builder.config.versions == builder.config.versions == ["3.14", "1"]

    def test_empty_default(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": []}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"
        builder.get_default_versions = lambda: ["old", "new"]

        assert builder.config.versions == builder.config.versions == ["old", "new"]

    def test_version_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": [1]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Version #1 in field `tool.hatch.build.targets.foo.versions` must be a string"
        ):
            _ = builder.config.versions

    def test_version_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Version #1 in field `tool.hatch.build.targets.foo.versions` cannot be an empty string"
        ):
            _ = builder.config.versions

    def test_unknown_version(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"versions": ["9000", "1", "42"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"
        builder.get_version_api = lambda: {"1": str}

        with pytest.raises(
            ValueError, match="Unknown versions in field `tool.hatch.build.targets.foo.versions`: 42, 9000"
        ):
            _ = builder.config.versions


class TestHookConfig:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.hook_config == builder.config.hook_config == {}

    def test_target_not_table(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"hooks": "bar"}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.hooks` must be a table"):
            _ = builder.config.hook_config

    def test_target_hook_not_table(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"hooks": {"bar": "baz"}}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.hooks.bar` must be a table"):
            _ = builder.config.hook_config

    def test_global_not_table(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": "foo"}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.hooks` must be a table"):
            _ = builder.config.hook_config

    def test_global_hook_not_table(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": "bar"}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.hooks.foo` must be a table"):
            _ = builder.config.hook_config

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"bar": "baz"}}}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.hook_config["foo"]["bar"] == "baz"

    def test_order(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {"hooks": {"foo": {"bar": "baz"}}, "targets": {"foo": {"hooks": {"baz": {"foo": "bar"}}}}}
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.hook_config == {"foo": {"bar": "baz"}, "baz": {"foo": "bar"}}

    def test_target_overrides_global(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {"hooks": {"foo": {"bar": "baz"}}, "targets": {"foo": {"hooks": {"foo": {"baz": "bar"}}}}}
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.hook_config["foo"]["baz"] == "bar"

    def test_env_var_no_hooks(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"bar": "baz"}}}}}}
        builder = MockBuilder(str(isolation), config=config)

        with EnvVars({BuildEnvVars.NO_HOOKS: "true"}):
            assert builder.config.hook_config == {}

    def test_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"bar": "baz", "enable-by-default": False},
                            "bar": {"foo": "baz", "enable-by-default": False},
                            "baz": {"foo": "bar"},
                        }
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.hook_config == {"baz": {"foo": "bar"}}

    def test_env_var_all_override_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"bar": "baz", "enable-by-default": False},
                            "bar": {"foo": "baz", "enable-by-default": False},
                            "baz": {"foo": "bar"},
                        }
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)

        with EnvVars({BuildEnvVars.HOOKS_ENABLE: "true"}):
            assert builder.config.hook_config == {
                "foo": {"bar": "baz", "enable-by-default": False},
                "bar": {"foo": "baz", "enable-by-default": False},
                "baz": {"foo": "bar"},
            }

    def test_env_var_specific_override_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"bar": "baz", "enable-by-default": False},
                            "bar": {"foo": "baz", "enable-by-default": False},
                            "baz": {"foo": "bar"},
                        }
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)

        with EnvVars({f"{BuildEnvVars.HOOK_ENABLE_PREFIX}FOO": "true"}):
            assert builder.config.hook_config == {
                "foo": {"bar": "baz", "enable-by-default": False},
                "baz": {"foo": "bar"},
            }


class TestDependencies:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.dependencies == builder.config.dependencies == []

    def test_target_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dependencies": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.foo.dependencies` must be an array"):
            _ = builder.config.dependencies

    def test_target_dependency_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"dependencies": [9000]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Dependency #1 of field `tool.hatch.build.targets.foo.dependencies` must be a string"
        ):
            _ = builder.config.dependencies

    def test_global_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"dependencies": 9000}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Field `tool.hatch.build.dependencies` must be an array"):
            _ = builder.config.dependencies

    def test_global_dependency_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"dependencies": [9000]}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Dependency #1 of field `tool.hatch.build.dependencies` must be a string"):
            _ = builder.config.dependencies

    def test_hook_require_runtime_dependencies_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"require-runtime-dependencies": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Option `require-runtime-dependencies` of build hook `foo` must be a boolean"
        ):
            _ = builder.config.dependencies

    def test_hook_require_runtime_features_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"require-runtime-features": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Option `require-runtime-features` of build hook `foo` must be an array"):
            _ = builder.config.dependencies

    def test_hook_require_runtime_features_feature_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"require-runtime-features": [9000]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Feature #1 of option `require-runtime-features` of build hook `foo` must be a string"
        ):
            _ = builder.config.dependencies

    def test_hook_require_runtime_features_feature_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"require-runtime-features": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match="Feature #1 of option `require-runtime-features` of build hook `foo` cannot be an empty string",
        ):
            _ = builder.config.dependencies

    def test_hook_require_runtime_features_feature_unknown(self, isolation):
        config = {
            "project": {"name": "my_app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"hooks": {"foo": {"require-runtime-features": ["foo_bar"]}}}}},
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError,
            match=(
                "Feature `foo-bar` of option `require-runtime-features` of build hook `foo` is not defined in "
                "field `project.optional-dependencies`"
            ),
        ):
            _ = builder.config.dependencies

    def test_hook_dependencies_not_array(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"dependencies": 9000}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(TypeError, match="Option `dependencies` of build hook `foo` must be an array"):
            _ = builder.config.dependencies

    def test_hook_dependency_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"hooks": {"foo": {"dependencies": [9000]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Dependency #1 of option `dependencies` of build hook `foo` must be a string"
        ):
            _ = builder.config.dependencies

    def test_correct(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "dependencies": ["bar"],
                        "hooks": {"foobar": {"dependencies": ["test1"]}},
                        "targets": {"foo": {"dependencies": ["baz"], "hooks": {"foobar": {"dependencies": ["test2"]}}}},
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_require_runtime_dependencies(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "dependencies": ["foo"]},
            "tool": {
                "hatch": {
                    "build": {
                        "require-runtime-dependencies": True,
                        "dependencies": ["bar"],
                        "hooks": {"foobar": {"dependencies": ["test1"]}},
                        "targets": {"foo": {"dependencies": ["baz"], "hooks": {"foobar": {"dependencies": ["test2"]}}}},
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_require_runtime_features(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "optional-dependencies": {"bar_baz": ["foo"]}},
            "tool": {
                "hatch": {
                    "build": {
                        "require-runtime-features": ["bar-baz"],
                        "dependencies": ["bar"],
                        "hooks": {"foobar": {"dependencies": ["test1"]}},
                        "targets": {"foo": {"dependencies": ["baz"], "hooks": {"foobar": {"dependencies": ["test2"]}}}},
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_env_var_no_hooks(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"]},
                            "bar": {"dependencies": ["bar"]},
                            "baz": {"dependencies": ["baz"]},
                        },
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with EnvVars({BuildEnvVars.NO_HOOKS: "true"}):
            assert builder.config.dependencies == []

    def test_hooks_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"], "enable-by-default": False},
                            "bar": {"dependencies": ["bar"], "enable-by-default": False},
                            "baz": {"dependencies": ["baz"]},
                        },
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dependencies == ["baz"]

    def test_hooks_env_var_all_override_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"], "enable-by-default": False},
                            "bar": {"dependencies": ["bar"], "enable-by-default": False},
                            "baz": {"dependencies": ["baz"]},
                        },
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with EnvVars({BuildEnvVars.HOOKS_ENABLE: "true"}):
            assert builder.config.dependencies == ["foo", "bar", "baz"]

    def test_hooks_env_var_specific_override_enable_by_default(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"], "enable-by-default": False},
                            "bar": {"dependencies": ["bar"], "enable-by-default": False},
                            "baz": {"dependencies": ["baz"]},
                        },
                    }
                }
            }
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with EnvVars({f"{BuildEnvVars.HOOK_ENABLE_PREFIX}FOO": "true"}):
            assert builder.config.dependencies == ["foo", "baz"]

    def test_hooks_require_runtime_dependencies(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "dependencies": ["baz"]},
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"], "enable-by-default": False},
                            "bar": {"dependencies": ["bar"], "require-runtime-dependencies": True},
                        },
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_hooks_require_runtime_dependencies_disabled(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "dependencies": ["baz"]},
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {
                                "dependencies": ["foo"],
                                "enable-by-default": False,
                                "require-runtime-dependencies": True,
                            },
                            "bar": {"dependencies": ["bar"]},
                        },
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dependencies == ["bar"]

    def test_hooks_require_runtime_features(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "optional-dependencies": {"foo_bar": ["baz"]}},
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {"dependencies": ["foo"], "enable-by-default": False},
                            "bar": {"dependencies": ["bar"], "require-runtime-features": ["foo-bar"]},
                        },
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

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

    def test_hooks_require_runtime_features_disabled(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1", "optional-dependencies": {"foo_bar": ["baz"]}},
            "tool": {
                "hatch": {
                    "build": {
                        "hooks": {
                            "foo": {
                                "dependencies": ["foo"],
                                "enable-by-default": False,
                                "require-runtime-features": ["foo-bar"],
                            },
                            "bar": {"dependencies": ["bar"]},
                        },
                    }
                }
            },
        }
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.dependencies == ["bar"]


class TestFileSelectionDefaults:
    def test_include(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.default_include() == []

    def test_exclude(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.default_exclude() == []

    def test_packages(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.default_packages() == []

    def test_only_include(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.default_only_include() == []

    def test_global_exclude(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.default_global_exclude() == ["*.py[cdo]", "/dist"]


class TestPatternInclude:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.include_spec is None

    def test_global_becomes_spec(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert isinstance(builder.config.include_spec, pathspec.GitIgnoreSpec)

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.include` must be an array of strings"):
            _ = builder.config.include_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"include": ["foo", "bar/baz"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.include_spec.match_file(f"foo{separator}file.py")
        assert builder.config.include_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.include_spec.match_file(f"bar{separator}file.py")

    def test_global_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Pattern #1 in field `tool.hatch.build.include` must be a string"):
            _ = builder.config.include_spec

    def test_global_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.include` cannot be an empty string"
        ):
            _ = builder.config.include_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_global_packages_included(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"packages": ["bar"], "include": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.include_spec.match_file(f"foo{separator}file.py")
        assert builder.config.include_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.include_spec.match_file(f"baz{separator}bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"include": ["foo", "bar/baz"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.include_spec.match_file(f"foo{separator}file.py")
        assert builder.config.include_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.include_spec.match_file(f"bar{separator}file.py")

    def test_target_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"include": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Pattern #1 in field `tool.hatch.build.targets.foo.include` must be a string"
        ):
            _ = builder.config.include_spec

    def test_target_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"include": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.targets.foo.include` cannot be an empty string"
        ):
            _ = builder.config.include_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target_overrides_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"include": ["foo"], "targets": {"foo": {"include": ["bar"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert not builder.config.include_spec.match_file(f"foo{separator}file.py")
        assert builder.config.include_spec.match_file(f"bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target_packages_included(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"packages": ["bar"], "include": ["foo"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.include_spec.match_file(f"foo{separator}file.py")
        assert builder.config.include_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.include_spec.match_file(f"baz{separator}bar{separator}file.py")


class TestPatternExclude:
    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_default(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        builder = MockBuilder(str(isolation))

        assert isinstance(builder.config.exclude_spec, pathspec.GitIgnoreSpec)
        assert builder.config.exclude_spec.match_file(f"dist{separator}file.py")

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"exclude": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.exclude` must be an array of strings"):
            _ = builder.config.exclude_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"exclude": ["foo", "bar/baz"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
        assert builder.config.exclude_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    def test_global_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"exclude": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Pattern #1 in field `tool.hatch.build.exclude` must be a string"):
            _ = builder.config.exclude_spec

    def test_global_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"exclude": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.exclude` cannot be an empty string"
        ):
            _ = builder.config.exclude_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"exclude": ["foo", "bar/baz"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
        assert builder.config.exclude_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    def test_target_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"exclude": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Pattern #1 in field `tool.hatch.build.targets.foo.exclude` must be a string"
        ):
            _ = builder.config.exclude_spec

    def test_target_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"exclude": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.targets.foo.exclude` cannot be an empty string"
        ):
            _ = builder.config.exclude_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target_overrides_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"exclude": ["foo"], "targets": {"foo": {"exclude": ["bar"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert not builder.config.exclude_spec.match_file(f"foo{separator}file.py")
        assert builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_vcs_git(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        with temp_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"exclude": ["foo"]}}}}
            builder = MockBuilder(str(temp_dir), config=config)

            vcs_ignore_file = temp_dir / ".gitignore"
            vcs_ignore_file.write_text("/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert builder.config.exclude_spec.match_file(f"bar{separator}file.py")
            assert builder.config.exclude_spec.match_file(f"baz{separator}bar{separator}file.pyc")
            assert not builder.config.exclude_spec.match_file(f"baz{separator}bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_ignore_vcs_git(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        with temp_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"ignore-vcs": True, "exclude": ["foo"]}}}}
            builder = MockBuilder(str(temp_dir), config=config)

            vcs_ignore_file = temp_dir / ".gitignore"
            vcs_ignore_file.write_text("/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_vcs_git_boundary(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        project_dir = temp_dir / "project"
        project_dir.mkdir()
        (project_dir / ".git").mkdir()

        with project_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"exclude": ["foo"]}}}}
            builder = MockBuilder(str(project_dir), config=config)

            vcs_ignore_file = temp_dir / ".gitignore"
            vcs_ignore_file.write_text("/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_vcs_git_exclude_whitelisted_file(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        with temp_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"exclude": ["foo/bar"]}}}}
            builder = MockBuilder(str(temp_dir), config=config)

            vcs_ignore_file = temp_dir / ".gitignore"
            vcs_ignore_file.write_text("foo/*\n!foo/bar")

            assert builder.config.path_is_excluded(f"foo{separator}deb") is True
            assert builder.config.path_is_excluded(f"foo{separator}bar") is True

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_vcs_mercurial(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        with temp_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"exclude": ["foo"]}}}}
            builder = MockBuilder(str(temp_dir), config=config)

            vcs_ignore_file = temp_dir / ".hgignore"
            vcs_ignore_file.write_text("syntax: glob\n/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert builder.config.exclude_spec.match_file(f"bar{separator}file.py")
            assert builder.config.exclude_spec.match_file(f"baz{separator}bar{separator}file.pyc")
            assert not builder.config.exclude_spec.match_file(f"baz{separator}bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_ignore_vcs_mercurial(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        with temp_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"ignore-vcs": True, "exclude": ["foo"]}}}}
            builder = MockBuilder(str(temp_dir), config=config)

            vcs_ignore_file = temp_dir / ".hgignore"
            vcs_ignore_file.write_text("syntax: glob\n/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_vcs_mercurial_boundary(self, temp_dir, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        project_dir = temp_dir / "project"
        project_dir.mkdir()
        (project_dir / ".hg").mkdir()

        with project_dir.as_cwd():
            config = {"tool": {"hatch": {"build": {"exclude": ["foo"]}}}}
            builder = MockBuilder(str(project_dir), config=config)

            vcs_ignore_file = temp_dir / ".hgignore"
            vcs_ignore_file.write_text("syntax: glob\n/bar\n*.pyc")

            assert builder.config.exclude_spec.match_file(f"foo{separator}file.py")
            assert not builder.config.exclude_spec.match_file(f"bar{separator}file.py")

    def test_override_default_global_exclude_patterns(self, isolation):
        builder = MockBuilder(str(isolation))
        builder.config.default_global_exclude = list

        assert builder.config.exclude_spec is None
        assert not builder.config.path_is_excluded(".git/file")


class TestPatternArtifacts:
    def test_default(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.artifact_spec is None

    def test_global_becomes_spec(self, isolation):
        config = {"tool": {"hatch": {"build": {"artifacts": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert isinstance(builder.config.artifact_spec, pathspec.GitIgnoreSpec)

    def test_global_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"artifacts": ""}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.artifacts` must be an array of strings"):
            _ = builder.config.artifact_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"artifacts": ["foo", "bar/baz"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.artifact_spec.match_file(f"foo{separator}file.py")
        assert builder.config.artifact_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.artifact_spec.match_file(f"bar{separator}file.py")

    def test_global_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"artifacts": [0]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Pattern #1 in field `tool.hatch.build.artifacts` must be a string"):
            _ = builder.config.artifact_spec

    def test_global_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"artifacts": [""]}}}}
        builder = MockBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.artifacts` cannot be an empty string"
        ):
            _ = builder.config.artifact_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"artifacts": ["foo", "bar/baz"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert builder.config.artifact_spec.match_file(f"foo{separator}file.py")
        assert builder.config.artifact_spec.match_file(f"bar{separator}baz{separator}file.py")
        assert not builder.config.artifact_spec.match_file(f"bar{separator}file.py")

    def test_target_pattern_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"artifacts": [0]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            TypeError, match="Pattern #1 in field `tool.hatch.build.targets.foo.artifacts` must be a string"
        ):
            _ = builder.config.artifact_spec

    def test_target_pattern_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"foo": {"artifacts": [""]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        with pytest.raises(
            ValueError, match="Pattern #1 in field `tool.hatch.build.targets.foo.artifacts` cannot be an empty string"
        ):
            _ = builder.config.artifact_spec

    @pytest.mark.parametrize("separator", ["/", "\\"])
    def test_target_overrides_global(self, isolation, separator, platform):
        if separator == "\\" and not platform.windows:
            pytest.skip("Not running on Windows")

        config = {"tool": {"hatch": {"build": {"artifacts": ["foo"], "targets": {"foo": {"artifacts": ["bar"]}}}}}}
        builder = MockBuilder(str(isolation), config=config)
        builder.PLUGIN_NAME = "foo"

        assert not builder.config.artifact_spec.match_file(f"foo{separator}file.py")
        assert builder.config.artifact_spec.match_file(f"bar{separator}file.py")


class TestPatternMatching:
    def test_include_explicit(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.include_path("foo/file.py")
        assert not builder.config.include_path("bar/file.py")

    def test_no_include_greedy(self, isolation):
        builder = MockBuilder(str(isolation))

        assert builder.config.include_path("foo/file.py")
        assert builder.config.include_path("bar/file.py")

    def test_exclude_precedence(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": ["foo"], "exclude": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert not builder.config.include_path("foo/file.py")
        assert not builder.config.include_path("bar/file.py")

    def test_artifact_super_precedence(self, isolation):
        config = {"tool": {"hatch": {"build": {"include": ["foo"], "exclude": ["foo"], "artifacts": ["foo"]}}}}
        builder = MockBuilder(str(isolation), config=config)

        assert builder.config.include_path("foo/file.py")
        assert not builder.config.include_path("bar/file.py")
