"""Unit tests for plugin connector _inspect_plugin_package method.

Tests cover:
- Extracting requirements.txt from ZIP
- Parsing dependency lines
- Handling missing requirements.txt
- Handling empty/malformed requirements.txt
"""

from __future__ import annotations

import zipfile
import io
from unittest.mock import Mock
from importlib import import_module


def get_connector_module():
    """Lazy import to avoid circular import issues."""
    return import_module('langbot.pkg.plugin.connector')


def create_mock_connector():
    """Create a mock PluginRuntimeConnector instance for testing."""
    connector = get_connector_module()
    mock_app = Mock()
    mock_app.logger = Mock()
    mock_app.instance_config = Mock()
    mock_app.instance_config.data = {'plugin': {'enable': True}}

    # Mock disconnect callback
    async def mock_disconnect_callback(connector):
        pass

    return connector.PluginRuntimeConnector(mock_app, mock_disconnect_callback)


def create_zip_with_requirements(requirements_content: str) -> bytes:
    """Create a ZIP file containing requirements.txt with given content."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w') as zf:
        zf.writestr('requirements.txt', requirements_content)
    return buf.getvalue()


def create_zip_with_nested_requirements(requirements_content: str) -> bytes:
    """Create a ZIP file with requirements.txt in nested directory."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w') as zf:
        zf.writestr('plugin/requirements.txt', requirements_content)
    return buf.getvalue()


def create_zip_without_requirements() -> bytes:
    """Create a ZIP file without requirements.txt."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w') as zf:
        zf.writestr('main.py', 'print("hello")')
        zf.writestr('manifest.yaml', 'name: test')
    return buf.getvalue()


class TestExtractDepsMetadata:
    """Tests for dependency metadata extraction from plugin packages."""

    def test_extract_simple_requirements(self):
        """Test extracting simple requirements.txt."""
        connector_instance = create_mock_connector()

        # Create test ZIP
        zip_bytes = create_zip_with_requirements('requests>=2.0\nflask==1.0\nnumpy')

        # Create task context
        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        assert task_context.metadata.get('deps_total') == 3
        assert task_context.metadata.get('deps_list') == ['requests>=2.0', 'flask==1.0', 'numpy']

    def test_extract_requirements_with_comments_and_empty_lines(self):
        """Test that comments and empty lines are filtered."""
        connector_instance = create_mock_connector()

        requirements = """# This is a comment
requests>=2.0

# Another comment
flask==1.0

numpy"""
        zip_bytes = create_zip_with_requirements(requirements)

        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        assert task_context.metadata.get('deps_total') == 3
        assert '# This is a comment' not in task_context.metadata.get('deps_list', [])

    def test_extract_nested_requirements(self):
        """Test extracting requirements.txt from nested directory."""
        connector_instance = create_mock_connector()

        zip_bytes = create_zip_with_nested_requirements('requests\nflask')

        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        # Should find nested requirements.txt (ends with 'requirements.txt')
        assert task_context.metadata.get('deps_total') == 2

    def test_no_requirements_in_zip(self):
        """Test handling ZIP without requirements.txt."""
        connector_instance = create_mock_connector()

        zip_bytes = create_zip_without_requirements()

        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        # metadata should remain empty (no deps found)
        assert task_context.metadata.get('deps_total') is None
        assert task_context.metadata.get('deps_list') is None

    def test_empty_requirements_file(self):
        """Test handling empty requirements.txt."""
        connector_instance = create_mock_connector()

        zip_bytes = create_zip_with_requirements('')

        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        # deps_total should be 0 (empty list after filtering)
        assert task_context.metadata.get('deps_total') == 0
        assert task_context.metadata.get('deps_list') == []

    def test_requirements_only_comments(self):
        """Test handling requirements.txt with only comments."""
        connector_instance = create_mock_connector()

        requirements = """# Comment 1
# Comment 2
# Comment 3"""
        zip_bytes = create_zip_with_requirements(requirements)

        task_context = Mock()
        task_context.metadata = {}

        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        assert task_context.metadata.get('deps_total') == 0
        assert task_context.metadata.get('deps_list') == []

    def test_task_context_none_returns_early(self):
        """Test that method returns early when task_context is None."""
        connector_instance = create_mock_connector()

        zip_bytes = create_zip_with_requirements('requests')

        # Should return without error when task_context is None
        connector_instance._inspect_plugin_package(zip_bytes, None)

        # No exception should be raised

    def test_malformed_zip_handling(self):
        """Test handling malformed ZIP bytes."""
        connector_instance = create_mock_connector()

        # Invalid ZIP bytes
        invalid_bytes = b'not a valid zip file'

        task_context = Mock()
        task_context.metadata = {}

        # Should silently handle exception (pass in try/except)
        connector_instance._inspect_plugin_package(invalid_bytes, task_context)

        # metadata should remain unchanged
        assert task_context.metadata == {}

    def test_requirements_with_unicode_decode_error(self):
        """Test handling requirements.txt with non-UTF8 content."""
        connector_instance = create_mock_connector()

        # Create ZIP with non-UTF8 content in requirements.txt
        buf = io.BytesIO()
        with zipfile.ZipFile(buf, 'w') as zf:
            # Write bytes that will cause decode issues
            # \x80 is invalid UTF-8, but errors='ignore' will skip it
            zf.writestr('requirements.txt', b'requests\nflask\n\x80invalid')
        zip_bytes = buf.getvalue()

        task_context = Mock()
        task_context.metadata = {}

        # errors='ignore' will decode \x80invalid as 'invalid' (skipping \x80)
        connector_instance._inspect_plugin_package(zip_bytes, task_context)

        # All 3 lines will be parsed (requests, flask, invalid)
        assert task_context.metadata.get('deps_total') == 3
        assert 'invalid' in task_context.metadata.get('deps_list', [])
