"""
Unit tests for DSL control flow functionality.
Tests if, loop, and for_each control flow structures.
"""

import pytest
import asyncio
from typing import Dict, Any, List

from opentester.core.executors.base import BaseExecutor, ExecutionContext, StepExecutionResult
from opentester.core.executors.cli import CLIExecutor
from opentester.models.dsl import IfConditionType, IfStep, LoopStep, ForEachStep, TestStep, ActionType


class MockExecutor(BaseExecutor):
    """Mock executor for testing control flow."""

    def __init__(self, context: ExecutionContext):
        super().__init__(context)
        self.executed_steps: List[str] = []

    async def execute_step(self, step: Dict[str, Any]) -> StepExecutionResult:
        """Execute a mock step."""
        action = step.get("action", "")
        name = step.get("name", action)
        self.executed_steps.append(name)

        return StepExecutionResult(
            success=True,
            message=f"Executed: {name}",
            output=f"output from {name}",
            data={"exit_code": 0},
        )


@pytest.fixture
def execution_context():
    """Create a test execution context."""
    return ExecutionContext(
        execution_id="test-exec-001",
        project_id="test-project",
        case_id="test-case",
        target_type="cli",
        variables={"test_var": "hello", "counter": "5"},
    )


@pytest.fixture
def mock_executor(execution_context):
    """Create a mock executor."""
    return MockExecutor(execution_context)


class TestIfCondition:
    """Tests for if condition control flow."""

    @pytest.mark.asyncio
    async def test_if_condition_output_contains_true(self, mock_executor, execution_context):
        """Test if condition with output_contains - condition true."""
        # Add a previous step result to context
        execution_context.step_results.append(
            StepExecutionResult(
                success=True,
                message="Previous step",
                output="Hello World",
                data={"exit_code": 0},
            )
        )

        if_step = {
            "action": "if",
            "condition": {
                "type": "output_contains",
                "pattern": "World",
            },
            "then": [
                {"action": "exec", "name": "then_step_1", "command": "echo then"},
                {"action": "exec", "name": "then_step_2", "command": "echo then2"},
            ],
            "else": [
                {"action": "exec", "name": "else_step", "command": "echo else"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert result.data["executed_branch"] == "then"
        assert len(result.data["branch_results"]) == 2
        assert "then_step_1" in mock_executor.executed_steps
        assert "then_step_2" in mock_executor.executed_steps
        assert "else_step" not in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_output_contains_false(self, mock_executor, execution_context):
        """Test if condition with output_contains - condition false."""
        # Add a previous step result to context
        execution_context.step_results.append(
            StepExecutionResult(
                success=True,
                message="Previous step",
                output="Hello World",
                data={"exit_code": 0},
            )
        )

        if_step = {
            "action": "if",
            "condition": {
                "type": "output_contains",
                "pattern": "NotFound",
            },
            "then": [
                {"action": "exec", "name": "then_step", "command": "echo then"},
            ],
            "else": [
                {"action": "exec", "name": "else_step_1", "command": "echo else"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is False
        assert result.data["executed_branch"] == "else"
        assert "then_step" not in mock_executor.executed_steps
        assert "else_step_1" in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_exit_code_true(self, mock_executor, execution_context):
        """Test if condition with exit_code - condition true."""
        execution_context.step_results.append(
            StepExecutionResult(
                success=True,
                message="Previous step",
                output="",
                data={"exit_code": 0},
            )
        )

        if_step = {
            "action": "if",
            "condition": {
                "type": "exit_code",
                "expected_code": 0,
            },
            "then": [
                {"action": "exec", "name": "success_step", "command": "echo success"},
            ],
            "else": [
                {"action": "exec", "name": "failure_step", "command": "echo failure"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "success_step" in mock_executor.executed_steps
        assert "failure_step" not in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_var_equals_true(self, mock_executor, execution_context):
        """Test if condition with var_equals - condition true."""
        if_step = {
            "action": "if",
            "condition": {
                "type": "var_equals",
                "variable": "test_var",
                "expected": "hello",
            },
            "then": [
                {"action": "exec", "name": "match_step", "command": "echo match"},
            ],
            "else": [
                {"action": "exec", "name": "no_match_step", "command": "echo no match"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "match_step" in mock_executor.executed_steps
        assert "no_match_step" not in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_var_not_equals(self, mock_executor, execution_context):
        """Test if condition with var_not_equals."""
        if_step = {
            "action": "if",
            "condition": {
                "type": "var_not_equals",
                "variable": "test_var",
                "expected": "world",
            },
            "then": [
                {"action": "exec", "name": "not_equal_step", "command": "echo not equal"},
            ],
            "else": [
                {"action": "exec", "name": "equal_step", "command": "echo equal"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "not_equal_step" in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_var_contains(self, mock_executor, execution_context):
        """Test if condition with var_contains."""
        if_step = {
            "action": "if",
            "condition": {
                "type": "var_contains",
                "variable": "test_var",
                "pattern": "ell",
            },
            "then": [
                {"action": "exec", "name": "contains_step", "command": "echo contains"},
            ],
            "else": [
                {"action": "exec", "name": "not_contains_step", "command": "echo not contains"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "contains_step" in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_var_empty(self, mock_executor, execution_context):
        """Test if condition with var_empty."""
        execution_context.set_var("empty_var", "")

        if_step = {
            "action": "if",
            "condition": {
                "type": "var_empty",
                "variable": "empty_var",
            },
            "then": [
                {"action": "exec", "name": "empty_step", "command": "echo empty"},
            ],
            "else": [
                {"action": "exec", "name": "not_empty_step", "command": "echo not empty"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "empty_step" in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_var_not_empty(self, mock_executor, execution_context):
        """Test if condition with var_not_empty."""
        if_step = {
            "action": "if",
            "condition": {
                "type": "var_not_empty",
                "variable": "test_var",
            },
            "then": [
                {"action": "exec", "name": "not_empty_step", "command": "echo not empty"},
            ],
            "else": [
                {"action": "exec", "name": "empty_step", "command": "echo empty"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert "not_empty_step" in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_if_condition_no_else(self, mock_executor, execution_context):
        """Test if condition with no else branch."""
        execution_context.step_results.append(
            StepExecutionResult(
                success=True,
                message="Previous step",
                output="test output",
                data={"exit_code": 1},
            )
        )

        if_step = {
            "action": "if",
            "condition": {
                "type": "exit_code",
                "expected_code": 0,
            },
            "then": [
                {"action": "exec", "name": "then_step", "command": "echo then"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(if_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["condition_met"] is False
        assert result.data["executed_branch"] == "else"
        assert len(result.data["branch_results"]) == 0  # No else branch to execute


class TestLoop:
    """Tests for loop control flow."""

    @pytest.mark.asyncio
    async def test_loop_basic(self, mock_executor, execution_context):
        """Test basic loop execution."""
        # Set up a counter that will be checked in condition
        execution_context.set_var("loop_count", "0")

        loop_step = {
            "action": "loop",
            "max_iterations": 3,
            "loop_interval": 0.01,  # Fast for testing
            "condition": {
                "type": "var_not_equals",
                "variable": "loop_count",
                "expected": "3",
            },
            "body": [
                {"action": "exec", "name": "body_step", "command": "echo body"},
            ],
        }

        # Simulate the loop condition changing
        call_count = [0]
        async def mock_step_executor(step):
            call_count[0] += 1
            execution_context.set_var("loop_count", str(call_count[0]))
            return await mock_executor.execute_step(step)

        result = await mock_executor.execute_control_flow_step(loop_step, mock_step_executor)

        assert result.success is True
        assert result.data["iterations"] == 3

    @pytest.mark.asyncio
    async def test_loop_max_iterations_limit(self, mock_executor, execution_context):
        """Test loop respects max_iterations limit."""
        # Condition always true, should hit max_iterations
        loop_step = {
            "action": "loop",
            "max_iterations": 5,
            "loop_interval": 0.01,
            "condition": {
                "type": "var_equals",
                "variable": "test_var",
                "expected": "hello",  # Always true
            },
            "body": [
                {"action": "exec", "name": "body_step", "command": "echo body"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(loop_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["iterations"] == 5  # Hit max_iterations

    @pytest.mark.asyncio
    async def test_loop_zero_iterations(self, mock_executor, execution_context):
        """Test loop with condition initially false."""
        loop_step = {
            "action": "loop",
            "max_iterations": 10,
            "condition": {
                "type": "var_equals",
                "variable": "test_var",
                "expected": "not_hello",  # Always false
            },
            "body": [
                {"action": "exec", "name": "body_step", "command": "echo body"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(loop_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["iterations"] == 0
        assert "body_step" not in mock_executor.executed_steps


class TestForEach:
    """Tests for for_each control flow."""

    @pytest.mark.asyncio
    async def test_for_each_list(self, mock_executor, execution_context):
        """Test for_each with a list of items."""
        for_each_step = {
            "action": "for_each",
            "items": ["item1", "item2", "item3"],
            "var": "current_item",
            "body": [
                {"action": "exec", "name": "process_item", "command": "echo ${{vars.current_item}}"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(for_each_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["item_count"] == 3
        assert result.data["var_name"] == "current_item"
        assert len(result.data["results"]) == 3
        # Check that loop variable was set correctly
        assert execution_context.get_var("current_item") == "item3"  # Last item

    @pytest.mark.asyncio
    async def test_for_each_empty_list(self, mock_executor, execution_context):
        """Test for_each with empty list."""
        for_each_step = {
            "action": "for_each",
            "items": [],
            "var": "item",
            "body": [
                {"action": "exec", "name": "process_item", "command": "echo item"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(for_each_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["item_count"] == 0
        assert "process_item" not in mock_executor.executed_steps

    @pytest.mark.asyncio
    async def test_for_each_variable_reference(self, mock_executor, execution_context):
        """Test for_each with variable reference for items."""
        # Set up a variable with comma-separated items
        execution_context.set_var("my_items", "a,b,c")

        for_each_step = {
            "action": "for_each",
            "items": "${vars.my_items}",
            "var": "current",
            "body": [
                {"action": "exec", "name": "process", "command": "echo process"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(for_each_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["item_count"] == 3

    @pytest.mark.asyncio
    async def test_for_each_single_item(self, mock_executor, execution_context):
        """Test for_each with single item."""
        for_each_step = {
            "action": "for_each",
            "items": ["only_item"],
            "var": "item",
            "body": [
                {"action": "exec", "name": "process_item", "command": "echo item"},
            ],
        }

        result = await mock_executor.execute_control_flow_step(for_each_step, mock_executor.execute_step)

        assert result.success is True
        assert result.data["item_count"] == 1
        assert "process_item" in mock_executor.executed_steps


class TestDSLModels:
    """Tests for DSL model validation."""

    def test_if_step_model(self):
        """Test IfStep model validation."""
        step = IfStep(
            action="if",
            condition={
                "type": "var_equals",
                "variable": "test",
                "expected": "value",
            },
            **{
                "then": [
                    {"action": "exec", "name": "step1", "command": "echo 1"},
                ],
                "else": [
                    {"action": "exec", "name": "step2", "command": "echo 2"},
                ],
            }
        )

        assert step.action == "if"
        assert step.condition.type == "var_equals"
        # Access via model_dump to get alias-resolved values
        data = step.model_dump()
        assert len(data["then_steps"]) == 1
        assert len(data["else_steps"]) == 1

    def test_loop_step_model(self):
        """Test LoopStep model validation."""
        step = LoopStep(
            action="loop",
            max_iterations=5,
            condition={
                "type": "exit_code",
                "expected_code": 0,
            },
            **{
                "body": [
                    {"action": "exec", "name": "body_step", "command": "echo body"},
                ],
            }
        )

        assert step.action == "loop"
        assert step.max_iterations == 5
        data = step.model_dump()
        assert len(data["for_each_body"]) == 1

    def test_for_each_step_model(self):
        """Test ForEachStep model validation."""
        step = ForEachStep(
            action="for_each",
            items=["a", "b", "c"],
            var="item",  # Use alias
            **{
                "body": [
                    {"action": "exec", "name": "process", "command": "echo ${{vars.item}}"},
                ],
            }
        )

        assert step.action == "for_each"
        assert step.var_name == "item"
        data = step.model_dump()
        assert len(data["for_each_body"]) == 1

    def test_if_condition_type_enum(self):
        """Test IfConditionType enum values."""
        assert IfConditionType.OUTPUT_CONTAINS == "output_contains"
        assert IfConditionType.EXIT_CODE == "exit_code"
        assert IfConditionType.VAR_EQUALS == "var_equals"
        assert IfConditionType.VAR_NOT_EQUALS == "var_not_equals"
        assert IfConditionType.VAR_CONTAINS == "var_contains"
        assert IfConditionType.VAR_EMPTY == "var_empty"
        assert IfConditionType.VAR_NOT_EMPTY == "var_not_empty"


class TestCLIExecutorControlFlow:
    """Integration tests for CLIExecutor with control flow."""

    @pytest.mark.asyncio
    async def test_cli_executor_if_step(self):
        """Test CLIExecutor executing an if step."""
        context = ExecutionContext(
            execution_id="test-exec",
            project_id="test-project",
            case_id="test-case",
            target_type="cli",
            variables={},
        )
        executor = CLIExecutor(context)

        # First execute a command to set up output
        exec_step = {"action": "exec", "command": "echo 'Hello World'", "capture": ["stdout"]}
        await executor.execute_step(exec_step)

        # Now test if condition on that output
        if_step = {
            "action": "if",
            "condition": {
                "type": "output_contains",
                "pattern": "World",
            },
            "then": [
                {"action": "exec", "name": "found_step", "command": "echo 'Found it!'", "capture": ["stdout"]},
            ],
            "else": [
                {"action": "exec", "name": "not_found_step", "command": "echo 'Not found'", "capture": ["stdout"]},
            ],
        }

        result = await executor.execute_step(if_step)

        assert result.success is True
        assert result.data["condition_met"] is True
        assert result.data["executed_branch"] == "then"

    @pytest.mark.asyncio
    async def test_cli_executor_for_each(self):
        """Test CLIExecutor executing a for_each step."""
        context = ExecutionContext(
            execution_id="test-exec",
            project_id="test-project",
            case_id="test-case",
            target_type="cli",
            variables={},
        )
        executor = CLIExecutor(context)

        for_each_step = {
            "action": "for_each",
            "items": ["a", "b"],
            "var": "letter",
            "body": [
                {"action": "exec", "name": "echo_letter", "command": "echo 'Letter: ${{vars.letter}}'", "capture": ["stdout"]},
            ],
        }

        result = await executor.execute_step(for_each_step)

        assert result.success is True
        assert result.data["item_count"] == 2


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
