"""Tests for autopypath._validate module."""

import logging
import os
from pathlib import Path
from types import MappingProxyType

import pytest
from testspec import Assert, PytestAction, TestSpec

from autopypath import _validate
from autopypath._exceptions import AutopypathError
from autopypath._load_strategy import _LoadStrategy
from autopypath._marker_type import _MarkerType
from autopypath._path_resolution import _PathResolution

# fmt: off

@pytest.mark.parametrize(
    'testspec', [
        PytestAction('CONTEXT_001',
            name='valid context file',
            action=_validate.context_file, args=[__file__],
            exception=AutopypathError),
        PytestAction('CONTEXT_002',
            name='non-existent context file',
            action=_validate.context_file, args=['non_existent_file.cfg'],
            exception=AutopypathError),
        PytestAction('CONTEXT_003',
            name='context file as Path',
            action=_validate.context_file, args=[Path(__file__)],
            expected=Path(__file__).resolve()),
        PytestAction('CONTEXT_004',
            name='Context file is a directory',
            action=_validate.context_file, args=[Path('.')],
            exception=AutopypathError),
        PytestAction('CONTEXT_005',
            name='context file as string relative to cwd',
            action=_validate.context_file, args=['data'],
            exception=AutopypathError),  # Relative to cwd
        PytestAction('CONTEXT_006',
            name='context file as None',
            action=_validate.context_file, args=[None],
            exception=AutopypathError),
    ])
def test_context_file(testspec: TestSpec) -> None:
    """Test Config with context_file."""
    testspec.run()


@pytest.mark.parametrize(
    'testspec', [
        PytestAction('ROOT_REPO_001',
            name='invalid root_repo',
            action=_validate.root_repo_path, args=['not-my-repo-does-not-exist'],
            exception=AutopypathError),
        PytestAction('ROOT_REPO_002',
            name='valid root_repo',
            action=_validate.root_repo_path, args=['.'],
            expected=Path('.')),
        PytestAction('ROOT_REPO_003',
            name='None root_repo',
            action=_validate.root_repo_path, args=[None],
            exception=AutopypathError),
        PytestAction('ROOT_REPO_004',
            name='root_repo as Path',
            action=_validate.root_repo_path, args=[Path('.')],
            expected=Path('.')),
        PytestAction('ROOT_REPO_005',
            name='root_repo as empty string',
            action=_validate.root_repo_path, args=[''],
            exception=AutopypathError),
        PytestAction('ROOT_REPO_006',
            name='Non-existent root_repo with multi-level path',
            action=_validate.root_repo_path, args=[Path('data','more')],
            exception=AutopypathError),
        PytestAction('ROOT_REPO_007',
            name='root_repo two levels up from current directory',
            action=_validate.root_repo_path, args=[Path('..','..')],
            expected=Path('..','..')),
        PytestAction('ROOT_REPO_008',
            name='root_repo pointing to a file instead of a directory',
            action=_validate.root_repo_path, args=[Path('data','not_a_directory')],
            exception=AutopypathError),
        PytestAction('ROOT_REPO_009',
            name='root_repo pointing to test data directory',
            action=_validate.root_repo_path, args=[Path(__file__).parent / Path('data')],
            expected=(Path(__file__).parent / Path('data')).resolve()),
        PytestAction('ROOT_REPO_010',
            name='root_repo with path include null byte',
            action=_validate.root_repo_path, args=[Path('data', 'invalid\0path')],
            exception=AutopypathError),
    ])
def test_root_repo_path(testspec: TestSpec) -> None:
    """Test Config with root_repo_path."""
    testspec.run()


@pytest.mark.parametrize(
    'testspec', [
        PytestAction('MARKER_001',
            name='valid repo_markers',
            action=_validate.repo_markers,
            args=[{'.git': _MarkerType.DIR, 'pyproject.toml': _MarkerType.FILE}],
            expected={'.git': _MarkerType.DIR, 'pyproject.toml': _MarkerType.FILE}),
        PytestAction('MARKER_002',
            name='empty repo_markers',
            action=_validate.repo_markers, args=[{}],
            expected=None),
        PytestAction('MARKER_003',
            name='single repo_marker',
            action=_validate.repo_markers, args=[{'setup.py': _MarkerType.FILE}],
            expected={'setup.py': _MarkerType.FILE}),
        PytestAction('MARKER_004',
            name='string repo_markers',
            action=_validate.repo_markers, args=[{'.git': 'dir', 'setup.py': 'file'}],
            expected={'.git': _MarkerType.DIR, 'setup.py': _MarkerType.FILE}),
        PytestAction('MARKER_005',
            name='mixed type repo_markers',
            action=_validate.repo_markers, args=[{'.git': _MarkerType.DIR, 'setup.py': 'file'}],
            expected={'.git': _MarkerType.DIR, 'setup.py': _MarkerType.FILE}),
        PytestAction('MARKER_006',
            name='invalid repo_marker',
            action=_validate.repo_markers, args=[{'.git': 'DIR'}],
            exception=AutopypathError),
        PytestAction('MARKER_007',
            name='invalid repo_marker type',
            action=_validate.repo_markers, args=[{'.git': 123}],
            exception=AutopypathError),
        PytestAction('MARKER_008',
            name='None repo_markers',
            action=_validate.repo_markers, args=[None],
            expected=None),
        PytestAction('MARKER_009',
            name='no repo_markers',
            action=_validate.repo_markers, kwargs={'value': {}},
            expected=None),
        PytestAction('MARKER_010',
            name='whitespace repo_marker key',
            action=_validate.repo_markers, args=[{'   ': _MarkerType.DIR}],
            exception=AutopypathError),
        PytestAction('MARKER_011',
            name='non-string repo_marker key',
            action=_validate.repo_markers, args=[{123: _MarkerType.DIR}],
            exception=AutopypathError),
        PytestAction('MARKER_012',
            name='non-mapping repo_markers',
            action=_validate.repo_markers, args=[[('setup.py', _MarkerType.FILE)]],
            exception=AutopypathError),
        PytestAction('MARKER_013',
            name='repo_marker key exceeding max length',
            action=_validate.repo_markers, args=[{'a' * 65: _MarkerType.DIR}],
            exception=AutopypathError),
        PytestAction('MARKER_014',
            name='repo_marker key containing path separator',
            action=_validate.repo_markers, args=[{'inva/lid': _MarkerType.DIR}],
            exception=AutopypathError),
        PytestAction('MARKER_015',
            name='repo_marker key being Windows reserved name',
            action=_validate.repo_markers, args=[{'CON': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_016',
            name='repo_marker key containing forbidden characters',
            action=_validate.repo_markers, args=[{'inva<lid': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_017',
            name='repo_marker key being empty string',
            action=_validate.repo_markers, args=[{'': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_018',
            name='repo_marker key having leading whitespace',
            action=_validate.repo_markers, args=[{'  setup.py': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_019',
            name='repo_marker key having trailing whitespace',
            action=_validate.repo_markers, args=[{'setup.py  ': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_020',
            name='repo_marker key containing null character',
            action=_validate.repo_markers, args=[{'setup\0.py': _MarkerType.FILE}],
            exception=AutopypathError),
        PytestAction('MARKER_021',
            name='Validate repo_marker attr returns MappingProxyType',
            action=_validate.repo_markers, args=[{'setup.py': _MarkerType.FILE}],
            expected=MappingProxyType,
            assertion=Assert.ISINSTANCE),
    ]
)
def test_repo_markers(testspec: TestSpec) -> None:
    """Test Config with repo_markers."""
    testspec.run()

@pytest.mark.parametrize('testspec', [
    PytestAction('PATHS_001',
        name='valid list of paths',
        action=_validate.paths, args=[['src', 'lib', 'utils']],
        expected=(Path('src'), Path('lib'), Path('utils'))),
    PytestAction('PATHS_002',
        name='empty paths',
        action=_validate.paths, args=[[]],
        expected=None),
    PytestAction('PATHS_003',
        name='single path',
        action=_validate.paths, args=[['src']],
        expected=(Path('src'),)),
    PytestAction('PATHS_004',
        name='None paths',
        action=_validate.paths, kwargs={'value': None},
        expected=None),
    PytestAction('PATHS_005',
        name='no paths',
        action=_validate.paths, kwargs={'value': []},
        expected=None),
    PytestAction('PATHS_006',
        name='invalid path type',
        action=_validate.paths, args=[['src', 123, 'utils']],
        exception=AutopypathError),
    PytestAction('PATHS_007',
        name='Path objects',
        action=_validate.paths, args=[[Path('src'), Path('lib')]],
        expected=(Path('src'), Path('lib'))),
    PytestAction('PATHS_008',
        name='path having leading whitespace',
        action=_validate.paths, args=[['  src', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_009',
        name='path having trailing whitespace',
        action=_validate.paths, args=[['src  ', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_010',
        name='path being only whitespace',
        action=_validate.paths, args=[['   ', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_011',
        name='path being only forward slashes',
        action=_validate.paths, args=[['///', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_012',
        name='path being only backslashes',
        action=_validate.paths, args=[['\\\\\\', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_013',
        name='path having segment with leading whitespace',
        action=_validate.paths, args=[['src/  utils', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_014',
        name='path having segment with trailing whitespace',
        action=_validate.paths, args=[['src/utils  ', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_015',
        name='path having whitespace segment',
        action=_validate.paths, args=[['src/ /utils', 'lib']],
        exception=AutopypathError),
    PytestAction('PATHS_016',
        name='Path having segment being only whitespace',
        action=_validate.paths, args=[[Path('src/   /utils'), Path('lib')]],
        exception=AutopypathError),
    PytestAction('PATHS_017',
        name='Path having segment with leading whitespace',
        action=_validate.paths, args=[[Path('src/  utils'), Path('lib')]],
        exception=AutopypathError),
    PytestAction('PATHS_018',
        name='Path having segment with trailing whitespace',
        action=_validate.paths, args=[[Path('src/utils  /more'), Path('lib')]],
        exception=AutopypathError),
    PytestAction('PATHS_019',
        name='Create config with path being empty string',
        action=_validate.paths, args=[['']],
        exception=AutopypathError),
    PytestAction('PATHS_020',
        name='Create config with path being neither str nor Path',
        action=_validate.paths, args=[[123]],
        exception=AutopypathError),
    PytestAction('PATHS_021',
        name='Create config with paths being neither sequence nor None',
        action=_validate.paths, args=[123],
        exception=AutopypathError),
    PytestAction('PATHS_022',
        name='Validate paths attr returns tuple of Path objects',
        action=_validate.paths, args=[['src', 'lib']],
        expected=tuple,
        assertion=Assert.ISINSTANCE),
])
def test_paths(testspec: TestSpec) -> None:
    """Test Config with paths"""
    testspec.run()


@pytest.mark.parametrize('testspec', [
    PytestAction('LOAD_STRATEGY_001',
        name='valid load_strategy',
        action=_validate.load_strategy,
        args=[_LoadStrategy.PREPEND],
        expected=_LoadStrategy.PREPEND),
    PytestAction('LOAD_STRATEGY_002',
        name='load_strategy as string',
        action=_validate.load_strategy,
        args=['prepend'],
        expected=_LoadStrategy.PREPEND),
    PytestAction('LOAD_STRATEGY_003',
        name='invalid load_strategy string',
        action=_validate.load_strategy,
        args=['invalid_strategy'],
        exception=AutopypathError),
    PytestAction('LOAD_STRATEGY_004',
        name='invalid load_strategy type',
        action=_validate.load_strategy,
        args=[123],
        exception=AutopypathError),
    PytestAction('LOAD_STRATEGY_005',
        name='None load_strategy',
        action=_validate.load_strategy,
        args=[None],
        expected=None),
])
def test_load_strategy(testspec: TestSpec) -> None:
    """Test Config with load_strategy"""
    testspec.run()

@pytest.mark.parametrize('testspec', [
    PytestAction('RESOLVE_001',
        name='valid path_resolution_order',
        action=_validate.path_resolution_order,
        args=[['manual', 'pyproject']],
        expected=(_PathResolution.MANUAL, _PathResolution.PYPROJECT)),
    PytestAction('RESOLVE_002',
        name='path_resolution_order as enums',
        action=_validate.path_resolution_order,
        args=[[_PathResolution.PYPROJECT, _PathResolution.MANUAL]],
        expected=(_PathResolution.PYPROJECT, _PathResolution.MANUAL)),
    PytestAction('RESOLVE_003',
        name='invalid path_resolution_order string',
        action=_validate.path_resolution_order,
        args=[['invalid_source']],
        exception=AutopypathError),
    PytestAction('RESOLVE_004',
        name='invalid path_resolution_order type',
        action=_validate.path_resolution_order,
        args=[[123]],
        exception=AutopypathError),
    PytestAction('RESOLVE_005',
        name='empty path_resolution_order',
        action=_validate.path_resolution_order,
        args=[[]],
        expected=None),
    PytestAction('RESOLVE_006',
        name='None path_resolution_order',
        action=_validate.path_resolution_order,
        args=[None],
        expected=None),
    PytestAction('RESOLVE_007',
        name='no path_resolution_order',
        action=_validate.path_resolution_order,
        kwargs={'value': []},
        expected=None),
    PytestAction('RESOLVE_008',
        name='duplicate path_resolution_order entries',
        action=_validate.path_resolution_order,
        args=[['manual', 'pyproject', 'manual']],
        exception=AutopypathError),
    PytestAction('RESOLVE_009',
        name='single path_resolution_order entry',
        action=_validate.path_resolution_order,
        args=[['pyproject']],
        expected=(_PathResolution.PYPROJECT,)),
    PytestAction('RESOLVE_010',
        name='mixed type path_resolution_order entries',
        action=_validate.path_resolution_order,
        args=[['manual', _PathResolution.PYPROJECT]],
        expected=(_PathResolution.MANUAL, _PathResolution.PYPROJECT)),
    PytestAction('RESOLVE_011',
        name='path_resolution_order having leading/trailing whitespace',
        action=_validate.path_resolution_order,
        args=[[' manual ', 'pyproject']],
        exception=AutopypathError),
    PytestAction('RESOLVE_012',
        name='path_resolution_order being neither sequence nor None',
        action=_validate.path_resolution_order,
        args=[123],
        exception=AutopypathError),
    PytestAction('RESOLVE_013',
        name='path_resolution_order containing non-str/non-enum',
        action=_validate.path_resolution_order,
        args=[['manual', 123]],
        exception=AutopypathError),
    PytestAction('RESOLVE_014',
        name="Create config with path_resolution_order as a string",
        action=_validate.path_resolution_order,
        args=['manual'],
        exception=AutopypathError),
])
def test_path_resolution_order(testspec: TestSpec) -> None:
    """Test Config with path_resolution_order"""
    testspec.run()


@pytest.mark.parametrize('testspec', [
    PytestAction('DRY_RUN_001',
        name='dry_run as True',
        action=_validate.dry_run, args=[True],
        expected=True),
    PytestAction('DRY_RUN_002',
        name='dry_run as False',
        action=_validate.dry_run, args=[False],
        expected=False),
    PytestAction('DRY_RUN_003',
        name='dry_run as string',
        action=_validate.dry_run, args=['true'],
        exception=AutopypathError),
    PytestAction('DRY_RUN_004',
        name='dry_run as None',
        action=_validate.dry_run, args=[None],
        exception=AutopypathError),
])
def test_dry_run(testspec: TestSpec) -> None:
    """Test Config with dry_run."""
    testspec.run()

@pytest.mark.parametrize('testspec', [
    PytestAction('STRICT_001',
        name='strict as True',
        action=_validate.strict, args=[True],
        expected=True),
    PytestAction('STRICT_002',
        name='strict as False',
        action=_validate.strict, args=[False],
        expected=False),
    PytestAction('STRICT_003',
        name='strict as string',
        action=_validate.strict, args=['false'],
        exception=AutopypathError),
    PytestAction('STRICT_004',
        name='strict as None',
        action=_validate.strict, args=[None],
        exception=AutopypathError),
])
def test_strict(testspec: TestSpec) -> None:
    """Test Config with strict."""
    testspec.run()

@pytest.mark.parametrize('testspec', [
    PytestAction('LOG_LEVEL_001',
        name='log_level NOTSET',
        action=_validate.log_level, args=[logging.NOTSET],
        expected=logging.NOTSET),
    PytestAction('LOG_LEVEL_002',
        name='log_level DEBUG',
        action=_validate.log_level, args=[logging.DEBUG],
        expected=logging.DEBUG),
    PytestAction('LOG_LEVEL_003',
        name='log_level INFO',
        action=_validate.log_level, args=[logging.INFO],
        expected=logging.INFO),
    PytestAction('LOG_LEVEL_004',
        name='log_level WARNING',
        action=_validate.log_level, args=[logging.WARNING],
        expected=logging.WARNING),
    PytestAction('LOG_LEVEL_005',
        name='log_level ERROR',
        action=_validate.log_level, args=[logging.ERROR],
        expected=logging.ERROR),
    PytestAction('LOG_LEVEL_006',
        name='log_level CRITICAL',
        action=_validate.log_level, args=[logging.CRITICAL],
        expected=logging.CRITICAL),
    PytestAction('LOG_LEVEL_007',
        name='log_level as None (should return NOTSET)',
        action=_validate.log_level, args=[None],
        expected=logging.NOTSET),
    PytestAction('LOG_LEVEL_008',
        name='invalid log_level type (string)',
        action=_validate.log_level, args=['INFO'],
        exception=AutopypathError),
    PytestAction('LOG_LEVEL_009',
        name='invalid log_level type (float)',
        action=_validate.log_level, args=[10.5],
        exception=AutopypathError),
    PytestAction('LOG_LEVEL_010',
        name='invalid log_level value (out of range)',
        action=_validate.log_level, args=[999],
        exception=AutopypathError),
])
def test_log_level(testspec: TestSpec) -> None:
    """Test Config with log_level."""
    testspec.run()

@pytest.mark.parametrize('testspec', [
    PytestAction('PATH_OR_STR_001',
        name='valid string path',
        action=_validate.validate_path_or_str,
        args=['some/path/to/file.txt'],
        expected=Path('some/path/to/file.txt')),
    PytestAction('PATH_OR_STR_002',
        name='valid Path object',
        action=_validate.validate_path_or_str,
        args=[Path('another/path/to/dir')],
        expected=Path('another/path/to/dir')),
    PytestAction('PATH_OR_STR_003',
        name='invalid type (int)',
        action=_validate.validate_path_or_str,
        args=[123],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_004',
        name='path with null byte',
        action=_validate.validate_path_or_str,
        args=['invalid\0path'],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_005',
        name='path that is only forward slashes',
        action=_validate.validate_path_or_str,
        args=['///'],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_006',
        name='path that is only backslashes',
        action=_validate.validate_path_or_str,
        args=['\\\\\\'],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_007',
        name='windows drive letter segment',
        action=_validate.validate_path_or_str,
        args=['C:/folder/file.txt'],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_008',
        name='absolute windows path',
        action=_validate.validate_path_or_str,
        args=['C:\\folder\\subfolder\\file.txt'],
        exception=AutopypathError),
    PytestAction('PATH_OR_STR_009',
        name='absolute posix path',
        action=_validate.validate_path_or_str,
        args=['/folder/subfolder/file.txt'],
        exception=AutopypathError),
])
def test_validate_path_or_str_posix(testspec: TestSpec) -> None:
    """Test _validate_path_or_str function using PytestAction style for posix paths."""
    if os.name != 'posix':
        pytest.skip('Skipping POSIX-specific tests on non-POSIX platform')
    testspec.run()
