"""LLM-powered test generation tool.

Copyright 2025 Smart AI Memory, LLC
Licensed under the Apache License, Version 2.0
"""

import logging
from pathlib import Path

from ._shared import _validate_file_path

logger = logging.getLogger(__name__)


class RealTestGenerator:
    """Generates actual test code using LLM."""

    def __init__(
        self,
        project_root: str = ".",
        output_dir: str = "tests/generated",
        api_key: str | None = None,
        use_llm: bool = True,
    ):
        """Initialize test generator.

        Args:
            project_root: Project root directory
            output_dir: Directory for generated tests (relative to project_root)
            api_key: Anthropic API key (or uses env var)
            use_llm: Whether to use LLM for intelligent test generation

        """
        self.project_root = Path(project_root).resolve()
        self.output_dir = self.project_root / output_dir
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.api_key = api_key
        self.use_llm = use_llm

        # Initialize LLM client if needed
        self._llm = None
        if use_llm:
            self._initialize_llm()

    def _initialize_llm(self):
        """Initialize Anthropic LLM client."""
        try:
            import os

            from anthropic import Anthropic

            # Try to load .env file
            try:
                from dotenv import load_dotenv

                load_dotenv()
            except ImportError:
                pass  # python-dotenv not required

            api_key = self.api_key or os.environ.get("ANTHROPIC_API_KEY")
            if not api_key:
                logger.warning(
                    "No Anthropic API key found. Set ANTHROPIC_API_KEY environment variable "
                    "or create .env file with ANTHROPIC_API_KEY=your_key_here. "
                    "Falling back to basic templates.",
                )
                self.use_llm = False
                return

            self._llm = Anthropic(api_key=api_key)
            logger.info("✓ LLM client initialized successfully with Claude")

        except ImportError as e:
            logger.warning(f"Required package not installed: {e}. Falling back to templates")
            self.use_llm = False
        except Exception as e:  # noqa: BLE001
            logger.warning(f"Failed to initialize LLM: {e}. Falling back to templates")
            self.use_llm = False

    def generate_tests_for_file(self, source_file: str, missing_lines: list[int]) -> Path:
        """Generate tests for uncovered code in a file.

        Args:
            source_file: Path to source file
            missing_lines: Line numbers without coverage

        Returns:
            Path to generated test file

        Raises:
            RuntimeError: If test generation fails

        """
        logger.info(f"Generating tests for {source_file} (lines: {missing_lines[:5]}...)")

        # Read source file
        source_path = Path(source_file)
        if not source_path.exists():
            source_path = self.project_root / source_file

        # Resolve to absolute path for relative_to() to work correctly
        source_path = source_path.resolve()

        try:
            source_code = source_path.read_text()
        except Exception as e:  # noqa: BLE001
            raise RuntimeError(f"Cannot read source file: {e}") from e

        # Create unique test name from full path to avoid collisions
        relative_path = source_path.relative_to(self.project_root)
        parts_str = "_".join(relative_path.parts)
        test_name = f"test_{parts_str.replace('.py', '')}_generated.py"
        test_path = self.output_dir / test_name

        # Generate tests using LLM or template
        if self.use_llm and self._llm:
            test_code = self._generate_llm_tests(source_file, source_code, missing_lines)
        else:
            test_code = self._generate_basic_test_template(source_file, source_code, missing_lines)

        # Write test file
        validated_path = _validate_file_path(str(test_path))
        validated_path.parent.mkdir(parents=True, exist_ok=True)
        validated_path.write_text(test_code)

        logger.info(f"Generated test file: {test_path}")
        return test_path

    def _generate_llm_tests(
        self,
        source_file: str,
        source_code: str,
        missing_lines: list[int],
    ) -> str:
        """Generate tests using LLM (Claude).

        Args:
            source_file: Source file path
            source_code: Source file content
            missing_lines: Uncovered line numbers

        Returns:
            Generated test code

        Raises:
            RuntimeError: If LLM generation fails

        """
        logger.info(f"Using LLM to generate intelligent tests for {source_file}")

        # Extract API signatures using AST
        api_docs = self._extract_api_docs(source_code)

        # Extract module path
        module_path = source_file.replace("/", ".").replace(".py", "")

        # Create prompt for Claude with full context
        prompt = f"""Generate comprehensive pytest tests for the following Python code.

**Source File:** `{source_file}`
**Module Path:** `{module_path}`
**Uncovered Lines:** {missing_lines[:20]}

{api_docs}

**Full Source Code:**
```python
{source_code}
```

**CRITICAL Requirements - API Accuracy:**
1. **READ THE SOURCE CODE CAREFULLY** - Extract exact API signatures from:
   - Dataclass definitions (@dataclass) - use EXACT parameter names
   - Function signatures - match parameter names and types
   - Class __init__ methods - use correct constructor arguments

2. **DO NOT GUESS** parameter names - if you see:
   ```python
   @dataclass
   class Foo:
       bar: str  # Parameter name is 'bar', NOT 'bar_name'
   ```
   Then use: `Foo(bar="value")` NOT `Foo(bar_name="value")`

3. **Computed Properties** - Do NOT pass @property values to constructors:
   - If source has `@property def total(self): return self.a + self.b`
   - Then DO NOT use `Foo(total=10)` - it's computed from `a` and `b`

**Test Requirements:**
1. Write complete, runnable pytest tests
2. Focus on covering uncovered lines: {missing_lines[:10]}
3. Include:
   - Test class with descriptive name
   - Test methods for key functions/classes
   - Proper imports from the actual module path
   - Mock external dependencies (database, API calls, etc.)
   - Edge cases (empty inputs, None, zero, negative numbers)
   - Error handling tests (invalid input, exceptions)
4. Follow pytest best practices
5. Use clear, descriptive test method names
6. Add docstrings explaining what each test validates

**Output Format:**
Return ONLY the Python test code, starting with imports. No markdown, no explanations.
"""

        try:
            # Try Sonnet models only (Capable tier) - do NOT downgrade
            models_to_try = [
                "claude-sonnet-4-6",  # Sonnet 4.6 (February 2026 - latest)
                "claude-3-5-sonnet-20241022",  # 3.5 Sonnet Oct 2024
                "claude-3-5-sonnet-20240620",  # 3.5 Sonnet Jun 2024
            ]

            response = None
            last_error = None

            for model_name in models_to_try:
                try:
                    response = self._llm.messages.create(
                        model=model_name,
                        max_tokens=12000,  # Increased to prevent truncation on large files
                        temperature=0.3,  # Lower temperature for consistent code
                        messages=[{"role": "user", "content": prompt}],
                    )
                    logger.info(f"✓ Using Sonnet model: {model_name}")
                    break
                except Exception as e:  # noqa: BLE001
                    last_error = e
                    logger.debug(f"Model {model_name} not available: {e}")
                    continue

            if response is None:
                error_msg = f"All Sonnet models unavailable. Last error: {last_error}"
                logger.error(error_msg)
                raise RuntimeError(error_msg)

            test_code = response.content[0].text

            # Clean up markdown if present
            if "```python" in test_code:
                test_code = test_code.split("```python")[1].split("```")[0].strip()
            elif "```" in test_code:
                test_code = test_code.split("```")[1].split("```")[0].strip()

            logger.info(f"✓ LLM generated {len(test_code)} chars of test code")
            return test_code

        except Exception as e:  # noqa: BLE001
            logger.error(f"LLM test generation failed: {e}, falling back to template")
            return self._generate_basic_test_template(source_file, source_code, missing_lines)

    def _extract_api_docs(self, source_code: str) -> str:
        """Extract API signatures from source code using AST.

        Args:
            source_code: Python source code

        Returns:
            Formatted API documentation for LLM prompt

        """
        try:
            import sys
            from pathlib import Path

            # Add scripts to path
            scripts_dir = Path(__file__).parent.parent.parent.parent / "scripts"
            if str(scripts_dir) not in sys.path:
                sys.path.insert(0, str(scripts_dir))

            from ast_api_extractor import extract_api_signatures, format_api_docs

            classes, functions = extract_api_signatures(source_code)
            return format_api_docs(classes, functions)
        except Exception as e:  # noqa: BLE001
            logger.warning(f"AST extraction failed: {e}, proceeding without API docs")
            return "# API extraction failed - use source code carefully"

    def _generate_basic_test_template(
        self,
        source_file: str,
        source_code: str,
        missing_lines: list[int],
    ) -> str:
        """Generate basic test template.

        IMPORTANT: Placeholder tests use pytest.skip() to prevent false greens.
        Tests must be implemented before they will pass.

        Args:
            source_file: Source file path
            source_code: Source file content
            missing_lines: Uncovered line numbers

        Returns:
            Test code as string

        """
        # Extract module name
        module_path = source_file.replace("/", ".").replace(".py", "")
        first_line = missing_lines[0] if missing_lines else 0

        template = f'''"""Auto-generated tests for {source_file}.

Coverage gaps on lines: {missing_lines[:10]}

WARNING: This file contains placeholder tests that are skipped by default.
You must implement the actual test logic before they will run.

To allow placeholder tests temporarily, set ATTUNE_ALLOW_PLACEHOLDER_TESTS=1
"""

import os
import pytest


# Check if placeholder tests are allowed (for development only)
ALLOW_PLACEHOLDERS = os.getenv("ATTUNE_ALLOW_PLACEHOLDER_TESTS", "").lower() in ("1", "true")


class TestGeneratedCoverage:
    """Tests to improve coverage for {source_file}."""

    def test_module_imports(self):
        """Test that module can be imported."""
        try:
            import {module_path}
            assert {module_path} is not None
        except ImportError as e:
            pytest.fail(f"Module import failed: {{e}}")

    @pytest.mark.skipif(not ALLOW_PLACEHOLDERS, reason="Placeholder test - implement actual logic")
    def test_placeholder_for_lines_{first_line}(self):
        """Placeholder test for uncovered code.

        TODO: Implement actual test logic for lines {missing_lines[:5]}

        This test is SKIPPED by default to prevent false positive coverage.
        Implement the test logic, then remove the @pytest.mark.skipif decorator.
        """
        pytest.fail(
            "PLACEHOLDER: Implement test logic for lines {missing_lines[:5]}. "
            "Remove @pytest.mark.skipif when done."
        )
'''
        return template
