
import pytest
from unittest.mock import Mock, call
import re 
import ast 
from project_generator.domain.usecases.update_use_case import UpdateProjectUseCase
from project_generator.domain.models.project_models import ProjectInventory


@pytest.fixture
def mock_analyzer():
    analyzer = Mock()

    analyzer.analyze.return_value = ProjectInventory(
        is_valid=True,
        project_path="/fake/project",
        existing_tools=[],
        existing_prompts=[],
        existing_resources=[],
        existing_resource_uris=[]
    )
    return analyzer

@pytest.fixture
def mock_injector():
    injector = Mock()
    return injector

@pytest.fixture
def update_use_case(mock_analyzer, mock_injector):
    return UpdateProjectUseCase(analyzer=mock_analyzer, injector=mock_injector)


def test_execute_invalid_project(update_use_case, mock_analyzer):
    mock_analyzer.analyze.return_value.is_valid = False
    with pytest.raises(FileNotFoundError, match="No se encontró un proyecto MCP válido"):
        update_use_case.execute([], [], [])

def test_execute_no_components_to_add(update_use_case):
    count, warnings, msg = update_use_case.execute([], [], [])
    assert count == 0
    assert msg == "No components specified to add."
    assert warnings == []

def test_execute_name_collision_tool(update_use_case, mock_analyzer):
    mock_analyzer.analyze.return_value.existing_tools = ["existing_tool"]
    tools_to_add = [{"name": "existing_tool", "description": "desc", "params": "", "return_type": "dict"}]
    with pytest.raises(ValueError, match="La herramienta 'existing_tool' ya existe."):
        update_use_case.execute(tools_to_add, [], [])



def test_execute_validation_invalid_return_type(update_use_case):
    tools_to_add = [{"name": "bad_tool", "description": "desc", "params": "", "return_type": "List<>"}]
    with pytest.raises(ValueError, match="no es una anotación de tipo Python válida"):
        update_use_case.execute(tools_to_add, [], [])

def test_execute_validation_uri_collision_internal(update_use_case):
    resources_to_add = [
        {"name": "res1", "uri": "test://{id}", "description": "d", "params": "p", "return_type": "t"},
        {"name": "res2", "uri": "test://{other_id}", "description": "d", "params": "p", "return_type": "t"}
    ]
    with pytest.raises(ValueError, match="Conflicto de URI detectado entre nuevos resources"):
        update_use_case.execute([], [], resources_to_add)

def test_execute_validation_uri_collision_existing(update_use_case, mock_analyzer):
    mock_analyzer.analyze.return_value.existing_resource_uris = ["test://{id_existente}"]
    resources_to_add = [{"name": "res_new", "uri": "test://{new_id}", "description": "d", "params": "p", "return_type": "t"}]
    with pytest.raises(ValueError, match="colisiona con un resource existente"):
        update_use_case.execute([], [], resources_to_add)

def test_execute_backup_and_inject_success(update_use_case, mock_injector, mocker):
    mock_create_backup = mocker.patch.object(update_use_case, '_create_backup', return_value="/fake/backup/path")
    tools_to_add = [{"name": "new_tool", "description": "d", "params": "p", "return_type": "t"}]
    prompts_to_add = [{"name": "new_prompt", "description": "d"}]
    resources_to_add = [{"name": "new_res", "uri":"u", "description": "d", "params":"p", "return_type": "t"}]

    count, warnings, msg = update_use_case.execute(tools_to_add, prompts_to_add, resources_to_add)

    assert count == 3
    assert msg == "Backup exitoso en: /fake/backup/path"
    mock_create_backup.assert_called_once()
    mock_injector.inject_tools.assert_called_once_with(tools_to_add, mocker.ANY)
    mock_injector.inject_prompts.assert_called_once_with(prompts_to_add, mocker.ANY)
    mock_injector.inject_resources.assert_called_once_with(resources_to_add, mocker.ANY)

def test_execute_injection_fails_rollback_called(update_use_case, mock_injector, mocker):
    mock_create_backup = mocker.patch.object(update_use_case, '_create_backup', return_value="/fake/backup/rollback")
    mock_restore_backup = mocker.patch.object(update_use_case, '_restore_backup')
    mock_injector.inject_tools.side_effect = Exception("Injection failed!")
    tools_to_add = [{"name": "fail_tool", "description": "d", "params": "p", "return_type": "t"}]

    with pytest.raises(RuntimeError, match="La inyección falló y el proyecto fue restaurado"):
        update_use_case.execute(tools_to_add, [], [])

    mock_create_backup.assert_called_once()
    mock_injector.inject_tools.assert_called_once()
    mock_restore_backup.assert_called_once_with("/fake/backup/rollback", "/fake/project")





def test_create_backup_success(update_use_case, mocker):
    mocker.patch("os.makedirs")
    mock_copytree = mocker.patch("shutil.copytree")
    mock_cleanup = mocker.patch.object(update_use_case, '_cleanup_old_backups')
    mocker.patch("datetime.datetime") 

    backup_path = update_use_case._create_backup("/fake/project")

    assert backup_path.startswith("/fake/project/.mcp_backups/backup_")
    mock_copytree.assert_called_once_with("/fake/project/src", backup_path)
    mock_cleanup.assert_called_once()

def test_cleanup_old_backups(update_use_case, mocker):
    mocker.patch("os.listdir", return_value=["backup_1", "backup_2", "backup_3", "backup_4", "backup_5", "backup_6", "other_file"])
    mocker.patch("os.path.isdir", return_value=True)

    mocker.patch("os.path.getmtime", side_effect=[1, 2, 3, 4, 5, 6])
    mock_rmtree = mocker.patch("shutil.rmtree")

    update_use_case._max_backups = 5 
    update_use_case._cleanup_old_backups("/fake/project/.mcp_backups")


    mock_rmtree.assert_called_once_with("/fake/project/.mcp_backups/backup_1")

def test_restore_backup_success(update_use_case, mocker):
    mocker.patch("os.path.exists", return_value=True)
    mock_rmtree = mocker.patch("shutil.rmtree")
    mock_copytree = mocker.patch("shutil.copytree")

    update_use_case._restore_backup("/fake/backup/path", "/fake/project")

    mock_rmtree.assert_called_once_with("/fake/project/src")
    mock_copytree.assert_called_once_with("/fake/backup/path", "/fake/project/src")


def test_validate_components_warnings(update_use_case, mock_analyzer):
    inventory = mock_analyzer.analyze.return_value
    tools = [{"name": "tool_no_return", "description": "d", "params": "p"}] 

    resources = [{"name": "res_no_uri_no_return", "description": "d", "params": "p"}] 
    warnings = update_use_case._validate_components(tools, [], resources, inventory)

    assert len(warnings) == 3
    assert any("tool_no_return': No se especificó tipo de retorno" in w for w in warnings)
    assert any("res_no_uri_no_return': No se especificó tipo de retorno" in w for w in warnings)
    assert any("res_no_uri_no_return': No se especificó URI" in w for w in warnings)

def test_create_backup_fails(update_use_case, mocker):
    mocker.patch("os.makedirs")
    mocker.patch("shutil.copytree", side_effect=OSError("Disk full"))
    with pytest.raises(OSError, match="No se pudo crear el backup"):
        update_use_case._create_backup("/fake/project")

def test_cleanup_old_backups_fails(update_use_case, mocker, caplog):
    mocker.patch("os.listdir", return_value=["backup_1", "backup_2", "backup_3", "backup_4", "backup_5", "backup_6"])
    mocker.patch("os.path.isdir", return_value=True)
    mocker.patch("os.path.getmtime", side_effect=[1, 2, 3, 4, 5, 6])
    mocker.patch("shutil.rmtree", side_effect=OSError("File in use"))
    update_use_case._max_backups = 5
    update_use_case._cleanup_old_backups("/fake/backups")
    assert "No se pudo limpiar backups antiguos: File in use" in caplog.text

def test_restore_backup_fails(update_use_case, mocker, caplog):
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("shutil.rmtree", side_effect=OSError("Cannot delete src"))

    try:
        update_use_case._restore_backup("/fake/backup/path", "/fake/project")
    except OSError:
        pass
    assert "¡FALLO CRÍTICO DURANTE LA RESTAURACIÓN" in caplog.text

def test_execute_general_error_pre_injection(update_use_case, mock_analyzer, mocker):

    mock_analyzer.analyze.return_value.is_valid = True
    mocker.patch.object(update_use_case, '_create_backup', side_effect=Exception("Backup failed"))
    tools_to_add = [{"name": "new_tool", "description": "d", "params": "p", "return_type": "t"}]
    with pytest.raises(Exception, match="Backup failed"):
        update_use_case.execute(tools_to_add, [], [])

def test_execute_general_error_and_cleanup_fails(update_use_case, mock_injector, mocker, caplog):


    mock_create_backup = mocker.patch.object(update_use_case, '_create_backup', return_value="/fake/backup/path")

    mocker.patch("os.path.exists", return_value=True)

    mock_injector.inject_tools.side_effect = Exception("Injection failed")

    mock_rmtree_cleanup = mocker.patch("shutil.rmtree", side_effect=OSError("Cleanup failed"))

    tools_to_add = [{"name": "new_tool", "description": "d", "params": "p", "return_type": "t"}]
    

    with pytest.raises(RuntimeError, match="La inyección falló"):
        update_use_case.execute(tools_to_add, [], [])


    assert "¡Fallo durante la inyección de código!: Injection failed" in caplog.text




    assert "¡FALLO CRÍTICO DURANTE LA RESTAURACIÓN" in caplog.text 
    assert "Cleanup failed" in caplog.text

    mock_create_backup.assert_called_once()
    mock_injector.inject_tools.assert_called_once()

    mock_rmtree_cleanup.assert_called()