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')
