# File: tests/unit/scripts/test_docstring_compliance_tiered.py
# Purpose: Test tiered docstring compliance system with edge cases.
# Context: Validates Domain/Application/Infrastructure layer detection and enforcement.

"""
Tests for tiered docstring compliance system.

Covers:
- Path classification (Domain/Application/Infrastructure/Presentation/Tests)
- Docstring validation per tier
- Pre-commit hook Domain layer detection
- Edge cases (generator, async, malformed paths)
"""

import ast

# Import modules under test
import sys
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts"))

from comment_header_audit import (
    DocstringPolicy,
    get_policy_for_file,
    validate_docstring_tiered,
)
from pre_commit_docstring_check import is_domain_layer_file


class TestDomainLayerDetection:
    """Test structural path analysis for Domain layer classification."""

    def test_domain_layer_detection_basic(self):
        """Detect Domain layer file with correct path structure."""
        assert is_domain_layer_file("src/noveler/domain/services/my_service.py")
        assert is_domain_layer_file(Path("src/noveler/domain/services/my_service.py"))

    def test_domain_layer_detection_nested(self):
        """Detect Domain layer with deeply nested subdirectories."""
        assert is_domain_layer_file("src/noveler/domain/value_objects/episode/title.py")
        assert is_domain_layer_file("src/noveler/domain/entities/project/project.py")

    def test_not_domain_layer_application(self):
        """Reject Application layer files."""
        assert not is_domain_layer_file("src/noveler/application/use_cases/create_episode.py")

    def test_not_domain_layer_infrastructure(self):
        """Reject Infrastructure layer files."""
        assert not is_domain_layer_file("src/noveler/infrastructure/repositories/episode_repo.py")

    def test_not_domain_layer_presentation(self):
        """Reject Presentation layer files."""
        assert not is_domain_layer_file("src/noveler/presentation/cli/commands.py")

    def test_not_domain_layer_tests(self):
        """Reject Test files."""
        assert not is_domain_layer_file("tests/unit/domain/test_episode.py")

    def test_not_domain_layer_path_with_domain_in_name(self):
        """Reject files in Infrastructure that happen to have 'domain' in directory name."""
        # Edge case: domain_models directory in Infrastructure
        assert not is_domain_layer_file("src/noveler/infrastructure/domain_models/entities.py")

    def test_domain_layer_detection_path_object(self):
        """Handle Path objects correctly."""
        path = Path("src") / "noveler" / "domain" / "services" / "my_service.py"
        assert is_domain_layer_file(path)

    def test_not_domain_layer_relative_path_incomplete(self):
        """Reject paths with incomplete structure."""
        assert not is_domain_layer_file("src/noveler/domain.py")  # File, not directory
        assert not is_domain_layer_file("src/noveler")  # Directory only
        assert not is_domain_layer_file("domain/services/my_service.py")  # Missing src/noveler

    def test_domain_layer_detection_malformed_path(self):
        """Handle malformed paths gracefully."""
        assert not is_domain_layer_file(None)
        assert not is_domain_layer_file("")
        assert not is_domain_layer_file(123)  # type: ignore


class TestDocstringValidationStrictTier:
    """Test STRICT tier validation for Domain layer."""

    def test_strict_tier_complete_docstring(self):
        """Accept docstring with all required sections."""
        docstring = """
        Calculate royalty amount.

        Purpose:
            Compute author payment from sales and rate.

        Args:
            sales: Total sales amount
            rate: Royalty rate (0.0-1.0)

        Returns:
            Calculated royalty amount

        Raises:
            ValueError: If rate out of range

        Side Effects:
            None
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=True,
            has_args=True,
            has_raises=True,
        )
        assert len(findings) == 0

    def test_strict_tier_missing_purpose(self):
        """Reject docstring missing Purpose section."""
        docstring = "Docstring without Purpose section"
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=False,
            has_args=False,
            has_raises=False,
        )
        assert any("Purpose" in f.message for f in findings)

    def test_strict_tier_missing_args(self):
        """Reject docstring missing Args when function has parameters."""
        docstring = """
        Do something.

        Purpose:
            This is the purpose.

        Returns:
            Something

        Side Effects:
            None
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=True,
            has_args=True,  # Function HAS args
            has_raises=False,
        )
        assert any("Args" in f.message for f in findings)

    def test_strict_tier_missing_side_effects(self):
        """Reject docstring missing Side Effects section."""
        docstring = """
        Do something.

        Purpose:
            This is the purpose.

        Args:
            param: A parameter

        Returns:
            Something

        Raises:
            ValueError
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=True,
            has_args=True,
            has_raises=True,
        )
        assert any("Side Effects" in f.message or "side.effects" in f.message.lower() for f in findings)


class TestDocstringValidationModerateTier:
    """Test MODERATE tier validation for Application layer."""

    def test_moderate_tier_purpose_only(self):
        """Accept docstring with Purpose only (minimal requirement)."""
        docstring = """
        Create new episode.

        Purpose:
            Initialize episode with title and manuscript.
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/application/use_cases/test.py"),
            1,
            docstring,
            DocstringPolicy.MODERATE,
            has_returns=True,
            has_args=True,
            has_raises=True,
        )
        assert len(findings) == 0

    def test_moderate_tier_missing_purpose(self):
        """Reject docstring missing Purpose section."""
        docstring = "Just a one-liner"
        findings = validate_docstring_tiered(
            Path("src/noveler/application/use_cases/test.py"),
            1,
            docstring,
            DocstringPolicy.MODERATE,
            has_returns=False,
            has_args=False,
            has_raises=False,
        )
        assert any("Purpose" in f.message for f in findings)

    def test_moderate_tier_does_not_require_args(self):
        """MODERATE tier does NOT require Args section."""
        docstring = """
        Create episode.

        Purpose:
            Initialize episode.
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/application/use_cases/test.py"),
            1,
            docstring,
            DocstringPolicy.MODERATE,
            has_returns=True,
            has_args=True,  # Has args
            has_raises=False,
        )
        # Should NOT complain about missing Args
        assert not any("Args" in f.message for f in findings)


class TestGeneratorFunctionHandling:
    """Test handling of generator functions (yield statements)."""

    def test_generator_function_detection(self):
        """Generator functions should be detected correctly."""
        code = """
def my_generator():
    \"\"\"Generate items.\"\"\"
    yield 1
    yield 2
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.FunctionDef)

        # Check if AST contains Yield nodes
        has_yield = any(isinstance(node, ast.Yield) for node in ast.walk(func))
        assert has_yield, "Generator should have Yield nodes"

    def test_generator_docstring_validation(self):
        """Generator with complete docstring should pass STRICT validation."""
        docstring = """
        Generate items.

        Purpose:
            Iterate through collection items.

        Returns:
            Iterator of item values

        Side Effects:
            None
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=True,  # Generators return values
            has_args=False,
            has_raises=False,
        )
        assert len(findings) == 0


class TestAsyncFunctionHandling:
    """Test handling of async/await functions."""

    def test_async_function_ast_structure(self):
        """Async functions should parse correctly in AST."""
        code = """
async def fetch_data():
    \"\"\"Fetch remote data.\"\"\"
    return await get_api()
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.AsyncFunctionDef)

    def test_async_function_docstring_validation(self):
        """Async function with complete docstring should pass validation."""
        docstring = """
        Fetch episode data.

        Purpose:
            Retrieve episode from remote service asynchronously.

        Returns:
            Episode object

        Raises:
            NetworkError: If remote service is unavailable

        Side Effects:
            - Network I/O (HTTP request)
        """
        findings = validate_docstring_tiered(
            Path("src/noveler/domain/services/test.py"),
            1,
            docstring,
            DocstringPolicy.STRICT,
            has_returns=True,
            has_args=False,
            has_raises=True,
        )
        assert len(findings) == 0


class TestPathClassificationByExtension:
    """Test get_policy_for_file for all layer types."""

    def test_domain_layer_classification(self):
        """Domain layer files get STRICT policy."""
        policy = get_policy_for_file(Path("src/noveler/domain/services/my_service.py"))
        assert policy == DocstringPolicy.STRICT

    def test_application_layer_classification(self):
        """Application layer files get MODERATE policy."""
        policy = get_policy_for_file(Path("src/noveler/application/use_cases/create_episode.py"))
        assert policy == DocstringPolicy.MODERATE

    def test_infrastructure_layer_classification(self):
        """Infrastructure layer files get RELAXED policy."""
        policy = get_policy_for_file(Path("src/noveler/infrastructure/services/yaml_service.py"))
        assert policy == DocstringPolicy.RELAXED

    def test_presentation_layer_classification(self):
        """Presentation layer files get MINIMAL policy."""
        policy = get_policy_for_file(Path("src/noveler/presentation/cli/commands.py"))
        assert policy == DocstringPolicy.MINIMAL

    def test_tests_layer_classification(self):
        """Test files get EXEMPT policy."""
        policy = get_policy_for_file(Path("tests/unit/domain/test_episode.py"))
        assert policy == DocstringPolicy.EXEMPT


class TestBackslashPathHandling:
    """Test Windows-style paths (backslash)."""

    def test_windows_path_domain_detection(self):
        """Handle Windows paths with backslashes."""
        win_path = r"src\noveler\domain\services\test.py"
        assert is_domain_layer_file(win_path)

    def test_windows_path_application_detection(self):
        """Handle Windows paths for Application layer."""
        win_path = r"src\noveler\application\use_cases\test.py"
        assert not is_domain_layer_file(win_path)


class TestCaseSensitivity:
    """Test case sensitivity in path matching."""

    def test_lowercase_path_detection(self):
        """Lowercase paths should work."""
        assert is_domain_layer_file("src/noveler/domain/services/test.py")

    def test_uppercase_in_middle_rejected(self):
        """Uppercase in path components should be rejected (path not found)."""
        # Most systems are case-sensitive, but this tests the logic
        result = is_domain_layer_file("src/noveler/Domain/services/test.py")
        # Should fail because 'Domain' != 'domain'
        assert not result


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
