# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
#
# Portions derived from https://github.com/microsoft/autogen are under the MIT License.
# SPDX-License-Identifier: MIT
import tempfile

import pytest

from autogen.coding.base import CodeBlock
from autogen.coding.func_with_reqs import FunctionWithRequirements, with_requirements
from autogen.coding.local_commandline_code_executor import LocalCommandLineCodeExecutor
from autogen.import_utils import optional_import_block, run_for_optional_imports

with optional_import_block() as result:
    import pandas


classes_to_test = [LocalCommandLineCodeExecutor]


def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b


@with_requirements(python_packages=["pandas"], global_imports=["pandas"])
def load_data() -> "pandas.DataFrame":
    """Load some sample data.

    Returns:
        pandas.DataFrame: A DataFrame with the following columns: name(str), location(str), age(int)
    """
    data = {
        "name": ["John", "Anna", "Peter", "Linda"],
        "location": ["New York", "Paris", "Berlin", "London"],
        "age": [24, 13, 53, 33],
    }
    return pandas.DataFrame(data)


@with_requirements(global_imports=["NOT_A_REAL_PACKAGE"])
def function_incorrect_import() -> "pandas.DataFrame":
    return pandas.DataFrame()


@with_requirements(python_packages=["NOT_A_REAL_PACKAGE"])
def function_incorrect_dep() -> "pandas.DataFrame":
    return pandas.DataFrame()


def function_missing_reqs() -> "pandas.DataFrame":
    return pandas.DataFrame()


@pytest.mark.parametrize("cls", classes_to_test)
@run_for_optional_imports(["pandas"], "test")
def test_can_load_function_with_reqs(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        executor = cls(work_dir=temp_dir, functions=[load_data])
        code = f"""from {executor.functions_module} import load_data
import pandas

# Get first row's name
print(load_data().iloc[0]['name'])"""

        result = executor.execute_code_blocks(
            code_blocks=[
                CodeBlock(language="python", code=code),
            ]
        )
        assert result.output == "John\n"
        assert result.exit_code == 0


@pytest.mark.parametrize("cls", classes_to_test)
@run_for_optional_imports(["pandas"], "test")
def test_can_load_function(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        executor = cls(work_dir=temp_dir, functions=[add_two_numbers])
        code = f"""from {executor.functions_module} import add_two_numbers
print(add_two_numbers(1, 2))"""

        result = executor.execute_code_blocks(
            code_blocks=[
                CodeBlock(language="python", code=code),
            ]
        )
        assert result.output == "3\n"
        assert result.exit_code == 0


# TODO - only run this test for containerized executors, as the environment is not guaranteed to have pandas installed
# It is common for the local environment to have pandas installed, so this test will not work as expected
# @pytest.mark.parametrize("cls", classes_to_test)
# @skip_on_missing_imports(["pandas"], "test")
# def test_fails_for_missing_reqs(cls) -> None:
#     with tempfile.TemporaryDirectory() as temp_dir:
#         executor = cls(work_dir=temp_dir, functions=[function_missing_reqs])
#         code = f"""from {executor.functions_module} import function_missing_reqs
# function_missing_reqs()"""

#         with pytest.raises(ValueError):
#             executor.execute_code_blocks(
#                 code_blocks=[
#                     CodeBlock(language="python", code=code),
#                 ]
#             )


@pytest.mark.parametrize("cls", classes_to_test)
@run_for_optional_imports(["pandas"], "test")
def test_fails_for_function_incorrect_import(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        executor = cls(work_dir=temp_dir, functions=[function_incorrect_import])
        code = f"""from {executor.functions_module} import function_incorrect_import
function_incorrect_import()"""

        with pytest.raises(ValueError):
            executor.execute_code_blocks(
                code_blocks=[
                    CodeBlock(language="python", code=code),
                ]
            )


@pytest.mark.parametrize("cls", classes_to_test)
@run_for_optional_imports(["pandas"], "test")
def test_fails_for_function_incorrect_dep(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        executor = cls(work_dir=temp_dir, functions=[function_incorrect_dep])
        code = f"""from {executor.functions_module} import function_incorrect_dep
function_incorrect_dep()"""

        with pytest.raises(ValueError):
            executor.execute_code_blocks(
                code_blocks=[
                    CodeBlock(language="python", code=code),
                ]
            )


@pytest.mark.parametrize("cls", classes_to_test)
def test_formatted_prompt(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        executor = cls(work_dir=temp_dir, functions=[add_two_numbers])

        result = executor.format_functions_for_prompt()
        assert (
            '''def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
'''
            in result
        )


@pytest.mark.parametrize("cls", classes_to_test)
def test_formatted_prompt_str_func(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        func = FunctionWithRequirements.from_str(
            '''
def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
'''
        )
        executor = cls(work_dir=temp_dir, functions=[func])

        result = executor.format_functions_for_prompt()
        assert (
            '''def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
'''
            in result
        )


@pytest.mark.parametrize("cls", classes_to_test)
def test_can_load_str_function_with_reqs(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        func = FunctionWithRequirements.from_str(
            '''
def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
'''
        )

        executor = cls(work_dir=temp_dir, functions=[func])
        code = f"""from {executor.functions_module} import add_two_numbers
print(add_two_numbers(1, 2))"""

        result = executor.execute_code_blocks(
            code_blocks=[
                CodeBlock(language="python", code=code),
            ]
        )
        assert result.output == "3\n"
        assert result.exit_code == 0


@pytest.mark.parametrize("cls", classes_to_test)
def test_cant_load_broken_str_function_with_reqs(cls) -> None:
    with pytest.raises(ValueError):
        _ = FunctionWithRequirements.from_str(
            '''
invaliddef add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
'''
        )


@pytest.mark.parametrize("cls", classes_to_test)
def test_cant_run_broken_str_function_with_reqs(cls) -> None:
    with tempfile.TemporaryDirectory() as temp_dir:
        func = FunctionWithRequirements.from_str(
            '''
def add_two_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
'''
        )

        executor = cls(work_dir=temp_dir, functions=[func])
        code = f"""from {executor.functions_module} import add_two_numbers
print(add_two_numbers(object(), False))"""

        result = executor.execute_code_blocks(
            code_blocks=[
                CodeBlock(language="python", code=code),
            ]
        )
        assert "TypeError: unsupported operand type(s) for +:" in result.output
        assert result.exit_code == 1
