# Copyright (c) Microsoft. All rights reserved.

import logging
from typing import Any

import pytest

from agent_framework import (
    EdgeDuplicationError,
    Executor,
    GraphConnectivityError,
    TypeCompatibilityError,
    ValidationTypeEnum,
    WorkflowBuilder,
    WorkflowContext,
    WorkflowValidationError,
    handler,
    validate_workflow_graph,
)
from agent_framework._workflows._edge import SingleEdgeGroup


class StringExecutor(Executor):
    @handler
    async def handle_string(self, message: str, ctx: WorkflowContext[str]) -> None:
        await ctx.send_message(message.upper())


class StringAggregator(Executor):
    """A mock executor that aggregates results from multiple executors."""

    @handler
    async def mock_handler(self, messages: list[str], ctx: WorkflowContext[str]) -> None:
        # This mock simply returns the data incremented by 1
        await ctx.send_message("Aggregated: " + ", ".join(messages))


class IntExecutor(Executor):
    @handler
    async def handle_int(self, message: int, ctx: WorkflowContext[int]) -> None:
        await ctx.send_message(message * 2)


class AnyExecutor(Executor):
    @handler
    async def handle_any(self, message: Any, ctx: WorkflowContext[Any]) -> None:
        await ctx.send_message(f"Processed: {message}")


class NoOutputTypesExecutor(Executor):
    @handler
    async def handle_message(self, message: str, ctx: WorkflowContext) -> None:
        await ctx.send_message("processed")  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]


class MultiTypeExecutor(Executor):
    @handler
    async def handle_string(self, message: str, ctx: WorkflowContext[str]) -> None:
        await ctx.send_message(f"String: {message}")

    @handler
    async def handle_int(self, message: int, ctx: WorkflowContext[str]) -> None:
        await ctx.send_message(f"Int: {message}")


def test_valid_workflow_passes_validation():
    executor1 = StringExecutor(id="string_executor")
    executor2 = StringExecutor(id="string_executor_2")

    # Create a valid workflow
    workflow = (
        WorkflowBuilder(start_executor=executor1)
        .add_edge(executor1, executor2)
        .build()  # This should not raise any exceptions
    )

    assert workflow is not None


def test_duplicate_executor_ids_fail_validation():
    executor1 = StringExecutor(id="dup")
    executor2 = IntExecutor(id="dup")

    with pytest.raises(ValueError) as exc_info:
        (WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build())

    assert str(exc_info.value) == "Duplicate executor ID 'dup' detected in workflow."


def test_edge_duplication_validation_fails():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")

    with pytest.raises(EdgeDuplicationError) as exc_info:
        WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor2).build()

    assert "executor1->executor2" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.EDGE_DUPLICATION


def test_type_compatibility_validation_fails():
    string_executor = StringExecutor(id="string_executor")
    int_executor = IntExecutor(id="int_executor")

    with pytest.raises(TypeCompatibilityError) as exc_info:
        WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()

    error = exc_info.value
    assert error.source_executor_id == "string_executor"
    assert error.target_executor_id == "int_executor"
    assert error.validation_type == ValidationTypeEnum.TYPE_COMPATIBILITY


def test_type_compatibility_with_any_type_passes():
    string_executor = StringExecutor(id="string_executor")
    any_executor = AnyExecutor(id="any_executor")

    # This should not raise an exception
    workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, any_executor).build()

    assert workflow is not None


def test_type_compatibility_with_no_output_types():
    no_output_executor = NoOutputTypesExecutor(id="no_output")
    string_executor = StringExecutor(id="string_executor")

    # This should pass validation since no output types are specified
    workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()

    assert workflow is not None


def test_multi_type_executor_compatibility():
    string_executor = StringExecutor(id="string_executor")
    multi_type_executor = MultiTypeExecutor(id="multi_type")

    # String executor outputs strings, multi-type can handle strings
    workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, multi_type_executor).build()

    assert workflow is not None


def test_graph_connectivity_unreachable_executors():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")
    executor3 = StringExecutor(id="executor3")  # This will be unreachable

    with pytest.raises(GraphConnectivityError) as exc_info:
        WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor3, executor2).build()

    assert "unreachable" in str(exc_info.value).lower()
    assert "executor3" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.GRAPH_CONNECTIVITY


def test_graph_connectivity_isolated_executors():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")
    executor3 = StringExecutor(id="executor3")  # This will be isolated

    # Create edges that include an isolated executor (self-loop that's not connected to main graph)
    edge_groups = [
        SingleEdgeGroup(executor1.id, executor2.id),
        SingleEdgeGroup(executor3.id, executor3.id),
    ]  # Self-loop to include in graph

    executors: dict[str, Executor] = {executor1.id: executor1, executor2.id: executor2, executor3.id: executor3}

    with pytest.raises(GraphConnectivityError) as exc_info:
        validate_workflow_graph(edge_groups, executors, executor1, [])

    assert "unreachable" in str(exc_info.value).lower()
    assert "executor3" in str(exc_info.value)


def test_disconnected_start_executor_not_in_graph():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")
    executor3 = StringExecutor(id="executor3")  # Not in graph

    with pytest.raises(GraphConnectivityError) as exc_info:
        WorkflowBuilder(start_executor=executor3).add_edge(executor1, executor2).build()

    assert "The following executors are unreachable from the start executor 'executor3'" in str(exc_info.value)


def test_missing_start_executor():
    with pytest.raises(TypeError):
        WorkflowBuilder()  # type: ignore[call-arg]  # ty: ignore[missing-argument]


def test_workflow_validation_error_base_class():
    error = WorkflowValidationError("Test message", ValidationTypeEnum.EDGE_DUPLICATION)
    assert str(error) == "[EDGE_DUPLICATION] Test message"
    assert error.message == "Test message"
    assert error.validation_type == ValidationTypeEnum.EDGE_DUPLICATION


def test_complex_workflow_validation():
    # Create a workflow with multiple paths
    executor1 = StringExecutor(id="executor1")
    executor2 = MultiTypeExecutor(id="executor2")
    executor3 = StringExecutor(id="executor3")
    executor4 = AnyExecutor(id="executor4")

    workflow = (
        WorkflowBuilder(start_executor=executor1)
        .add_edge(executor1, executor2)  # str -> MultiType (compatible)
        .add_edge(executor2, executor3)  # MultiType -> str (compatible)
        .add_edge(executor2, executor4)  # MultiType -> Any (compatible)
        .add_edge(executor3, executor4)  # str -> Any (compatible)
        .build()
    )

    assert workflow is not None


def test_type_compatibility_inheritance():
    class BaseExecutor(Executor):
        @handler
        async def handle_base(self, message: str, ctx: WorkflowContext[str]) -> None:
            await ctx.send_message("base")

    class DerivedExecutor(Executor):
        @handler
        async def handle_derived(self, message: str, ctx: WorkflowContext[str]) -> None:
            await ctx.send_message("derived")

    base_executor = BaseExecutor(id="base")
    derived_executor = DerivedExecutor(id="derived")

    # This should pass since both handle str
    workflow = WorkflowBuilder(start_executor=base_executor).add_edge(base_executor, derived_executor).build()

    assert workflow is not None


def test_direct_validation_function():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")
    edge_groups = [SingleEdgeGroup(executor1.id, executor2.id)]
    executors: dict[str, Executor] = {executor1.id: executor1, executor2.id: executor2}

    # This should not raise any exceptions
    validate_workflow_graph(edge_groups, executors, executor1, [])

    # Test with invalid start executor
    executor3 = StringExecutor(id="executor3")
    with pytest.raises(GraphConnectivityError):
        validate_workflow_graph(edge_groups, executors, executor3, [])


def test_fan_out_validation():
    source = StringExecutor(id="source")
    target1 = StringExecutor(id="target1")
    target2 = AnyExecutor(id="target2")

    workflow = WorkflowBuilder(start_executor=source).add_fan_out_edges(source, [target1, target2]).build()

    assert workflow is not None


def test_fan_in_validation():
    start_executor = StringExecutor(id="start")
    source1 = StringExecutor(id="source1")
    source2 = StringExecutor(id="source2")
    target = StringAggregator(id="target")

    # Create a proper fan-in by having a start executor that connects to both sources
    workflow = (
        WorkflowBuilder(start_executor=start_executor)
        .add_edge(start_executor, source1)  # Start connects to source1
        .add_edge(start_executor, source2)  # Start connects to source2
        .add_fan_in_edges([source1, source2], target)  # Both sources fan-in to target
        .build()
    )

    assert workflow is not None


def test_chain_validation():
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")
    executor3 = AnyExecutor(id="executor3")

    workflow = WorkflowBuilder(start_executor=executor1).add_chain([executor1, executor2, executor3]).build()

    assert workflow is not None


def test_logging_for_missing_output_types(caplog: Any) -> None:
    caplog.set_level(logging.WARNING)

    # Create executor without output types
    no_output_executor = NoOutputTypesExecutor(id="no_output")
    string_executor = StringExecutor(id="string_executor")

    # This should trigger a warning log
    workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()

    assert workflow is not None
    assert "has no output type annotations" in caplog.text
    assert "Consider adding WorkflowContext[T] generics" in caplog.text


def test_logging_for_missing_input_types(caplog: Any) -> None:
    caplog.set_level(logging.WARNING)

    class NoInputTypesExecutor(Executor):
        # Handler without type annotation for input parameter
        async def handle_message(self, message: Any, ctx: WorkflowContext[Any]) -> None:
            await ctx.send_message("processed")

        def _discover_handlers(self) -> None:
            # Override to manually register handler without type info
            self._handlers[str] = self.handle_message

    string_executor = StringExecutor(id="string_executor")
    no_input_executor = NoInputTypesExecutor(id="no_input")

    # This should pass since NoInputTypesExecutor has no proper input types
    workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, no_input_executor).build()

    assert workflow is not None


def test_self_loop_detection_warning(caplog: Any) -> None:
    caplog.set_level(logging.WARNING)

    executor = StringExecutor(id="self_loop_executor")

    # Create a self-loop
    workflow = WorkflowBuilder(start_executor=executor).add_edge(executor, executor).build()

    assert workflow is not None
    assert "Self-loop detected" in caplog.text
    assert "may cause infinite recursion" in caplog.text


def test_handler_validation_basic(caplog: Any) -> None:
    caplog.set_level(logging.WARNING)

    # Test basic handler validation - ensure the validation code runs without errors
    start_executor = StringExecutor(id="start")
    target_executor = StringExecutor(id="target")

    workflow = WorkflowBuilder(start_executor=start_executor).add_edge(start_executor, target_executor).build()

    assert workflow is not None
    # Just ensure the validation runs without errors


def test_dead_end_detection(caplog: Any) -> None:
    caplog.set_level(logging.INFO)

    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")  # This will be a dead end

    workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()

    assert workflow is not None
    assert "Dead-end executors detected" in caplog.text
    assert "executor2" in caplog.text
    assert "Verify these are intended as final nodes" in caplog.text


def test_successful_type_compatibility_logging(caplog: Any) -> None:
    caplog.set_level(logging.DEBUG)

    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")

    workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()

    assert workflow is not None
    assert "Type compatibility validated for edge" in caplog.text
    assert "Compatible type pairs" in caplog.text


def test_multiple_dead_ends_detection(caplog: Any) -> None:
    caplog.set_level(logging.INFO)

    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")  # Dead end
    executor3 = StringExecutor(id="executor3")  # Dead end

    workflow = (
        WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor3).build()
    )

    assert workflow is not None
    assert "Dead-end executors detected" in caplog.text
    assert "executor2" in caplog.text and "executor3" in caplog.text


def test_single_executor_workflow(caplog: Any) -> None:
    caplog.set_level(logging.INFO)

    # Test workflow with minimal structure
    executor1 = StringExecutor(id="executor1")
    executor2 = StringExecutor(id="executor2")

    # Create a simple two-executor workflow to avoid graph validation issues
    workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()

    assert workflow is not None
    # Should detect executor2 as dead end
    assert "Dead-end executors detected" in caplog.text


def test_enhanced_type_compatibility_error_details():
    string_executor = StringExecutor(id="string_executor")
    int_executor = IntExecutor(id="int_executor")

    with pytest.raises(TypeCompatibilityError) as exc_info:
        WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()

    error = exc_info.value
    # Verify enhanced error contains detailed type information
    assert "Source executor outputs types" in str(error)
    assert "target executor can only handle types" in str(error)
    assert error.source_types is not None
    assert error.target_types is not None


def test_union_type_compatibility_validation() -> None:
    class UnionOutputExecutor(Executor):
        @handler
        async def handle_message(self, message: str, ctx: WorkflowContext[str | int]) -> None:
            await ctx.send_message("output")

    class UnionInputExecutor(Executor):
        @handler
        async def handle_message(self, message: str, ctx: WorkflowContext[str]) -> None:
            await ctx.send_message("processed")

    union_output = UnionOutputExecutor(id="union_output")
    union_input = UnionInputExecutor(id="union_input")

    # This should pass validation due to type compatibility (str)
    workflow = WorkflowBuilder(start_executor=union_output).add_edge(union_output, union_input).build()

    assert workflow is not None


def test_generic_type_compatibility() -> None:
    class ListOutputExecutor(Executor):
        @handler
        async def handle_message(self, message: str, ctx: WorkflowContext[list[str]]) -> None:
            await ctx.send_message(["output"])

    class ListInputExecutor(Executor):
        @handler
        async def handle_message(self, message: list[str], ctx: WorkflowContext[str]) -> None:
            await ctx.send_message("processed")

    list_output = ListOutputExecutor(id="list_output")
    list_input = ListInputExecutor(id="list_input")

    # This should pass validation for generic type compatibility
    workflow = WorkflowBuilder(start_executor=list_output).add_edge(list_output, list_input).build()

    assert workflow is not None


def test_validation_enum_usage() -> None:
    # Test that all validation types use the enum correctly
    edge_error = EdgeDuplicationError("test->test")
    assert edge_error.validation_type == ValidationTypeEnum.EDGE_DUPLICATION

    type_error = TypeCompatibilityError("source", "target", [str], [int])
    assert type_error.validation_type == ValidationTypeEnum.TYPE_COMPATIBILITY

    graph_error = GraphConnectivityError("test message")
    assert graph_error.validation_type == ValidationTypeEnum.GRAPH_CONNECTIVITY

    # Test enum string representation
    assert str(ValidationTypeEnum.EDGE_DUPLICATION) == "ValidationTypeEnum.EDGE_DUPLICATION"
    assert ValidationTypeEnum.EDGE_DUPLICATION.value == "EDGE_DUPLICATION"


def test_handler_ctx_missing_annotation_raises() -> None:
    # Validation now happens at handler registration time, not workflow build time
    with pytest.raises(ValueError) as exc:

        class BadExecutor(Executor):  # pyright: ignore[reportUnusedClass]
            @handler  # pyright: ignore[reportUnknownArgumentType]
            async def handle(self, message: str, ctx) -> None:  # type: ignore[no-untyped-def]
                pass

    assert "must have a WorkflowContext" in str(exc.value)


def test_handler_ctx_invalid_t_out_entries_raises() -> None:
    # Validation now happens at handler registration time, not workflow build time
    with pytest.raises(ValueError) as exc:

        class BadExecutor(Executor):  # pyright: ignore[reportUnusedClass]
            @handler  # pyright: ignore[reportUnknownArgumentType]
            async def handle(self, message: str, ctx: WorkflowContext[123]) -> None:  # type: ignore[valid-type]  # ty: ignore[invalid-type-form]
                pass

    assert "invalid type entry" in str(exc.value)


def test_handler_ctx_none_is_allowed() -> None:
    class NoneExecutor(Executor):
        @handler
        async def handle(self, message: str, ctx: WorkflowContext) -> None:
            # does not emit
            return None

    start = StringExecutor(id="s")
    none_exec = NoneExecutor(id="n")

    # Should build successfully
    wf = WorkflowBuilder(start_executor=start).add_edge(start, none_exec).build()
    assert wf is not None


def test_handler_ctx_any_is_allowed_but_skips_type_checks(caplog: Any) -> None:
    caplog.set_level(logging.WARNING)

    class AnyOutExecutor(Executor):
        @handler
        async def handle(self, message: str, ctx: WorkflowContext[Any]) -> None:
            return None

    start = StringExecutor(id="s")
    any_out = AnyOutExecutor(id="a")

    # Builds; later edges from this executor will skip type compatibility when outputs are unspecified
    wf = WorkflowBuilder(start_executor=start).add_edge(start, any_out).build()
    assert wf is not None


# region Output Validation Tests


class OutputExecutor(Executor):
    @handler
    async def handle_string(self, message: str, ctx: WorkflowContext[str, str]) -> None:
        pass


def test_output_validation_with_valid_output_executors():
    """Test that output validation passes when output executors exist and have output types."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")

    # Build workflow with valid output executors
    workflow = WorkflowBuilder(start_executor=executor1, output_from=[executor2]).add_edge(executor1, executor2).build()

    assert workflow is not None
    assert {ex.id for ex in workflow.get_output_executors()} == {"executor2"}


def test_output_validation_with_multiple_valid_output_executors():
    """Test that output validation passes with multiple valid output executors."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")
    executor3 = OutputExecutor(id="executor3")

    workflow = (
        WorkflowBuilder(start_executor=executor1, output_from=[executor1, executor3])
        .add_edge(executor1, executor2)
        .add_edge(executor2, executor3)
        .build()
    )

    assert workflow is not None
    assert {ex.id for ex in workflow.get_output_executors()} == {"executor1", "executor3"}


def test_output_validation_fails_for_nonexistent_executor():
    """Test that output validation fails when an output executor doesn't exist in the graph."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")
    edge_groups = [SingleEdgeGroup(executor1.id, executor2.id)]
    executors: dict[str, Executor] = {executor1.id: executor1, executor2.id: executor2}

    # Directly test validation with a nonexistent output executor
    with pytest.raises(WorkflowValidationError) as exc_info:
        validate_workflow_graph(edge_groups, executors, executor1, ["nonexistent_executor"])

    assert "not present in the workflow graph" in str(exc_info.value)
    assert "nonexistent_executor" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_fails_for_executor_without_output_types():
    """Test that output validation fails when an output executor has no output type annotations."""
    executor1 = OutputExecutor(id="executor1")
    no_output_executor = NoOutputTypesExecutor(id="no_output")

    with pytest.raises(WorkflowValidationError) as exc_info:
        (
            WorkflowBuilder(start_executor=executor1, output_from=[no_output_executor])
            .add_edge(executor1, no_output_executor)
            .build()
        )

    assert "must have output type annotations defined" in str(exc_info.value)
    assert "no_output" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_empty_explicit_designation_fails():
    """Test that explicit mode rejects an empty output/intermediate designation."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")

    with pytest.raises(WorkflowValidationError) as exc_info:
        WorkflowBuilder(start_executor=executor1, output_from=[]).add_edge(executor1, executor2).build()

    assert "at least one output or intermediate executor" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_with_valid_intermediate_executors():
    """Test that output validation passes when intermediate executors exist and have output types."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")

    workflow = (
        WorkflowBuilder(start_executor=executor1, intermediate_output_from=[executor1])
        .add_edge(executor1, executor2)
        .build()
    )

    assert workflow is not None
    assert {ex.id for ex in workflow.get_intermediate_executors()} == {"executor1"}
    assert workflow.is_intermediate_executor("executor1")
    assert not workflow.is_terminal_executor("executor2")


def test_output_validation_fails_for_designation_overlap():
    """Test that an executor cannot be both terminal and intermediate."""
    executor1 = OutputExecutor(id="executor1")

    with pytest.raises(WorkflowValidationError) as exc_info:
        WorkflowBuilder(
            start_executor=executor1,
            output_from=[executor1],
            intermediate_output_from=[executor1],
        ).build()

    assert "both output and intermediate" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_fails_for_duplicate_designation():
    """Test that duplicate output or intermediate designation entries are rejected."""
    executor1 = OutputExecutor(id="executor1")

    with pytest.raises(WorkflowValidationError) as exc_info:
        WorkflowBuilder(start_executor=executor1, output_from=[executor1, executor1]).build()

    assert "Duplicate output executor designation" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_fails_for_unknown_intermediate_executor():
    """Test that intermediate designation rejects executors outside the workflow graph."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")
    missing = OutputExecutor(id="missing")

    with pytest.raises(WorkflowValidationError) as exc_info:
        (
            WorkflowBuilder(start_executor=executor1, intermediate_output_from=[missing])
            .add_edge(executor1, executor2)
            .build()
        )

    assert "not present in the workflow graph" in str(exc_info.value)
    assert "missing" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_with_direct_validate_workflow_graph():
    """Test _output_validation directly via validate_workflow_graph function."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")
    edge_groups = [SingleEdgeGroup(executor1.id, executor2.id)]
    executors: dict[str, Executor] = {executor1.id: executor1, executor2.id: executor2}

    # Valid output executors
    validate_workflow_graph(edge_groups, executors, executor1, ["executor2"])

    # Invalid output executor (doesn't exist)
    with pytest.raises(WorkflowValidationError) as exc_info:
        validate_workflow_graph(edge_groups, executors, executor1, ["nonexistent"])

    assert "not present in the workflow graph" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_with_no_output_types_via_direct_validation():
    """Test _output_validation fails for executors without output types via direct validation."""
    executor1 = OutputExecutor(id="executor1")
    no_output_executor = NoOutputTypesExecutor(id="no_output")
    edge_groups = [SingleEdgeGroup(executor1.id, no_output_executor.id)]
    executors: dict[str, Executor] = {executor1.id: executor1, no_output_executor.id: no_output_executor}

    # Should fail because no_output_executor has no output types
    with pytest.raises(WorkflowValidationError) as exc_info:
        validate_workflow_graph(edge_groups, executors, executor1, ["no_output"])

    assert "must have output type annotations defined" in str(exc_info.value)
    assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION


def test_output_validation_partial_invalid_list():
    """Test that output validation fails if any executor in the list is invalid."""
    executor1 = OutputExecutor(id="executor1")
    executor2 = OutputExecutor(id="executor2")
    edge_groups = [SingleEdgeGroup(executor1.id, executor2.id)]
    executors: dict[str, Executor] = {executor1.id: executor1, executor2.id: executor2}

    # First executor is valid, second doesn't exist - validation should fail
    with pytest.raises(WorkflowValidationError) as exc_info:
        validate_workflow_graph(edge_groups, executors, executor1, ["executor2", "nonexistent"])

    assert "not present in the workflow graph" in str(exc_info.value)
    assert "nonexistent" in str(exc_info.value)


def test_output_validation_type_enum_value():
    """Test that OUTPUT_VALIDATION is properly defined in ValidationTypeEnum."""
    assert hasattr(ValidationTypeEnum, "OUTPUT_VALIDATION")
    assert ValidationTypeEnum.OUTPUT_VALIDATION.value == "OUTPUT_VALIDATION"


# endregion
