"""Tests for persuade.docs.validate module."""

import subprocess
from pathlib import Path
from unittest.mock import patch

from persuade.docs.validate import (
    extract_python_blocks,
    validate_documentation,
    validate_with_mypy,
    validate_with_ty,
)


class TestExtractPythonBlocks:
    """Test Python code block extraction from markdown."""

    def test_extract_single_block(self, tmp_path: Path) -> None:
        """Test extraction of a single Python code block."""
        md_file = tmp_path / "test.md"
        md_file.write_text(
            """
# Example

```python
def hello():
    print("hello")
```
"""
        )

        blocks = extract_python_blocks(md_file)
        assert len(blocks) == 1
        code, line_num = blocks[0]
        assert "def hello():" in code
        assert 'print("hello")' in code
        assert line_num == 4  # ```python starts at line 4

    def test_extract_multiple_blocks(self, tmp_path: Path) -> None:
        """Test extraction of multiple Python code blocks."""
        md_file = tmp_path / "test.md"
        md_file.write_text(
            """
# Example 1

```python
x = 1
```

# Example 2

```python
y = 2
```
"""
        )

        blocks = extract_python_blocks(md_file)
        assert len(blocks) == 2
        assert "x = 1" in blocks[0][0]
        assert "y = 2" in blocks[1][0]

    def test_extract_no_blocks(self, tmp_path: Path) -> None:
        """Test extraction when there are no Python blocks."""
        md_file = tmp_path / "test.md"
        md_file.write_text(
            """
# Just text

No code here.
"""
        )

        blocks = extract_python_blocks(md_file)
        assert len(blocks) == 0

    def test_extract_with_other_languages(self, tmp_path: Path) -> None:
        """Test that only Python blocks are extracted."""
        md_file = tmp_path / "test.md"
        md_file.write_text(
            """
```javascript
console.log("not python");
```

```python
print("python")
```

```bash
echo "not python"
```
"""
        )

        blocks = extract_python_blocks(md_file)
        assert len(blocks) == 1
        assert 'print("python")' in blocks[0][0]


class TestValidateWithTy:
    """Test ty type checking validation."""

    def test_validate_valid_code(self) -> None:
        """Test validation of syntactically and type-correct code."""
        code = """
def add(x: int, y: int) -> int:
    return x + y
"""
        is_valid, error = validate_with_ty(code, Path("test.md"), 1)
        assert is_valid, f"Expected valid code to pass, got error: {error}"
        assert error == ""

    def test_validate_syntax_error(self) -> None:
        """Test validation catches syntax errors."""
        code = "def broken(:"
        is_valid, error = validate_with_ty(code, Path("test.md"), 1)
        assert not is_valid, "Expected syntax error to be detected"
        # Error message should contain file reference or meaningful content
        assert error != "", f"Expected non-empty error message, got: '{error}'"

    def test_ty_not_found(self) -> None:
        """Test error handling when neither uvx nor ty is installed."""

        def mock_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
            raise FileNotFoundError(f"Command not found: {cmd[0]}")

        with patch("subprocess.run", side_effect=mock_run):
            code = "x: int = 1"
            is_valid, error = validate_with_ty(code, Path("test.md"), 1)

        assert not is_valid
        assert "ty not found" in error
        assert "uvx ty" in error or "pip install ty" in error

    def test_timeout_handling(self) -> None:
        """Test timeout error includes command and duration context."""

        def mock_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=30)

        with patch("subprocess.run", side_effect=mock_run):
            code = "x: int = 1"
            is_valid, error = validate_with_ty(code, Path("test.md"), 5)

        assert not is_valid
        assert "timeout" in error.lower()
        assert "test.md:5" in error
        assert "30s" in error

    def test_fallback_from_uvx_to_ty(self) -> None:
        """Test that when uvx fails, ty is tried as fallback."""
        call_count = {"uvx": 0, "ty": 0}

        def mock_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
            if cmd[0] == "uvx":
                call_count["uvx"] += 1
                raise FileNotFoundError("uvx not found")
            elif cmd[0] == "ty":
                call_count["ty"] += 1
                return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
            raise FileNotFoundError(f"Unknown command: {cmd[0]}")

        with patch("subprocess.run", side_effect=mock_run):
            code = "x: int = 1"
            is_valid, error = validate_with_ty(code, Path("test.md"), 1)

        assert is_valid, f"Expected valid result, got error: {error}"
        assert call_count["uvx"] == 1, "uvx should be tried first"
        assert call_count["ty"] == 1, "ty should be tried as fallback"

    def test_backwards_compat_alias_works(self) -> None:
        """Test that validate_with_mypy alias actually works, not just exists."""
        code = """
def add(x: int, y: int) -> int:
    return x + y
"""
        # Call the alias and verify it produces the same result
        is_valid, error = validate_with_mypy(code, Path("test.md"), 1)
        assert is_valid, f"Backwards compat alias should work, got error: {error}"
        assert error == ""

    def test_skip_partial_examples(self) -> None:
        """Test that partial examples with ... are skipped."""
        code = """
def incomplete(...):
    # ... implementation
    pass
"""
        is_valid, error = validate_with_ty(code, Path("test.md"), 1)
        assert is_valid
        assert error == ""


class TestValidateDocumentation:
    """Test full documentation validation."""

    def test_validate_directory_with_valid_code(self, tmp_path: Path) -> None:
        """Test validation of directory with valid Python examples."""
        # Create test markdown files
        (tmp_path / "doc1.md").write_text(
            """
```python
def func1() -> None:
    pass
```
"""
        )
        (tmp_path / "doc2.md").write_text(
            """
```python
x: int = 42
```
"""
        )

        total, failed = validate_documentation(tmp_path, strict=False)
        assert total == 2
        assert failed == 0

    def test_validate_directory_with_invalid_code(self, tmp_path: Path) -> None:
        """Test validation catches invalid Python examples."""
        (tmp_path / "bad.md").write_text(
            """
```python
def broken(:
    pass
```
"""
        )

        total, failed = validate_documentation(tmp_path, strict=False)
        assert total == 1
        assert failed == 1

    def test_validate_empty_directory(self, tmp_path: Path) -> None:
        """Test validation of directory with no markdown files."""
        total, failed = validate_documentation(tmp_path, strict=False)
        assert total == 0
        assert failed == 0

    def test_validate_skips_partial_examples(self, tmp_path: Path) -> None:
        """Test that partial examples are skipped in validation."""
        (tmp_path / "partial.md").write_text(
            """
```python
def incomplete(...):
    # ... rest of implementation
    pass
```
"""
        )

        total, failed = validate_documentation(tmp_path, strict=False)
        assert total == 1
        # Should not fail since partial examples are skipped
        assert failed == 0

    def test_validate_nested_directories(self, tmp_path: Path) -> None:
        """Test validation recursively finds markdown files."""
        nested = tmp_path / "sub" / "nested"
        nested.mkdir(parents=True)

        (tmp_path / "root.md").write_text("```python\nx = 1\n```")
        (tmp_path / "sub" / "sub.md").write_text("```python\ny = 2\n```")
        (nested / "nested.md").write_text("```python\nz = 3\n```")

        total, failed = validate_documentation(tmp_path, strict=False)
        assert total == 3
        assert failed == 0


class TestIntegration:
    """Integration tests with actual infrastructure docs."""

    def test_validate_infrastructure_docs_exist(self) -> None:
        """Test that infrastructure docs directory exists."""
        docs_path = Path(".claude/knowledge/infrastructure")
        # This test just verifies the path structure exists
        # Actual validation is done by the test suite in test_infrastructure_docs.py
        if docs_path.exists():
            assert docs_path.is_dir()

    def test_module_can_be_imported(self) -> None:
        """Test that the module can be imported successfully."""
        from persuade.docs import validate

        assert hasattr(validate, "extract_python_blocks")
        assert hasattr(validate, "validate_with_ty")
        assert hasattr(validate, "validate_with_mypy")  # Backwards compat alias
        assert hasattr(validate, "validate_documentation")
        assert hasattr(validate, "main")
        assert hasattr(validate, "logger")  # Logging support
