# Copyright (c) Microsoft. All rights reserved.

from collections.abc import Callable
from typing import Any
from unittest.mock import MagicMock

import pytest

from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import (
    KernelProcessMessageChannel,
)
from semantic_kernel.processes.kernel_process.kernel_process_step_context import (
    KernelProcessStepContext,
)
from semantic_kernel.processes.step_utils import (
    find_input_channels,
    get_fully_qualified_name,
    get_step_class_from_qualified_name,
)


@pytest.fixture
def mock_function() -> Callable[..., Any]:
    @kernel_function
    def mock_function(input: str, context: KernelProcessStepContext) -> None:
        pass

    return mock_function


def test_find_input_channels_empty_functions():
    channel = MagicMock(spec=KernelProcessMessageChannel)
    functions = {}
    with pytest.raises(ValueError) as exc_info:
        find_input_channels(channel, functions)
    assert str(exc_info.value) == "The step has not been initialized."


def test_find_input_channels_with_required_params(mock_function):
    channel = MagicMock(spec=KernelProcessMessageChannel)

    function = KernelFunction.from_method(method=mock_function, plugin_name="test")

    functions = {"TestFunction": function}

    inputs = find_input_channels(channel, functions)

    assert "TestFunction" in inputs
    assert "input" in inputs["TestFunction"]
    assert "context" in inputs["TestFunction"]


@kernel_function
def skipped_function(param1: "Kernel", param2: int = 0) -> None:
    pass


def test_find_input_channels_with_only_skipped_params():
    channel = MagicMock(spec=KernelProcessMessageChannel)

    function = KernelFunction.from_method(method=skipped_function, plugin_name="test")
    functions = {"SkippedFunction": function}

    inputs = find_input_channels(channel, functions)

    expected_inputs = {"SkippedFunction": {}}

    assert inputs == expected_inputs


@kernel_function
def function1(param1: str) -> None:
    pass


@kernel_function
def function2(param2: KernelProcessStepContext, param3: float = 0.0) -> None:
    pass


def test_find_input_channels_multiple_functions():
    channel = MagicMock(spec=KernelProcessMessageChannel)

    function1_instance = KernelFunction.from_method(method=function1, plugin_name="test")
    function2_instance = KernelFunction.from_method(method=function2, plugin_name="test")

    functions = {
        "Function1": function1_instance,
        "Function2": function2_instance,
    }

    inputs = find_input_channels(channel, functions)

    assert "Function1" in inputs
    assert "param1" in inputs["Function1"]
    assert inputs["Function1"]["param1"] is None

    assert "Function2" in inputs
    assert "param2" in inputs["Function2"]
    assert isinstance(inputs["Function2"]["param2"], KernelProcessStepContext)
    assert "param3" not in inputs["Function2"]


def test_get_fully_qualified_name_standard_class():
    class TestClass:
        pass

    result = get_fully_qualified_name(TestClass)
    expected = f"{TestClass.__module__}.TestClass"
    assert result == expected


def test_get_fully_qualified_name_builtin_type():
    result = get_fully_qualified_name(int)
    expected = "builtins.int"
    assert result == expected


def test_get_fully_qualified_name_third_party_class():
    from unittest.mock import MagicMock

    result = get_fully_qualified_name(MagicMock)
    expected = "unittest.mock.MagicMock"
    assert result == expected


def test_get_fully_qualified_name_nested_class():
    class OuterClass:
        class InnerClass:
            pass

    result = get_fully_qualified_name(OuterClass.InnerClass)
    expected = f"{OuterClass.__module__}.InnerClass"
    assert result == expected


# --- Tests for get_step_class_from_qualified_name ---


def test_get_step_class_empty_allowlist_blocks_all():
    """An empty allowlist sequence should block all modules."""
    with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
        get_step_class_from_qualified_name(
            "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
            allowed_module_prefixes=[],
        )


def test_get_step_class_none_allowlist_allows_any():
    """Passing None should disable the allowlist check entirely."""
    from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep

    cls = get_step_class_from_qualified_name(
        "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
        allowed_module_prefixes=None,
    )
    assert cls is KernelProcessStep


def test_get_step_class_prefix_without_dot_exact_match():
    """A prefix without a trailing dot should match the exact module name."""
    from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep

    cls = get_step_class_from_qualified_name(
        "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
        allowed_module_prefixes=["semantic_kernel.processes.kernel_process.kernel_process_step"],
    )
    assert cls is KernelProcessStep


def test_get_step_class_prefix_without_dot_segment_boundary():
    """A prefix without a trailing dot must not match partial segment names."""
    with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
        get_step_class_from_qualified_name(
            "semantic_kernel_evil.some_module.SomeClass",
            allowed_module_prefixes=["semantic_kernel"],
        )


def test_get_step_class_prefix_with_dot_matches_submodule():
    """A prefix with a trailing dot should match submodules."""
    from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep

    cls = get_step_class_from_qualified_name(
        "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep",
        allowed_module_prefixes=["semantic_kernel."],
    )
    assert cls is KernelProcessStep


def test_get_step_class_default_allowlist_blocks_non_sk_module():
    """Default allowlist should block modules outside semantic_kernel."""
    with pytest.raises(ProcessInvalidConfigurationException, match="not in the allowed module prefixes"):
        get_step_class_from_qualified_name("os.path.SomeClass")


def test_get_step_class_invalid_format():
    """Invalid class name format should raise an exception."""
    with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
        get_step_class_from_qualified_name("NoModule")
