
import pytest
from unittest.mock import Mock, mock_open, call
import os
from project_generator.infrastructure.driven_adapters.project_analyzer_adapter import ProjectAnalyzerAdapter
from project_generator.domain.models.project_models import ProjectInventory

@pytest.fixture
def analyzer():
    return ProjectAnalyzerAdapter()


TOOLS_CONTENT = """
@mcp.tool()
async def tool_one(...): ...


@mcp.tool()
async def tool_two(...): ...
"""
PROMPTS_CONTENT = """
@mcp.prompt("prompt_a")
async def prompt_a(...): ...

@mcp.prompt("prompt_b")
async def prompt_b(...): ...
"""
RESOURCES_CONTENT = """
@mcp.resource("res://uri/{id}")
async def resource_x(...): ...

@mcp.resource("other://{name}")
async def resource_y(...): ...
"""

def test_analyze_invalid_project(analyzer, mocker):
    mocker.patch("os.path.exists", return_value=False)
    inventory = analyzer.analyze("/invalid/path")
    assert inventory.is_valid is False
    assert inventory.project_path == "/invalid/path"

def test_analyze_valid_project_no_components(analyzer, mocker):
    mocker.patch("os.path.exists", side_effect=lambda p: 'pyproject.toml' in p or 'src' == os.path.basename(p) or '/fake/project' in p)

    mocker.patch("builtins.open", mock_open(read_data=""))

    inventory = analyzer.analyze("/fake/project")
    assert inventory.is_valid is True
    assert inventory.project_path == "/fake/project"
    assert inventory.existing_tools == []
    assert inventory.existing_prompts == []
    assert inventory.existing_resources == []
    assert inventory.existing_resource_uris == []

def test_analyze_valid_project_with_components(analyzer, mocker):

    def exists_side_effect(path):
        if path == '/fake/project/pyproject.toml': return True
        if path == '/fake/project/src': return True
        if path == '/fake/project/src/infrastructure/entry_points/mcp/tools.py': return True
        if path == '/fake/project/src/infrastructure/entry_points/mcp/prompts.py': return True
        if path == '/fake/project/src/infrastructure/entry_points/mcp/resources.py': return True
        return False
    mocker.patch("os.path.exists", side_effect=exists_side_effect)


    def open_side_effect(path, mode='r'):
        if 'tools.py' in path: return mock_open(read_data=TOOLS_CONTENT).return_value
        if 'prompts.py' in path: return mock_open(read_data=PROMPTS_CONTENT).return_value
        if 'resources.py' in path: return mock_open(read_data=RESOURCES_CONTENT).return_value
        return mock_open(read_data="").return_value
    mocker.patch("builtins.open", side_effect=open_side_effect)
    mocker.patch("os.path.abspath", side_effect=lambda p: p if p.startswith('/') else '/' + p)

    inventory = analyzer.analyze("/fake/project")

    assert inventory.is_valid is True
    assert inventory.existing_tools == ["tool_one", "tool_two"]
    assert inventory.existing_prompts == ["prompt_a", "prompt_b"]
    assert inventory.existing_resources == ["resource_x", "resource_y"]
    assert inventory.existing_resource_uris == ["res://uri/{id}", "other://{name}"]

def test_find_components_io_error(analyzer, mocker):
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("builtins.open", side_effect=IOError("Read error"))

    result = analyzer._find_components("any/path.py", r"pattern")
    assert result == []
    result_multi = analyzer._find_components_multiple_groups("any/path.py", r"pattern")
    assert result_multi == []

def test_find_components_with_tuple_match(analyzer, mocker):
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("builtins.open", mock_open(read_data="key=val\nkey2=val2"))

    pattern = r"(\w+)=(\w+)"
    

    result = analyzer._find_components("any/path.py", pattern, group_index=2)
    assert result == ["val", "val2"]


    result_group1 = analyzer._find_components("any/path.py", pattern, group_index=1)
    assert result_group1 == ["key", "key2"]
