"""Tests for the CodeGenerator class."""

import json
from unittest.mock import Mock, patch, MagicMock

import pytest

from scry_run.generator import (
    CodeGenerator,
    GeneratedCode,
    CodeGenerationError,
    CodeValidationError,
)


class TestGeneratedCode:
    """Tests for GeneratedCode dataclass."""

    def test_create_generated_code(self):
        """Test creating a GeneratedCode instance."""
        result = GeneratedCode(
            code="def test(): pass",
            code_type="method",
            docstring="A test method",
            dependencies=["import os"],
            packages=["requests"],
        )

        assert result.code == "def test(): pass"
        assert result.code_type == "method"
        assert result.docstring == "A test method"
        assert result.dependencies == ["import os"]
        assert result.packages == ["requests"]

    def test_generated_code_packages_default(self):
        """Test that packages defaults to empty list."""
        result = GeneratedCode(
            code="def test(): pass",
            code_type="method",
            docstring="A test method",
            dependencies=[],
        )

        assert result.packages == []


class TestCodeGenerator:
    """Tests for CodeGenerator class."""

    def test_init_without_backend_raises(self):
        """Test that init without available backend raises ValueError."""
        with patch.dict("os.environ", {}, clear=True):
            with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=False):
                with pytest.raises(ValueError, match="No available backend"):
                    CodeGenerator()

    def test_init_with_claude_backend(self):
        """Test init with claude backend."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")
            assert generator._backend is not None

    def test_validate_code_valid(self):
        """Test validation of valid Python code."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            # Should not raise
            generator._validate_code("def test(): return 42")

    def test_validate_code_invalid(self):
        """Test validation of invalid Python code."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            with pytest.raises(CodeValidationError, match="syntax error"):
                generator._validate_code("def test( return 42")

    def test_parse_response_valid_json(self):
        """Test parsing valid JSON response."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            response = json.dumps({
                "code": "def test(): pass",
                "code_type": "method",
                "docstring": "Test",
                "dependencies": [],
                "packages": ["requests"],
            })

            result = generator._parse_response(response)

            assert isinstance(result, GeneratedCode)
            assert result.code == "def test(): pass"
            assert result.packages == ["requests"]

    def test_parse_response_invalid_json(self):
        """Test parsing invalid JSON raises error."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            with pytest.raises(CodeGenerationError, match="Failed to parse JSON"):
                generator._parse_response("not valid json")

    def test_parse_response_missing_field(self):
        """Test parsing response with missing field raises error."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            response = json.dumps({
                "code": "def test(): pass",
                # Missing other required fields
            })

            with pytest.raises(CodeGenerationError, match="Missing required field"):
                generator._parse_response(response)

    def test_build_prompt_contains_context(self):
        """Test that build_prompt includes the context."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            prompt = generator._build_prompt(
                context="# My codebase\nclass Foo: pass",
                class_name="Foo",
                attr_name="bar",
            )

            assert "# My codebase" in prompt
            assert "Foo" in prompt
            assert "bar" in prompt

    def test_build_prompt_classmethod_flag(self):
        """Test that build_prompt handles classmethod flag."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            prompt_instance = generator._build_prompt(
                context="ctx",
                class_name="Foo",
                attr_name="bar",
                is_classmethod=False,
            )

            prompt_class = generator._build_prompt(
                context="ctx",
                class_name="Foo",
                attr_name="bar",
                is_classmethod=True,
            )

            assert "instance method/property" in prompt_instance
            assert "class method/property" in prompt_class

    def test_generate_success(self):
        """Test successful code generation."""
        from scry_run.backends.base import GeneratedCode as BackendGeneratedCode

        mock_backend = Mock()
        mock_backend.generate_code.return_value = BackendGeneratedCode(
            code="def bar(self):\n    return 42",
            code_type="method",
            docstring="Returns 42",
            dependencies=[],
            packages=[],
        )

        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            with patch("scry_run.backends.get_backend", return_value=mock_backend):
                generator = CodeGenerator(backend="claude")

                result = generator.generate(
                    context="class Foo: pass",
                    class_name="Foo",
                    attr_name="bar",
                )

                assert isinstance(result, GeneratedCode)
                assert result.code == "def bar(self):\n    return 42"
                assert result.code_type == "method"
                assert result.packages == []

    def test_generate_retries_on_validation_error(self):
        """Test that generate retries when validation fails."""
        from scry_run.backends.base import GeneratedCode as BackendGeneratedCode

        # Create a mock backend
        mock_backend = Mock()

        # Second call: valid code
        good_result = BackendGeneratedCode(
            code="def bar(self): return 42",
            code_type="method",
            docstring="Good",
            dependencies=[],
            packages=[],
        )

        # The backend raises CodeValidationError on syntax error
        # Generator catches this and retries
        mock_backend.generate_code.side_effect = [
            CodeValidationError("Generated code has syntax error"),  # First attempt fails
            good_result,  # Second attempt succeeds
        ]

        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            with patch("scry_run.backends.get_backend", return_value=mock_backend):
                generator = CodeGenerator(backend="claude")
                generator._backend = mock_backend

                result = generator.generate(
                    context="class Foo: pass",
                    class_name="Foo",
                    attr_name="bar",
                )

                assert result.code == "def bar(self): return 42"
                assert mock_backend.generate_code.call_count == 2

    def test_generate_fails_after_max_retries(self):
        """Test that generate fails after max retries."""
        # Create a mock backend that always fails with validation error
        mock_backend = Mock()
        mock_backend.generate_code.side_effect = CodeValidationError("Generated code has syntax error")

        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            with patch("scry_run.backends.get_backend", return_value=mock_backend):
                generator = CodeGenerator(backend="claude")
                generator._backend = mock_backend

                # Should fail after MAX_RETRIES attempts with CodeValidationError
                with pytest.raises(CodeValidationError):
                    generator.generate(
                        context="class Foo: pass",
                        class_name="Foo",
                        attr_name="bar",
                    )

                # Verify it retried MAX_RETRIES times
                assert mock_backend.generate_code.call_count == generator.MAX_RETRIES

    def test_build_frame_prompt_basic(self):
        """Test that _build_frame_prompt generates minimal prompt."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            prompt = generator._build_frame_prompt(
                class_name="MyClass",
                attr_name="my_method",
            )

            assert "MyClass" in prompt
            assert "my_method" in prompt
            assert "instance method/property" in prompt
            assert "JSON" in prompt

    def test_build_frame_prompt_with_runtime_context(self):
        """Test that _build_frame_prompt includes runtime context when provided."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            runtime_ctx = """# === RUNTIME CONTEXT ===
# Call stack (most recent call first):
# Frame 0: test_func() at test.py:42
#   result = app.my_method("hello")
#   Local variables:
#     x = 42
#     name = 'test'
"""

            prompt = generator._build_frame_prompt(
                class_name="MyClass",
                attr_name="my_method",
                runtime_context=runtime_ctx,
            )

            assert "MyClass" in prompt
            assert "my_method" in prompt
            assert "# === RUNTIME CONTEXT ===" in prompt
            assert "test_func()" in prompt
            assert "x = 42" in prompt

    def test_build_frame_prompt_classmethod(self):
        """Test that _build_frame_prompt handles classmethod flag."""
        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            generator = CodeGenerator(backend="claude")

            prompt_instance = generator._build_frame_prompt(
                class_name="Foo",
                attr_name="bar",
                is_classmethod=False,
            )

            prompt_class = generator._build_frame_prompt(
                class_name="Foo",
                attr_name="bar",
                is_classmethod=True,
            )

            assert "instance method/property" in prompt_instance
            assert "class method/property" in prompt_class

    def test_generate_extracts_runtime_context(self):
        """Test that generate extracts runtime context from full context."""
        from scry_run.backends.base import GeneratedCode as BackendGeneratedCode

        mock_backend = Mock()
        mock_backend.generate_code.return_value = BackendGeneratedCode(
            code="def bar(self):\n    return 42",
            code_type="method",
            docstring="Returns 42",
            dependencies=[],
            packages=[],
        )
        # Mark as supporting persistent context
        mock_backend.set_context = Mock()

        with patch("scry_run.backends.claude.ClaudeBackend.is_available", return_value=True):
            with patch("scry_run.backends.get_backend", return_value=mock_backend):
                generator = CodeGenerator(backend="claude")

                context_with_runtime = """class Foo: pass

# === RUNTIME CONTEXT ===
# Call stack
#   x = 42
# === END RUNTIME CONTEXT ===
"""

                result = generator.generate(
                    context=context_with_runtime,
                    class_name="Foo",
                    attr_name="bar",
                )

                # Check that set_context was called with static context only
                mock_backend.set_context.assert_called_once()
                static_ctx = mock_backend.set_context.call_args[0][0]
                assert "# === RUNTIME CONTEXT ===" not in static_ctx

                # Check that generate_code was called with prompt containing runtime context
                mock_backend.generate_code.assert_called_once()
                prompt = mock_backend.generate_code.call_args[0][0]
                assert "# === RUNTIME CONTEXT ===" in prompt
                assert "x = 42" in prompt
