"""Tests for coalesce expression function."""

import pytest

from flowire_sdk.node_base import BaseNode


class TestParseCoalesceArgs:
    """Tests for _parse_coalesce_args helper method."""

    def setup_method(self):
        """Create a concrete node instance for testing."""
        # Create minimal concrete implementation for testing
        from pydantic import BaseModel

        from flowire_sdk import BaseNodeOutput, NodeMetadata

        class TestInput(BaseModel):
            value: str = ""

        class TestOutput(BaseNodeOutput):
            result: str = ""

        class TestNode(BaseNode):
            input_schema = TestInput
            output_schema = TestOutput
            metadata = NodeMetadata(name="Test", description="Test node")

            async def execute_logic(self, validated_inputs, context):
                return TestOutput(result="test")

        self.node = TestNode()

    def test_two_simple_args(self):
        """Parse two simple node references."""
        result = self.node._parse_coalesce_args("nodeA.field, nodeB.field")
        assert result == ["nodeA.field", "nodeB.field"]

    def test_three_args_with_literal(self):
        """Parse args with a literal string fallback."""
        result = self.node._parse_coalesce_args('nodeA.field, nodeB.field, "default"')
        assert result == ["nodeA.field", "nodeB.field", '"default"']

    def test_quoted_string_with_comma(self):
        """Commas inside quotes should not split."""
        result = self.node._parse_coalesce_args('nodeA.field, "hello, world"')
        assert result == ["nodeA.field", '"hello, world"']

    def test_single_quoted_string(self):
        """Single quotes should work too."""
        result = self.node._parse_coalesce_args("nodeA.field, 'default'")
        assert result == ["nodeA.field", "'default'"]

    def test_nested_path(self):
        """Nested paths should be preserved."""
        result = self.node._parse_coalesce_args("nodeA.data.user.name, nodeB.data.user.name")
        assert result == ["nodeA.data.user.name", "nodeB.data.user.name"]

    def test_numeric_literal(self):
        """Numeric literals should be preserved."""
        result = self.node._parse_coalesce_args("nodeA.count, 0")
        assert result == ["nodeA.count", "0"]

    def test_boolean_literal(self):
        """Boolean literals should be preserved."""
        result = self.node._parse_coalesce_args("nodeA.flag, false")
        assert result == ["nodeA.flag", "false"]

    def test_whitespace_handling(self):
        """Whitespace around args should be trimmed."""
        result = self.node._parse_coalesce_args("  nodeA.field  ,   nodeB.field  ")
        assert result == ["nodeA.field", "nodeB.field"]

    def test_project_variable(self):
        """Project variables should be preserved."""
        result = self.node._parse_coalesce_args("nodeA.field, project.uuid-123")
        assert result == ["nodeA.field", "project.uuid-123"]

    def test_meta_field(self):
        """Meta fields should be preserved."""
        result = self.node._parse_coalesce_args("nodeA.field, _meta.execution_id")
        assert result == ["nodeA.field", "_meta.execution_id"]


class MockExecutionContext:
    """Mock context for testing expression resolution."""

    def __init__(self, node_results=None, project_variables=None):
        self.workflow_id = "wf_123"
        self.execution_id = "exec_456"
        self.node_id = "node_789"
        self.project_id = "proj_abc"
        self.user_id = "user_xyz"
        self.node_results = node_results or {}
        self._project_variables = project_variables or {}

    async def resolve_project_variable_by_id(self, variable_id: str):
        if variable_id in self._project_variables:
            return self._project_variables[variable_id]
        raise ValueError(f"Variable {variable_id} not found")

    async def resolve_credential(self, credential_id: str, credential_type: str):
        return {}

    async def resolve_project_variable(self, key: str):
        return None

    def register_secret(self, value: str):
        pass

    def get_secrets(self):
        return set()


class TestResolveCoalesce:
    """Tests for coalesce expression resolution."""

    def setup_method(self):
        """Create a concrete node instance for testing."""
        from pydantic import BaseModel

        from flowire_sdk import BaseNodeOutput, NodeMetadata

        class TestInput(BaseModel):
            value: str = ""

        class TestOutput(BaseNodeOutput):
            result: str = ""

        class TestNode(BaseNode):
            input_schema = TestInput
            output_schema = TestOutput
            metadata = NodeMetadata(name="Test", description="Test node")

            async def execute_logic(self, validated_inputs, context):
                return TestOutput(result="test")

        self.node = TestNode()

    @pytest.mark.asyncio
    async def test_first_value_wins(self):
        """First non-null value should be returned."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": "first_value"},
                "nodeB": {"field": "second_value"},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, nodeB.field", context
        )
        assert result == "first_value"

    @pytest.mark.asyncio
    async def test_fallback_when_first_null(self):
        """Should fall back to second value when first is null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": None},
                "nodeB": {"field": "fallback_value"},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, nodeB.field", context
        )
        assert result == "fallback_value"

    @pytest.mark.asyncio
    async def test_fallback_when_first_missing(self):
        """Should fall back when first node doesn't exist."""
        context = MockExecutionContext(
            node_results={
                "nodeB": {"field": "fallback_value"},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, nodeB.field", context
        )
        assert result == "fallback_value"

    @pytest.mark.asyncio
    async def test_literal_string_fallback(self):
        """Should return literal string when node values are null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": None},
            }
        )
        result = await self.node._resolve_coalesce(
            'nodeA.field, "default"', context
        )
        assert result == "default"

    @pytest.mark.asyncio
    async def test_literal_number_fallback(self):
        """Should return literal number when node values are null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"count": None},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.count, 42", context
        )
        assert result == 42

    @pytest.mark.asyncio
    async def test_literal_boolean_fallback(self):
        """Should return literal boolean when node values are null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"flag": None},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.flag, false", context
        )
        assert result is False

    @pytest.mark.asyncio
    async def test_all_null_returns_null(self):
        """Should return None when all sources are null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": None},
                "nodeB": {"field": None},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, nodeB.field", context
        )
        assert result is None

    @pytest.mark.asyncio
    async def test_empty_string_is_valid(self):
        """Empty string should be returned, not treated as null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": ""},
                "nodeB": {"field": "fallback"},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, nodeB.field", context
        )
        assert result == ""

    @pytest.mark.asyncio
    async def test_zero_is_valid(self):
        """Zero should be returned, not treated as null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"count": 0},
                "nodeB": {"count": 42},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.count, nodeB.count", context
        )
        assert result == 0

    @pytest.mark.asyncio
    async def test_false_is_valid(self):
        """False should be returned, not treated as null."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"flag": False},
                "nodeB": {"flag": True},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.flag, nodeB.flag", context
        )
        assert result is False

    @pytest.mark.asyncio
    async def test_nested_path(self):
        """Should handle nested paths correctly."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"data": {"user": {"name": "Alice"}}},
            }
        )
        result = await self.node._resolve_coalesce(
            "nodeA.data.user.name, \"Unknown\"", context
        )
        assert result == "Alice"

    @pytest.mark.asyncio
    async def test_project_variable(self):
        """Should resolve project variables."""
        context = MockExecutionContext(
            node_results={"nodeA": {"field": None}},
            project_variables={"uuid-123": "project_value"},
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, project.uuid-123", context
        )
        assert result == "project_value"

    @pytest.mark.asyncio
    async def test_meta_field(self):
        """Should resolve _meta fields."""
        context = MockExecutionContext(
            node_results={"nodeA": {"field": None}},
        )
        result = await self.node._resolve_coalesce(
            "nodeA.field, _meta.execution_id", context
        )
        assert result == "exec_456"


class TestCoalesceInParseValue:
    """Tests for coalesce in the full expression parsing pipeline."""

    def setup_method(self):
        """Create a concrete node instance for testing."""
        from pydantic import BaseModel

        from flowire_sdk import BaseNodeOutput, NodeMetadata

        class TestInput(BaseModel):
            value: str = ""

        class TestOutput(BaseNodeOutput):
            result: str = ""

        class TestNode(BaseNode):
            input_schema = TestInput
            output_schema = TestOutput
            metadata = NodeMetadata(name="Test", description="Test node")

            async def execute_logic(self, validated_inputs, context):
                return TestOutput(result="test")

        self.node = TestNode()

    @pytest.mark.asyncio
    async def test_pure_coalesce_expression(self):
        """Coalesce as entire string value."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"field": "value_a"},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(nodeA.field, nodeB.field)}}", context
        )
        assert result == "value_a"

    @pytest.mark.asyncio
    async def test_coalesce_in_template_string(self):
        """Coalesce embedded in a template string."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"name": "Alice"},
            }
        )
        result = await self.node._parse_value(
            "Hello, {{coalesce(nodeA.name, \"Guest\")}}!", context
        )
        assert result == "Hello, Alice!"

    @pytest.mark.asyncio
    async def test_coalesce_with_fallback_in_template(self):
        """Coalesce fallback in template string."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"name": None},
            }
        )
        result = await self.node._parse_value(
            "Hello, {{coalesce(nodeA.name, \"Guest\")}}!", context
        )
        assert result == "Hello, Guest!"

    @pytest.mark.asyncio
    async def test_coalesce_in_dict(self):
        """Coalesce in nested dict structure."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"value": "from_a"},
            }
        )
        result = await self.node._parse_value(
            {"key": "{{coalesce(nodeA.value, nodeB.value)}}"}, context
        )
        assert result == {"key": "from_a"}

    @pytest.mark.asyncio
    async def test_coalesce_in_list(self):
        """Coalesce in list structure."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"item": "first"},
            }
        )
        result = await self.node._parse_value(
            ["{{coalesce(nodeA.item, \"default\")}}"], context
        )
        assert result == ["first"]

    @pytest.mark.asyncio
    async def test_multiple_coalesce_in_string(self):
        """Multiple coalesce expressions in one string."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"first": "John", "last": None},
                "nodeB": {"last": "Doe"},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(nodeA.first, \"Unknown\")}} {{coalesce(nodeA.last, nodeB.last, \"Unknown\")}}",
            context
        )
        assert result == "John Doe"


class TestCoalesceValidation:
    """Tests for coalesce validation and edge cases."""

    def setup_method(self):
        """Create a concrete node instance for testing."""
        from pydantic import BaseModel

        from flowire_sdk import BaseNodeOutput, NodeMetadata

        class TestInput(BaseModel):
            value: str = ""

        class TestOutput(BaseNodeOutput):
            result: str = ""

        class TestNode(BaseNode):
            input_schema = TestInput
            output_schema = TestOutput
            metadata = NodeMetadata(name="Test", description="Test node")

            async def execute_logic(self, validated_inputs, context):
                return TestOutput(result="test")

        self.node = TestNode()

    def test_minimum_args_validation(self):
        """Coalesce with less than 2 args should raise error."""
        with pytest.raises(ValueError, match="at least 2 arguments"):
            self.node._parse_coalesce_args_validated("nodeA.field")

    def test_empty_args_validation(self):
        """Empty coalesce should raise error."""
        with pytest.raises(ValueError, match="at least 2 arguments"):
            self.node._parse_coalesce_args_validated("")

    @pytest.mark.asyncio
    async def test_coalesce_with_complex_nested_data(self):
        """Coalesce should handle complex nested structures."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {
                    "data": {
                        "users": [
                            {"name": "Alice", "age": 30},
                            {"name": "Bob", "age": 25},
                        ]
                    }
                },
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(nodeA.data.users.0.name, \"Unknown\")}}", context
        )
        assert result == "Alice"

    @pytest.mark.asyncio
    async def test_coalesce_returns_object(self):
        """Coalesce can return entire objects."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"config": {"setting": "value"}},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(nodeA.config, nodeB.config)}}", context
        )
        assert result == {"setting": "value"}

    @pytest.mark.asyncio
    async def test_coalesce_returns_array(self):
        """Coalesce can return arrays."""
        context = MockExecutionContext(
            node_results={
                "nodeA": {"items": [1, 2, 3]},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(nodeA.items, nodeB.items)}}", context
        )
        assert result == [1, 2, 3]

    @pytest.mark.asyncio
    async def test_coalesce_with_uuid_style_node_ids(self):
        """Coalesce should work with UUID-style node IDs."""
        # Simulates real workflow node IDs
        context = MockExecutionContext(
            node_results={
                "node-3d418147-58f5-4b1a-b04a-dc316cef1dc2": {"value": "from_first"},
                "node-282efff4-0e66-4abc-b138-c78ec7904615": {"value": "from_second"},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(node-3d418147-58f5-4b1a-b04a-dc316cef1dc2.value, node-282efff4-0e66-4abc-b138-c78ec7904615.value)}}",
            context,
        )
        assert result == "from_first"

    @pytest.mark.asyncio
    async def test_coalesce_uuid_first_node_missing(self):
        """Coalesce should fallback when first UUID node is not in results."""
        # Simulates trigger path where first node never executed
        context = MockExecutionContext(
            node_results={
                # First node not present (different trigger path)
                "node-282efff4-0e66-4abc-b138-c78ec7904615": {"value": "fallback_value"},
            }
        )
        result = await self.node._parse_value(
            "{{coalesce(node-3d418147-58f5-4b1a-b04a-dc316cef1dc2.value, node-282efff4-0e66-4abc-b138-c78ec7904615.value)}}",
            context,
        )
        assert result == "fallback_value"

    @pytest.mark.asyncio
    async def test_coalesce_both_nodes_missing_with_literal(self):
        """Coalesce should return literal when all nodes missing."""
        context = MockExecutionContext(
            node_results={}  # Neither node executed
        )
        result = await self.node._parse_value(
            '{{coalesce(node-abc.value, node-xyz.value, "default")}}',
            context,
        )
        assert result == "default"
