#!/usr/bin/env python3
"""
Tests for persuade.docstring_checker module.

This test suite validates the AST-based docstring checker handles all edge cases correctly.
"""

import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

from persuade.docstring_checker import check_file

# Project root path calculation:
# This file: tests/unit/claude/scripts/test_check_python_docstrings.py
# Path to root: tests/unit/claude/scripts/ -> tests/unit/claude/ -> tests/unit/
#               -> tests/ -> project root (5 levels up from __file__)
PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.parent.parent


class TestDocstringChecker(unittest.TestCase):
    """Test cases for the docstring checker."""

    def test_function_with_docstring(self) -> None:
        """Test that functions with docstrings are not flagged."""
        code = """
def hello():
    \"\"\"Say hello.\"\"\"
    print("Hello")
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_function_without_docstring(self) -> None:
        """Test that functions without docstrings are flagged."""
        code = """
def hello():
    print("Hello")
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][0], "function")
            self.assertEqual(missing[0][2], "hello")
        finally:
            filepath.unlink()

    def test_decorated_function(self) -> None:
        """Test that decorated functions are correctly detected (AST handles this!)."""
        code = """
@decorator
def hello():
    print("Hello")
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "hello")
        finally:
            filepath.unlink()

    def test_async_function(self) -> None:
        """Test that async functions are correctly detected."""
        code = """
async def fetch_data():
    return "data"
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][0], "async function")
            self.assertEqual(missing[0][2], "fetch_data")
        finally:
            filepath.unlink()

    def test_async_function_with_docstring(self) -> None:
        """Test that async functions with docstrings are not flagged."""
        code = """
async def fetch_data():
    \"\"\"Fetch data asynchronously.\"\"\"
    return "data"
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_class_with_docstring(self) -> None:
        """Test that classes with docstrings are not flagged."""
        code = """
class MyClass:
    \"\"\"A test class.\"\"\"
    pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_class_without_docstring(self) -> None:
        """Test that classes without docstrings are flagged."""
        code = """
class MyClass:
    pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][0], "class")
            self.assertEqual(missing[0][2], "MyClass")
        finally:
            filepath.unlink()

    def test_nested_function(self) -> None:
        """Test that nested functions are detected."""
        code = """
def outer():
    \"\"\"Outer function.\"\"\"
    def inner():
        print("inner")
    inner()
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)  # inner() has no docstring
            self.assertEqual(missing[0][2], "inner")
        finally:
            filepath.unlink()

    def test_method_in_class(self) -> None:
        """Test that methods inside classes are correctly detected."""
        code = """
class MyClass:
    \"\"\"A test class.\"\"\"

    def method_without_docstring(self):
        pass

    def method_with_docstring(self):
        \"\"\"A method with docstring.\"\"\"
        pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "method_without_docstring")
        finally:
            filepath.unlink()

    def test_private_function_ignored(self) -> None:
        """Test that private functions (starting with _) are ignored."""
        code = """
def _private_function():
    pass

def public_function():
    pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            # Only public_function should be flagged
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "public_function")
        finally:
            filepath.unlink()

    def test_special_methods_checked(self) -> None:
        """Test that special methods (__init__, etc.) are checked."""
        code = """
class MyClass:
    \"\"\"A test class.\"\"\"

    def __init__(self):
        pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            # __init__ should be flagged (it's a special method we check)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "__init__")
        finally:
            filepath.unlink()

    def test_property_decorator(self) -> None:
        """Test that property decorators are handled correctly."""
        code = """
class MyClass:
    \"\"\"A test class.\"\"\"

    @property
    def value(self):
        return self._value
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            # Property should be flagged (it's a public method)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "value")
        finally:
            filepath.unlink()

    def test_multiple_decorators(self) -> None:
        """Test that functions with multiple decorators are handled."""
        code = """
@decorator1
@decorator2
@decorator3
def complex_function():
    pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            self.assertEqual(len(missing), 1)
            self.assertEqual(missing[0][2], "complex_function")
        finally:
            filepath.unlink()

    def test_empty_file(self) -> None:
        """Test that empty files are handled gracefully."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("")
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertFalse(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_file_with_syntax_error(self) -> None:
        """Test that files with syntax errors are handled gracefully."""
        code = """
def broken_function(
    # Missing closing parenthesis
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            # Should return False, [] for syntax errors
            self.assertFalse(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_file_with_only_imports(self) -> None:
        """Test that files with only imports have no definitions."""
        code = """
import os
import sys
from pathlib import Path
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertFalse(has_defs)
            self.assertEqual(len(missing), 0)
        finally:
            filepath.unlink()

    def test_complex_real_world_example(self) -> None:
        """Test a complex real-world example with mixed cases."""
        code = """
\"\"\"Module docstring.\"\"\"

import os

class DataProcessor:
    \"\"\"Process data.\"\"\"

    def __init__(self, config):
        \"\"\"Initialize processor.\"\"\"
        self.config = config

    def process(self, data):
        # Missing docstring!
        return self._transform(data)

    def _transform(self, data):
        # Private method - should be ignored
        return data.upper()

    @property
    def status(self):
        # Property missing docstring
        return "ready"

@decorator
async def fetch_remote_data(url):
    \"\"\"Fetch data from URL.\"\"\"
    return await _fetch(url)

async def _fetch(url):
    # Private async function - should be ignored
    pass

def helper_function():
    # Missing docstring!
    pass
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)

        try:
            has_defs, missing = check_file(filepath)
            self.assertTrue(has_defs)
            # Should flag: process(), status, helper_function()
            # Should NOT flag: _transform(), _fetch() (private)
            self.assertEqual(len(missing), 3)
            missing_names = {m[2] for m in missing}
            self.assertEqual(missing_names, {"process", "status", "helper_function"})
        finally:
            filepath.unlink()


class TestDocstringCheckerScript(unittest.TestCase):
    """Test cases for the docstring checker wrapper script."""

    def test_nonexistent_path_error(self) -> None:
        """Test that non-existent paths produce clear error messages."""
        script_path = PROJECT_ROOT / ".claude/scripts/check_python_docstrings.py"
        result = subprocess.run(
            [
                sys.executable,
                str(script_path),
                "/nonexistent/path.py",
            ],
            capture_output=True,
            text=True,
        )
        self.assertEqual(result.returncode, 1)
        self.assertIn("does not exist", result.stderr)


if __name__ == "__main__":
    unittest.main()
