"""Comprehensive unit tests for workflow engine module."""

import asyncio
import time

import pytest

from sfi.workflowengine.workflowengine import (
    CPUTask,
    IOTask,
    ParallelTask,
    SerialTask,
    TaskResult,
    TaskStatus,
    TaskType,
    WorkflowEngine,
)

# ==================== TaskStatus Tests ====================


class TestTaskStatus:
    """Tests for TaskStatus enum."""

    def test_task_status_values(self):
        """Test TaskStatus has all expected values."""
        assert TaskStatus.PENDING.value == "pending"
        assert TaskStatus.READY.value == "ready"
        assert TaskStatus.RUNNING.value == "running"
        assert TaskStatus.COMPLETED.value == "completed"
        assert TaskStatus.FAILED.value == "failed"


class TestTaskType:
    """Tests for TaskType enum."""

    def test_task_type_values(self):
        """Test TaskType has all expected values."""
        assert TaskType.SERIAL.value == "serial"
        assert TaskType.PARALLEL.value == "parallel"
        assert TaskType.ASYNC.value == "async"
        assert TaskType.CPU.value == "cpu"


class TestTaskResult:
    """Tests for TaskResult dataclass."""

    def test_task_result_initialization(self):
        """Test TaskResult can be initialized with all fields."""
        error = ValueError("Test error")
        result = TaskResult(task_id="test_task", success=True, data="test_data", execution_time=1.5, error=error)

        assert result.task_id == "test_task"
        assert result.success is True
        assert result.data == "test_data"
        assert result.execution_time == 1.5
        assert result.error == error

    def test_task_result_without_error(self):
        """Test TaskResult can be initialized without error field."""
        result = TaskResult(task_id="test_task", success=True, data="test_data", execution_time=1.5)

        assert result.error is None


# ==================== IOTask Tests ====================


class TestIOTask:
    """Tests for IOTask class."""

    def test_io_task_initialization(self):
        """Test IOTask can be initialized."""
        task = IOTask("io_task_1", duration=2.5)

        assert task.task_id == "io_task_1"
        assert task.task_type == TaskType.ASYNC
        assert task.duration == 2.5
        assert task.dependencies == []
        assert task.status == TaskStatus.PENDING

    def test_io_task_with_dependencies(self):
        """Test IOTask can be initialized with dependencies."""
        dependencies = ["task_a", "task_b"]
        task = IOTask("io_task_2", duration=1.0, dependencies=dependencies)

        assert task.dependencies == dependencies

    @pytest.mark.asyncio
    async def test_io_task_execution(self):
        """Test IOTask executes correctly."""
        task = IOTask("io_task_3", duration=0.1)
        context = {}

        result = await task.execute(context)

        assert "io_task_3" in result
        assert "completed" in result

    @pytest.mark.asyncio
    async def test_io_task_with_timeout(self):
        """Test IOTask respects timeout."""
        task = IOTask("io_task_4", duration=0.12, timeout=0.1)
        context = {}

        with pytest.raises(asyncio.exceptions.TimeoutError):
            await task.execute(context)

    @pytest.mark.asyncio
    async def test_io_task_status_updates(self):
        """Test IOTask status can be updated."""
        task = IOTask("io_task_5", duration=0.1)

        assert task.status == TaskStatus.PENDING

        # Task status can be manually updated
        task.update_status(TaskStatus.READY)
        assert task.status == TaskStatus.READY

        task.update_status(TaskStatus.RUNNING)
        assert task.status == TaskStatus.RUNNING

        task.update_status(TaskStatus.COMPLETED)
        assert task.status == TaskStatus.COMPLETED


# ==================== CPUTask Tests ====================


class TestCPUTask:
    """Tests for CPUTask class."""

    def test_cpu_task_initialization(self):
        """Test CPUTask can be initialized."""
        task = CPUTask("cpu_task_1", iterations=100000)

        assert task.task_id == "cpu_task_1"
        assert task.task_type == TaskType.CPU
        assert task.iterations == 100000
        assert task.dependencies == []

    @pytest.mark.asyncio
    async def test_cpu_task_execution(self):
        """Test CPUTask executes correctly."""
        task = CPUTask("cpu_task_2", iterations=1000)
        context = {}

        result = await task.execute(context)

        assert "cpu_task_2" in result
        assert "completed" in result

    @pytest.mark.asyncio
    async def test_cpu_task_result_calculation(self):
        """Test CPUTask performs correct calculation."""
        task = CPUTask("cpu_task_3", iterations=100)
        context = {}

        result = await task.execute(context)

        # Sum of squares from 0 to 99 = 328350
        assert "328350" in result


# ==================== SerialTask Tests ====================


class TestSerialTask:
    """Tests for SerialTask class."""

    def test_serial_task_initialization(self):
        """Test SerialTask can be initialized."""

        def process_func(inputs, state):
            return inputs

        task = SerialTask("serial_task_1", process_func)

        assert task.task_id == "serial_task_1"
        assert task.task_type == TaskType.SERIAL
        assert task.process_func == process_func
        assert task.state == {}

    @pytest.mark.asyncio
    async def test_serial_task_execution(self):
        """Test SerialTask executes correctly."""

        def process_func(inputs, state):
            return {"processed": True}

        task = SerialTask("serial_task_2", process_func, dependencies=["dep1"])
        context = {"dep1": TaskResult("dep1", True, {"data": "test"}, 1.0)}

        result = await task.execute(context)

        assert "serial_task_2" in result
        assert "completed" in result

    @pytest.mark.asyncio
    async def test_serial_task_state_update(self):
        """Test SerialTask updates state after execution."""

        def process_func(inputs, state):
            state["executed"] = True
            return inputs

        task = SerialTask("serial_task_3", process_func)
        context = {}

        await task.execute(context)

        assert task.state == {"last_result": {}, "executed": True}

    @pytest.mark.asyncio
    async def test_serial_task_with_async_func(self):
        """Test SerialTask can execute async process function."""

        async def async_process_func(inputs, state):
            await asyncio.sleep(0.01)
            return {"async_result": True}

        task = SerialTask("serial_task_4", async_process_func)
        context = {}

        result = await task.execute(context)

        assert "serial_task_4" in result
        assert task.state == {"last_result": {"async_result": True}, "executed": True}


# ==================== ParallelTask Tests ====================


class TestParallelTask:
    """Tests for ParallelTask class."""

    def test_parallel_task_initialization(self):
        """Test ParallelTask can be initialized."""
        subtasks = [IOTask("sub1", 0.1), IOTask("sub2", 0.2)]
        task = ParallelTask("parallel_task_1", subtasks)

        assert task.task_id == "parallel_task_1"
        assert task.task_type == TaskType.PARALLEL
        assert len(task.subtasks) == 2
        assert task.max_concurrent == 3

    def test_parallel_task_with_custom_concurrency(self):
        """Test ParallelTask can have custom max_concurrent."""
        subtasks = [IOTask("sub1", 0.1)]
        task = ParallelTask("parallel_task_2", subtasks, max_concurrent=5)

        assert task.max_concurrent == 5

    @pytest.mark.asyncio
    async def test_parallel_task_execution(self):
        """Test ParallelTask executes subtasks in parallel."""
        subtasks = [
            IOTask("sub1", 0.1),
            IOTask("sub2", 0.1),
            IOTask("sub3", 0.1),
        ]
        task = ParallelTask("parallel_task_3", subtasks)
        context = {}

        result = await task.execute(context)

        assert "parallel_task_3" in result
        assert "completed" in result

    @pytest.mark.asyncio
    async def test_parallel_task_with_failures(self):
        """Test ParallelTask handles subtask failures."""

        def failing_func(inputs, state):
            raise ValueError("Test error")

        subtasks = [
            IOTask("sub1", 0.1),
            SerialTask("sub2", failing_func),
        ]
        task = ParallelTask("parallel_task_4", subtasks, max_concurrent=2)
        context = {}

        result = await task.execute(context)

        assert "partially failed" in result.lower()

    @pytest.mark.asyncio
    async def test_parallel_task_concurrency_limit(self):
        """Test ParallelTask respects max_concurrent limit."""
        import time

        subtasks = [IOTask(f"sub{i}", 0.1) for i in range(10)]
        task = ParallelTask("parallel_task_5", subtasks, max_concurrent=3)
        context = {}

        start_time = time.time()
        await task.execute(context)
        execution_time = time.time() - start_time

        # With max_concurrent=3 and 10 tasks each taking 0.1s,
        # should take approximately 0.4s (3+3+3+1 batches)
        assert execution_time < 0.6  # Should be faster than sequential


# ==================== WorkflowEngine Tests ====================


class TestWorkflowEngine:
    """Tests for WorkflowEngine class."""

    def test_workflow_engine_initialization(self):
        """Test WorkflowEngine can be initialized."""
        engine = WorkflowEngine()

        assert engine.max_concurrent == 4
        assert engine.tasks == {}
        assert engine.results == {}
        assert engine.execution_order == []

    def test_workflow_engine_custom_concurrency(self):
        """Test WorkflowEngine can have custom max_concurrent."""
        engine = WorkflowEngine(max_concurrent=8)

        assert engine.max_concurrent == 8

    def test_add_task(self):
        """Test WorkflowEngine can add tasks."""
        engine = WorkflowEngine()
        task = IOTask("task1", 1.0)

        engine.add_task(task)

        assert "task1" in engine.tasks
        assert engine.tasks["task1"] == task

    def test_validate_dependencies_success(self):
        """Test WorkflowEngine validates dependencies successfully."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 1.0))
        engine.add_task(IOTask("B", 1.0))
        engine.add_task(CPUTask("C", 1000, dependencies=["A"]))
        engine.add_task(IOTask("D", 1.0, dependencies=["A", "B"]))

        assert engine.validate_dependencies() is True

    def test_validate_dependencies_unknown_task(self):
        """Test WorkflowEngine raises error for unknown dependency."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 1.0))
        engine.add_task(IOTask("B", 1.0, dependencies=["C"]))

        with pytest.raises(ValueError, match="depends on unknown task"):
            engine.validate_dependencies()

    def test_validate_dependencies_circular(self):
        """Test WorkflowEngine detects circular dependencies."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 1.0, dependencies=["C"]))
        engine.add_task(IOTask("B", 1.0, dependencies=["A"]))
        engine.add_task(IOTask("C", 1.0, dependencies=["B"]))

        with pytest.raises(ValueError, match="Circular dependency"):
            engine.validate_dependencies()

    def test_calculate_execution_order(self):
        """Test WorkflowEngine calculates correct execution order."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 1.0))
        engine.add_task(IOTask("B", 1.0))
        engine.add_task(CPUTask("C", 1000, dependencies=["A"]))
        engine.add_task(IOTask("D", 1.0, dependencies=["A", "B"]))
        engine.add_task(IOTask("E", 1.0, dependencies=["C", "D"]))

        execution_order = engine.calculate_execution_order()

        assert execution_order == [["A", "B"], ["C", "D"], ["E"]]

    def test_calculate_execution_order_empty(self):
        """Test WorkflowEngine returns empty order for no tasks."""
        engine = WorkflowEngine()

        execution_order = engine.calculate_execution_order()

        assert execution_order == []

    @pytest.mark.asyncio
    async def test_execute_workflow_simple(self):
        """Test WorkflowEngine executes simple workflow."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 0.1))
        engine.add_task(IOTask("B", 0.1))
        engine.add_task(CPUTask("C", 1000, dependencies=["A"]))

        results = await engine.execute_workflow()

        assert len(results) == 3
        assert all(results[task_id].success for task_id in ["A", "B", "C"])

    @pytest.mark.asyncio
    async def test_execute_workflow_complex(self):
        """Test WorkflowEngine executes complex workflow with multiple levels."""
        engine = WorkflowEngine(max_concurrent=3)
        engine.add_task(IOTask("A", 0.1))
        engine.add_task(IOTask("B", 0.1))
        engine.add_task(CPUTask("C", 1000, dependencies=["A"]))
        engine.add_task(IOTask("D", 0.1, dependencies=["A", "B"]))
        engine.add_task(IOTask("E", 0.1, dependencies=["C", "D"]))

        results = await engine.execute_workflow()

        assert len(results) == 5
        assert results["A"].success
        assert results["B"].success
        assert results["C"].success
        assert results["D"].success
        assert results["E"].success

    @pytest.mark.asyncio
    async def test_execute_workflow_with_parallel_task(self):
        """Test WorkflowEngine executes workflow with ParallelTask."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("main", 0.1))
        subtasks = [IOTask("sub1", 0.1), IOTask("sub2", 0.1), IOTask("sub3", 0.1)]
        engine.add_task(ParallelTask("parallel", subtasks, dependencies=["main"], max_concurrent=2))
        engine.add_task(IOTask("final", 0.1, dependencies=["parallel"]))

        results = await engine.execute_workflow()

        assert len(results) == 3  # main, parallel, final
        assert all(task.success for task in results.values())

    @pytest.mark.asyncio
    async def test_execute_workflow_with_timeout_failure(self):
        """Test WorkflowEngine handles task timeout failures."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("fast", 0.05))
        engine.add_task(IOTask("slow", 5.0, timeout=0.1))

        results = await engine.execute_workflow()

        assert results["fast"].success
        assert not results["slow"].success
        assert isinstance(results["slow"].error, asyncio.TimeoutError)

    @pytest.mark.asyncio
    async def test_get_execution_summary(self):
        """Test WorkflowEngine generates correct execution summary."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("A", 0.1))
        engine.add_task(IOTask("B", 0.1))

        await engine.execute_workflow()
        summary = engine.get_execution_summary()

        assert summary["total_tasks"] == 2
        assert summary["completed"] == 2
        assert summary["failed"] == 0
        assert summary["pending"] == 0
        assert summary["success_rate"] == 1.0
        assert summary["total_execution_time"] > 0


# ==================== Integration Tests ====================


class TestIntegration:
    """Integration tests for complete workflows."""

    @pytest.mark.asyncio
    async def test_complete_workflow_with_all_task_types(self):
        """Test complete workflow using all task types."""
        engine = WorkflowEngine(max_concurrent=3)

        # Parallel data fetching
        engine.add_task(IOTask("fetch_users", 0.1))
        engine.add_task(IOTask("fetch_products", 0.1))
        engine.add_task(IOTask("fetch_orders", 0.1))

        # Serial processing
        def process_data(inputs, state):
            return {"processed": True}

        engine.add_task(SerialTask("process", process_data, dependencies=["fetch_users", "fetch_products"]))

        # CPU computation
        engine.add_task(CPUTask("calculate", 10000, dependencies=["process"]))

        # Parallel save
        engine.add_task(IOTask("save", 0.1, dependencies=["calculate"]))

        results = await engine.execute_workflow()

        assert len(results) == 6
        success_rate = engine.get_execution_summary()["success_rate"]
        assert success_rate == 1.0

    @pytest.mark.asyncio
    async def test_workflow_with_serial_and_parallel_mix(self):
        """Test workflow mixing serial and parallel tasks."""
        engine = WorkflowEngine()

        # Phase 1: Parallel IO tasks
        engine.add_task(IOTask("io1", 0.05))
        engine.add_task(IOTask("io2", 0.05))

        # Phase 2: Serial task
        def serial_func(inputs, state):
            return {"serial_result": True}

        engine.add_task(SerialTask("serial", serial_func, dependencies=["io1", "io2"]))

        # Phase 3: Nested parallel tasks
        subtasks = [IOTask(f"sub{i}", 0.05) for i in range(3)]
        engine.add_task(ParallelTask("parallel", subtasks, dependencies=["serial"]))

        # Phase 4: Final task
        engine.add_task(CPUTask("final", 1000, dependencies=["parallel"]))

        results = await engine.execute_workflow()

        # Engine results should contain 5 tasks: io1, io2, serial, parallel, final
        # ParallelTask's subtasks (sub0, sub1, sub2) are managed internally and not in engine.results
        assert len(results) == 5
        assert all(task.success for task in results.values())


# ==================== Edge Cases Tests ====================


class TestEdgeCases:
    """Tests for edge cases and boundary conditions."""

    def test_empty_workflow(self):
        """Test WorkflowEngine handles empty workflow."""
        engine = WorkflowEngine()

        execution_order = engine.calculate_execution_order()

        assert execution_order == []

    @pytest.mark.asyncio
    async def test_single_task_workflow(self):
        """Test WorkflowEngine executes single task."""
        engine = WorkflowEngine()
        engine.add_task(IOTask("only_task", 0.1))

        results = await engine.execute_workflow()

        assert len(results) == 1
        assert results["only_task"].success

    def test_task_get_execution_time_not_started(self):
        """Test Task.get_execution_time returns 0 for not started tasks."""
        task = IOTask("task", 1.0)

        execution_time = task.get_execution_time()

        assert execution_time == 0.0

    def test_task_get_execution_time_started_not_completed(self):
        """Test Task.get_execution_time returns 0 for incomplete tasks."""
        task = IOTask("task", 1.0)
        task.start_time = time.time()

        execution_time = task.get_execution_time()

        assert execution_time == 0.0

    def test_task_can_execute_no_dependencies(self):
        """Test Task.can_execute returns True for tasks with no dependencies."""
        task = IOTask("task", 1.0)

        assert task.can_execute(set()) is True

    def test_task_can_execute_satisfied_dependencies(self):
        """Test Task.can_execute returns True when dependencies are satisfied."""
        task = IOTask("task", 1.0, dependencies=["A", "B"])

        assert task.can_execute({"A", "B"}) is True

    def test_task_can_execute_unsatisfied_dependencies(self):
        """Test Task.can_execute returns False when dependencies are not satisfied."""
        task = IOTask("task", 1.0, dependencies=["A", "B", "C"])

        assert task.can_execute({"A", "B"}) is False

    def test_task_get_dependencies_returns_copy(self):
        """Test Task.get_dependencies returns a copy of dependencies."""
        task = IOTask("task", 1.0, dependencies=["A", "B"])

        deps = task.get_dependencies()
        deps.append("C")

        assert task.dependencies == ["A", "B"]
        assert len(deps) == 3

    @pytest.mark.asyncio
    async def test_workflow_with_multiple_dependency_levels(self):
        """Test WorkflowEngine handles deeply nested dependencies."""
        engine = WorkflowEngine()

        # Chain of dependencies: A -> B -> C -> D -> E
        engine.add_task(IOTask("A", 0.05))
        engine.add_task(IOTask("B", 0.05, dependencies=["A"]))
        engine.add_task(IOTask("C", 0.05, dependencies=["B"]))
        engine.add_task(IOTask("D", 0.05, dependencies=["C"]))
        engine.add_task(IOTask("E", 0.05, dependencies=["D"]))

        results = await engine.execute_workflow()

        assert len(results) == 5
        execution_order = engine.execution_order
        assert execution_order == [["A"], ["B"], ["C"], ["D"], ["E"]]


# ==================== Error Handling Tests ====================


class TestErrorHandling:
    """Tests for error handling and graceful degradation."""

    @pytest.mark.asyncio
    async def test_workflow_continues_after_task_failure(self):
        """Test WorkflowEngine continues execution after task failure."""
        engine = WorkflowEngine()

        engine.add_task(IOTask("success1", 0.05))
        engine.add_task(IOTask("failure", 5.0, timeout=0.1))
        engine.add_task(IOTask("success2", 0.05))

        results = await engine.execute_workflow()

        # Even with failure, workflow should complete
        assert results["success1"].success
        assert not results["failure"].success
        assert results["success2"].success

    @pytest.mark.asyncio
    async def test_workflow_with_exception_in_task(self):
        """Test WorkflowEngine handles exceptions in tasks."""

        def failing_func(inputs, state):
            raise RuntimeError("Task failed")

        engine = WorkflowEngine()
        engine.add_task(IOTask("good", 0.05))
        engine.add_task(SerialTask("bad", failing_func))

        results = await engine.execute_workflow()

        assert results["good"].success
        assert not results["bad"].success
        assert isinstance(results["bad"].error, RuntimeError)

    @pytest.mark.asyncio
    async def test_parallel_task_handles_mixed_success_failure(self):
        """Test ParallelTask handles mixture of successful and failed subtasks."""

        def failing_func(inputs, state):
            raise ValueError("Subtask failed")

        subtasks = [
            IOTask("good1", 0.05),
            IOTask("good2", 0.05),
            SerialTask("bad", failing_func),
        ]
        task = ParallelTask("mixed", subtasks, max_concurrent=3)

        result = await task.execute({})

        # Should complete with partial failure message
        assert "partially failed" in result.lower()

    def test_task_status_updates(self):
        """Test Task status updates correctly."""
        task = IOTask("task", 1.0)

        assert task.status == TaskStatus.PENDING

        task.update_status(TaskStatus.READY)
        assert task.status == TaskStatus.READY

        task.update_status(TaskStatus.RUNNING)
        assert task.status == TaskStatus.RUNNING

        task.update_status(TaskStatus.COMPLETED)
        assert task.status == TaskStatus.COMPLETED

        task.update_status(TaskStatus.FAILED)
        assert task.status == TaskStatus.FAILED


# ==================== Performance Tests ====================


class TestPerformance:
    """Performance-related tests."""

    @pytest.mark.asyncio
    async def test_parallel_execution_faster_than_serial(self):
        """Test parallel execution is faster than serial execution."""
        # Serial execution
        engine_serial = WorkflowEngine(max_concurrent=1)
        for i in range(5):
            engine_serial.add_task(IOTask(f"serial_{i}", 0.1))

        start_serial = time.time()
        await engine_serial.execute_workflow()
        serial_time = time.time() - start_serial

        # Parallel execution
        engine_parallel = WorkflowEngine(max_concurrent=5)
        for i in range(5):
            engine_parallel.add_task(IOTask(f"parallel_{i}", 0.1))

        start_parallel = time.time()
        await engine_parallel.execute_workflow()
        parallel_time = time.time() - start_parallel

        # Parallel should be significantly faster
        assert parallel_time < serial_time * 0.8

    @pytest.mark.asyncio
    async def test_concurrent_tasks_respect_semaphore_limit(self):
        """Test concurrent execution respects max_concurrent limit."""
        import time

        engine = WorkflowEngine(max_concurrent=2)
        for i in range(10):
            engine.add_task(IOTask(f"task_{i}", 0.1))

        start_time = time.time()
        await engine.execute_workflow()
        execution_time = time.time() - start_time

        # With max_concurrent=2 and 10 tasks each taking 0.1s,
        # should take approximately 0.5s (2+2+2+2+2 batches)
        # Give some margin for overhead
        assert execution_time < 0.8
