import json
import pytest
from pathlib import Path
from unittest.mock import Mock, mock_open, call
from rich.console import Console
import typer
import shutil
import os

from project_generator.infrastructure.entry_points.cli.handlers import CLIHandlers
from project_generator.infrastructure.entry_points.cli.parsers import (
    ToolDefinitionParser, PromptDefinitionParser, ResourceDefinitionParser
)
from project_generator.domain.models.project_models import ProjectRequest, GeneratedProjectInfo
from project_generator.domain.usecases.generation_use_case import GenerateProjectUseCase
from project_generator.domain.usecases.update_use_case import UpdateProjectUseCase
from project_generator.domain.usecases.restore_use_case import RestoreBackupUseCase
from project_generator.domain.models.project_models import ProjectAnalyzerGateway




@pytest.fixture
def mock_generation_use_case():
    use_case = Mock(spec=GenerateProjectUseCase)
    use_case.execute.return_value = GeneratedProjectInfo(
        zip_path="/tmp/test.zip",
        zip_filename="test_project_smcp.zip",
        temp_dir="/tmp/test_dir_gen"
    )
    return use_case

@pytest.fixture
def mock_update_use_case():
    use_case = Mock(spec=UpdateProjectUseCase)
    use_case.execute.return_value = (1, [], "Backup successful at /path/to/backup")
    return use_case

@pytest.fixture
def mock_restore_use_case():
    use_case = Mock(spec=RestoreBackupUseCase)
    use_case.list_backups.return_value = ["backup_1", "backup_2"]
    use_case.execute.return_value = "Project restored successfully from backup_1"
    return use_case

@pytest.fixture
def mock_analyzer():
    analyzer = Mock(spec=ProjectAnalyzerGateway)
    analyzer.analyze.return_value = Mock(is_valid=True, project_path="/fake/project")
    return analyzer

@pytest.fixture
def mock_console():
    return Mock(spec=Console)

@pytest.fixture
def cli_handlers(
    mock_generation_use_case,
    mock_update_use_case,
    mock_restore_use_case,
    mock_analyzer,
    mock_console
):
    return CLIHandlers(
        generation_use_case=mock_generation_use_case,
        update_use_case=mock_update_use_case,
        restore_use_case=mock_restore_use_case,
        analyzer=mock_analyzer,
        tool_parser=ToolDefinitionParser(),
        prompt_parser=PromptDefinitionParser(),
        resource_parser=ResourceDefinitionParser(),
        console=mock_console
    )



def test_handle_from_file_success(cli_handlers, mock_generation_use_case, mocker):
    config_data = {
        "project_name": "test_project_smcp",
        "dynamic_tools": [{"name": "test_tool", "description": "Test", "params":"", "return_type":"dict"}],
        "dynamic_prompts": [],
        "dynamic_resources": []
    }

    mock_file = mock_open(read_data=json.dumps(config_data))
    mocker.patch("pathlib.Path.open", mock_file)
    mock_move = mocker.patch("shutil.move")
    mocker.patch("pathlib.Path.exists", return_value=False)
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("shutil.rmtree")

    config_file = Path("/tmp/config.json")
    output_dir = Path("/tmp/output")

    cli_handlers.handle_from_file(config_file, output_dir)

    mock_generation_use_case.execute.assert_called_once()
    call_args = mock_generation_use_case.execute.call_args[0]
    assert isinstance(call_args[0], ProjectRequest)
    assert call_args[0].project_name == "test_project_smcp"
    assert call_args[1] is False
    mock_move.assert_called_once_with("/tmp/test.zip", output_dir / "test_project_smcp.zip")

def test_handle_create_success_zip(cli_handlers, mock_generation_use_case, mocker):
    mock_move = mocker.patch("shutil.move")
    mocker.patch("pathlib.Path.exists", return_value=False)
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("shutil.rmtree")

    project_name = "another_project_smcp"
    tool_definitions = ["tool1|Desc1|p1:str|str"]
    prompt_definitions = ["prompt1|DescP"]
    resource_definitions = ["res1|uri://{id}|DescR|id:int|dict"]
    output_dir = Path("/tmp/output")

    cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=False)

    mock_generation_use_case.execute.assert_called_once_with(mocker.ANY, False)
    call_args = mock_generation_use_case.execute.call_args[0][0]
    assert call_args.project_name == project_name
    assert len(call_args.dynamic_tools) == 1
    assert len(call_args.dynamic_prompts) == 1
    assert len(call_args.dynamic_resources) == 1
    mock_move.assert_called_once_with("/tmp/test.zip", output_dir / "test_project_smcp.zip")

def test_handle_create_success_no_zip(cli_handlers, mock_generation_use_case, mocker):
    mock_generation_use_case.execute.return_value = GeneratedProjectInfo(
        output_path="/tmp/test_dir_gen/output/folder_project_smcp",
        temp_dir="/tmp/test_dir_gen"
    )
    mock_move = mocker.patch("shutil.move")
    mocker.patch("pathlib.Path.exists", return_value=False)
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("shutil.rmtree")

    project_name = "folder_project_smcp"
    tool_definitions = []
    prompt_definitions = []
    resource_definitions = []
    output_dir = Path("/tmp/output_folder")

    cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=True)

    mock_generation_use_case.execute.assert_called_once_with(mocker.ANY, True)
    mock_move.assert_called_once_with(
        "/tmp/test_dir_gen/output/folder_project_smcp",
        str(output_dir / "folder_project_smcp")
    )

def test_handle_create_invalid_name_suffix(cli_handlers, mock_console):
    project_name = "invalid_project"
    tool_definitions = []
    prompt_definitions = []
    resource_definitions = []
    output_dir = Path("/tmp/output")

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: El nombre del proyecto 'invalid_project' debe terminar con el sufijo '_smcp'.")

def test_handle_create_invalid_name_format(cli_handlers, mock_console):
    project_name = "Invalid-Project_smcp"
    tool_definitions = []
    prompt_definitions = []
    resource_definitions = []
    output_dir = Path("/tmp/output")

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: El nombre del proyecto 'Invalid-Project_smcp' debe estar en snake_case (minúsculas y guiones bajos) y terminar con '_smcp'.")

def test_handle_create_invalid_tool_definition(cli_handlers, mock_console):
    project_name = "valid_project_smcp"
    tool_definitions = ["invalid_format"]
    prompt_definitions = []
    resource_definitions = []
    output_dir = Path("/tmp/output")

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=False)
    assert excinfo.value.exit_code == 1
    assert "Error parsing definition: Tool definition must have 4 parts" in cli_handlers._console.print.call_args[0][0]

def test_handle_interactive_all_components(cli_handlers, mock_generation_use_case, mocker):
    mock_move = mocker.patch("shutil.move")
    mocker.patch("pathlib.Path.exists", return_value=False)
    mocker.patch("os.path.exists", return_value=True)
    mocker.patch("shutil.rmtree")

    mock_prompt = mocker.patch("rich.prompt.Prompt.ask")
    mock_confirm = mocker.patch("rich.prompt.Confirm.ask")

    mock_prompt.side_effect = [
        "interactive_project_smcp",
        "tool_interactive", "Desc Tool", "p1:str", "dict",
        "prompt_interactive", "Desc Prompt",
        "resource_interactive", "uri://{id}", "Desc Res", "id:int", "list"
    ]
    mock_confirm.side_effect = [
        True, False,
        True, False,
        True, False,
        True
    ]

    cli_handlers.handle_interactive(Path("/tmp/output"))

    mock_generation_use_case.execute.assert_called_once()
    call_args = mock_generation_use_case.execute.call_args[0]
    request_arg = call_args[0]
    assert request_arg.project_name == "interactive_project_smcp"
    assert call_args[1] is False
    mock_move.assert_called_once()

def test_execute_generation_file_exists_error_zip(cli_handlers, mock_generation_use_case, mock_console, mocker):
    mocker.patch("pathlib.Path.exists", return_value=True)

    project_request = ProjectRequest(project_name="test_zip_exists_smcp", dynamic_tools=[])
    expected_filename = mock_generation_use_case.execute.return_value.zip_filename
    expected_filepath = Path("/tmp/output") / expected_filename

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers._execute_generation(project_request, Path("/tmp/output"), no_zip=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call(f"Error: File '{expected_filepath}' already exists.")

def test_execute_generation_folder_exists_error_nozip(cli_handlers, mock_generation_use_case, mock_console, mocker):
    mock_generation_use_case.execute.return_value = GeneratedProjectInfo(
        output_path="/tmp/temp_gen/output/test_folder_exists_smcp",
        temp_dir="/tmp/temp_gen"
    )
    mocker.patch("pathlib.Path.exists", return_value=True)
    mocker.patch("os.path.exists", return_value=True)
    mock_rmtree = mocker.patch("shutil.rmtree")

    project_request = ProjectRequest(project_name="test_folder_exists_smcp", dynamic_tools=[])

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers._execute_generation(project_request, Path("/tmp/output"), no_zip=True)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: Directory '/tmp/output/test_folder_exists_smcp' already exists.")
    mock_rmtree.assert_called_with("/tmp/temp_gen")

def test_execute_generation_success_zip_args(cli_handlers, mock_generation_use_case, mocker):
    mocker.patch("pathlib.Path.exists", return_value=False)
    mock_move = mocker.patch("shutil.move")
    mocker.patch("os.path.exists", return_value=True)
    mock_rmtree = mocker.patch("shutil.rmtree")

    project_request = ProjectRequest(project_name="test_zip_args_smcp", dynamic_tools=[])

    cli_handlers._execute_generation(project_request, Path("/tmp/output_zip"), no_zip=False)

    mock_generation_use_case.execute.assert_called_once_with(project_request, False)
    mock_move.assert_called_once_with("/tmp/test.zip", Path("/tmp/output_zip/test_project_smcp.zip"))
    mock_rmtree.assert_called_with("/tmp/test_dir_gen")

def test_execute_generation_success_nozip_args(cli_handlers, mock_generation_use_case, mocker):
    mock_generation_use_case.execute.return_value = GeneratedProjectInfo(
        output_path="/tmp/temp_nozip/output/test_nozip_args_smcp",
        temp_dir="/tmp/temp_nozip"
    )
    mocker.patch("pathlib.Path.exists", return_value=False)
    mock_move = mocker.patch("shutil.move")
    mocker.patch("os.path.exists", return_value=True)
    mock_rmtree = mocker.patch("shutil.rmtree")

    project_request = ProjectRequest(project_name="test_nozip_args_smcp", dynamic_tools=[])

    cli_handlers._execute_generation(project_request, Path("/tmp/output_nozip"), no_zip=True)

    mock_generation_use_case.execute.assert_called_once_with(project_request, True)
    mock_move.assert_called_once_with("/tmp/temp_nozip/output/test_nozip_args_smcp", str(Path("/tmp/output_nozip/test_nozip_args_smcp")))
    mock_rmtree.assert_called_with("/tmp/temp_nozip")

def test_handle_interactive_cancel_generation(cli_handlers, mocker):
    mock_prompt = mocker.patch("rich.prompt.Prompt.ask")
    mock_confirm = mocker.patch("rich.prompt.Confirm.ask")

    mock_prompt.side_effect = ["cancel_project_smcp"]
    mock_confirm.side_effect = [False, False, False, False]

    cli_handlers.handle_interactive(Path("/tmp/output"))

    cli_handlers._console.print.assert_any_call("Project generation cancelled.")

def test_handle_create_general_exception(cli_handlers, mock_generation_use_case, mocker):
    mock_generation_use_case.execute.side_effect = Exception("Unexpected Generation Error")

    project_name = "error_project_smcp"
    tool_definitions = []
    prompt_definitions = []
    resource_definitions = []
    output_dir = Path("/tmp/output")

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_create(project_name, tool_definitions, prompt_definitions, resource_definitions, output_dir, no_zip=False)
    assert excinfo.value.exit_code == 1
    cli_handlers._console.print.assert_any_call("An unexpected error occurred:\nUnexpected Generation Error")

def test_handle_interactive_exception(cli_handlers, mocker):
    mocker.patch("rich.prompt.Prompt.ask", side_effect=Exception("Prompt error test"))

    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_interactive(Path("/tmp/output"))
    assert excinfo.value.exit_code == 1
    cli_handlers._console.print.assert_any_call("An unexpected error occurred:\nPrompt error test")



def test_handle_add_success(cli_handlers, mock_update_use_case, mock_analyzer):
    tool_defs = ["add_tool|Desc Tool|p:int|dict"]
    prompt_defs = ["add_prompt|Desc Prompt"]
    resource_defs = ["add_res|uri|Desc Res|p:str|list"]

    cli_handlers.handle_add(tool_defs, prompt_defs, resource_defs)

    mock_analyzer.analyze.assert_called_once_with(".")
    mock_update_use_case.execute.assert_called_once()
    call_args = mock_update_use_case.execute.call_args[0]

    assert len(call_args) == 3

    tools_arg = call_args[0]
    assert isinstance(tools_arg, list)
    assert len(tools_arg) == 1
    assert tools_arg[0]['name'] == 'add_tool'
    assert tools_arg[0]['params'] == 'p:int'

    prompts_arg = call_args[1]
    assert isinstance(prompts_arg, list)
    assert len(prompts_arg) == 1
    assert prompts_arg[0]['name'] == 'add_prompt'

    resources_arg = call_args[2]
    assert isinstance(resources_arg, list)
    assert len(resources_arg) == 1
    assert resources_arg[0]['name'] == 'add_res'
    assert resources_arg[0]['uri'] == 'uri'

    cli_handlers._console.print.assert_any_call("\n¡Éxito! Se añadieron 1 componente(s) al proyecto.")

def test_handle_add_no_components(cli_handlers, mock_console, mock_analyzer):
    mock_analyzer.analyze.return_value.is_valid = True
    with pytest.raises(typer.Exit):
        cli_handlers.handle_add([], [], [])
    mock_console.print.assert_any_call("Advertencia: No se especificaron componentes para añadir. Usa --tool, --prompt, o --resource.")

def test_handle_add_invalid_project(cli_handlers, mock_analyzer, mock_console):
    mock_analyzer.analyze.return_value.is_valid = False
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_add(["tool|d|p|r"], [], [])
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: No se encontró un proyecto MCP válido en el directorio actual.")

def test_handle_add_validation_error(cli_handlers, mock_update_use_case, mock_console):
    mock_update_use_case.execute.side_effect = ValueError("Name collision!")
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_add(["tool|d|p|r"], [], [])
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: Name collision!")

def test_handle_add_runtime_error(cli_handlers, mock_update_use_case, mock_console):
    mock_update_use_case.execute.side_effect = RuntimeError("Injection failed!")
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_add(["tool|d|p|r"], [], [])
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error Crítico durante la inyección: Injection failed!")



def test_handle_restore_backup_list(cli_handlers, mock_restore_use_case, mock_console):
    cli_handlers.handle_restore_backup(backup_name=None, force=False)
    mock_restore_use_case.list_backups.assert_called_once()
    mock_console.print.assert_any_call("Backups disponibles (más recientes primero):")
    mock_console.print.assert_any_call("  1. backup_1")
    mock_console.print.assert_any_call("  2. backup_2")
    mock_restore_use_case.execute.assert_not_called()

def test_handle_restore_backup_no_backups(cli_handlers, mock_restore_use_case, mock_console):
    mock_restore_use_case.list_backups.return_value = []
    with pytest.raises(typer.Exit):
        cli_handlers.handle_restore_backup(backup_name=None, force=False)
    mock_console.print.assert_any_call("No se encontraron backups disponibles.")

def test_handle_restore_backup_invalid_project(cli_handlers, mock_analyzer, mock_console):
    mock_analyzer.analyze.return_value.is_valid = False
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_restore_backup(backup_name="backup_1", force=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: No se encontró un proyecto MCP válido en el directorio actual.")

def test_handle_restore_backup_name_not_found(cli_handlers, mock_restore_use_case, mock_console):
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_restore_backup(backup_name="non_existent", force=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error: El backup 'non_existent' no existe.")

def test_handle_restore_backup_confirm_yes(cli_handlers, mock_restore_use_case, mocker):
    mock_confirm = mocker.patch("rich.prompt.Confirm.ask", return_value=True)
    cli_handlers.handle_restore_backup(backup_name="backup_1", force=False)
    mock_confirm.assert_called_once()
    mock_restore_use_case.execute.assert_called_once_with("/fake/project", "backup_1")
    cli_handlers._console.print.assert_any_call(f"¡Éxito! {mock_restore_use_case.execute.return_value}")

def test_handle_restore_backup_confirm_no(cli_handlers, mock_restore_use_case, mocker):
    mock_confirm = mocker.patch("rich.prompt.Confirm.ask", return_value=False)
    with pytest.raises(typer.Exit):
        cli_handlers.handle_restore_backup(backup_name="backup_1", force=False)
    mock_confirm.assert_called_once()
    mock_restore_use_case.execute.assert_not_called()
    cli_handlers._console.print.assert_any_call("Restauración cancelada.")

def test_handle_restore_backup_force(cli_handlers, mock_restore_use_case, mocker):
    mock_confirm = mocker.patch("rich.prompt.Confirm.ask")
    cli_handlers.handle_restore_backup(backup_name="backup_1", force=True)
    mock_confirm.assert_not_called()
    mock_restore_use_case.execute.assert_called_once_with("/fake/project", "backup_1")
    cli_handlers._console.print.assert_any_call(f"¡Éxito! {mock_restore_use_case.execute.return_value}")

def test_handle_restore_backup_runtime_error(cli_handlers, mock_restore_use_case, mock_console, mocker):
    mock_restore_use_case.execute.side_effect = RuntimeError("Restore failed!")
    mocker.patch("rich.prompt.Confirm.ask", return_value=True)
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_restore_backup(backup_name="backup_1", force=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Error Crítico durante la restauración: Restore failed!")


def test_handle_from_file_exception(cli_handlers, mock_console, mocker):
    mocker.patch("pathlib.Path.open", side_effect=Exception("Cannot open file"))
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_from_file(Path("bad.json"), Path("."))
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("An unexpected error occurred:\nCannot open file")

def test_execute_generation_cleanup_error(cli_handlers, mock_generation_use_case, mock_console, mocker):


    temp_dir_path = "/tmp/test_dir_gen_cleanup_fail"

    mock_generation_use_case.execute.return_value = GeneratedProjectInfo(
        zip_path="/tmp/temp_zip.zip", 
        zip_filename="test_cleanup_fail_smcp.zip",
        temp_dir=temp_dir_path
    )


    move_exception = Exception("Move failed")
    mocker.patch("shutil.move", side_effect=move_exception)


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

    cleanup_exception = Exception("Cleanup failed in finally")
    mocker.patch("shutil.rmtree", side_effect=cleanup_exception)
    

    mocker.patch("pathlib.Path.exists", return_value=False)


    project_request = ProjectRequest(project_name="test_cleanup_fail_smcp", dynamic_tools=[])


    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers._execute_generation(project_request, Path("/tmp/output"), no_zip=False)


    assert excinfo.value.exit_code == 1


    mock_console.print.assert_any_call(f"An unexpected error occurred:\n{move_exception}")





    

    expected_warning_call = call(f"Warning: Failed to clean up temp directory {temp_dir_path}: {cleanup_exception}")
    

    assert expected_warning_call in mock_console.print.call_args_list, \
        f"Expected call '{expected_warning_call}' not found in actual calls: {mock_console.print.call_args_list}"

def test_handle_add_generic_exception(cli_handlers, mock_update_use_case, mock_console):
    mock_update_use_case.execute.side_effect = Exception("Generic Add Error")
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_add(["tool|d|p|r"], [], [])
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Ocurrió un error inesperado:\nGeneric Add Error")
    
def test_handle_restore_backup_generic_exception(cli_handlers, mock_restore_use_case, mock_console, mocker):
    mock_restore_use_case.execute.side_effect = Exception("Generic Restore Error")
    mocker.patch("rich.prompt.Confirm.ask", return_value=True)
    with pytest.raises(typer.Exit) as excinfo:
        cli_handlers.handle_restore_backup(backup_name="backup_1", force=False)
    assert excinfo.value.exit_code == 1
    mock_console.print.assert_any_call("Ocurrió un error inesperado:\nGeneric Restore Error")
