# tests/hyh/test_complexity.py
"""
Empirical Big-O complexity validation tests.

Uses the big_O library to measure actual runtime scaling and verify
that critical functions meet their documented complexity guarantees.

These tests are slower than unit tests (~10s each) but provide
empirical evidence that algorithms scale as expected.
"""

from datetime import UTC, datetime

import big_o
import pytest

from hyh.state import Task, TaskStatus, WorkflowState, detect_cycle


class TestDetectCycleComplexity:
    """Verify detect_cycle is O(V + E) not O(V²)."""

    @pytest.mark.slow
    def test_detect_cycle_linear_in_nodes(self) -> None:
        """detect_cycle should scale linearly with node count (no edges)."""

        def create_graph(n: int) -> dict[str, list[str]]:
            return {f"node-{i}": [] for i in range(int(n))}

        def measure_func(n: int) -> None:
            graph = create_graph(n)
            detect_cycle(graph)

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=10000,
            n_measures=10,
            n_repeats=3,
        )

        # Should be linear or better, not quadratic
        assert isinstance(best, (big_o.complexities.Constant, big_o.complexities.Linear)), (
            f"Expected O(1) or O(n), got {best}"
        )

    @pytest.mark.slow
    def test_detect_cycle_linear_chain(self) -> None:
        """detect_cycle on linear chain (V nodes, V-1 edges) should be O(V)."""

        def create_chain(n: int) -> dict[str, list[str]]:
            n = int(n)
            graph: dict[str, list[str]] = {}
            for i in range(n):
                if i == 0:
                    graph[f"node-{i}"] = []
                else:
                    graph[f"node-{i}"] = [f"node-{i - 1}"]
            return graph

        def measure_func(n: int) -> None:
            graph = create_chain(n)
            detect_cycle(graph)

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=5000,
            n_measures=10,
            n_repeats=3,
        )

        # Linear chain should be O(V) = O(n)
        assert isinstance(best, (big_o.complexities.Constant, big_o.complexities.Linear)), (
            f"Expected O(n) or better, got {best}"
        )


class TestWorkflowStateComplexity:
    """Verify WorkflowState operations meet complexity guarantees."""

    @pytest.mark.slow
    def test_rebuild_indexes_linearithmic(self) -> None:
        """rebuild_indexes should be O(n log n) due to sort."""

        def create_state(n: int) -> WorkflowState:
            n = int(n)
            tasks = {}
            for i in range(n):
                tasks[f"task-{i}"] = Task(
                    id=f"task-{i}",
                    description=f"Task {i}",
                    status=TaskStatus.PENDING,
                    dependencies=[],
                )
            return WorkflowState(tasks=tasks)

        def measure_func(n: int) -> None:
            state = create_state(n)
            state.rebuild_indexes()

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=10000,
            n_measures=10,
            n_repeats=3,
        )

        # Should be linearithmic or better (linear, constant)
        acceptable = (
            big_o.complexities.Constant,
            big_o.complexities.Linear,
            big_o.complexities.Linearithmic,
        )
        assert isinstance(best, acceptable), f"Expected O(n log n) or better, got {best}"

    @pytest.mark.slow
    def test_get_claimable_task_with_satisfied_deps(self) -> None:
        """get_claimable_task should be O(1) when first task is claimable."""

        def measure_func(n: int) -> None:
            n = int(n)
            tasks = {}
            # First task has no deps - immediately claimable
            tasks["claimable"] = Task(
                id="claimable",
                description="Claimable task",
                status=TaskStatus.PENDING,
                dependencies=[],
            )
            # Rest have deps on first task (will be blocked)
            for i in range(n):
                tasks[f"blocked-{i}"] = Task(
                    id=f"blocked-{i}",
                    description=f"Blocked task {i}",
                    status=TaskStatus.PENDING,
                    dependencies=["claimable"],
                )
            state = WorkflowState(tasks=tasks)
            state.get_claimable_task()

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=5000,
            n_measures=10,
            n_repeats=3,
        )

        # Should be linearithmic or better (includes state creation overhead)
        acceptable = (
            big_o.complexities.Constant,
            big_o.complexities.Logarithmic,
            big_o.complexities.Linear,
            big_o.complexities.Linearithmic,
        )
        assert isinstance(best, acceptable), f"Expected O(n log n) or better, got {best}"

    @pytest.mark.slow
    def test_worker_index_lookup_constant(self) -> None:
        """Worker index lookup should be O(1) regardless of task count."""

        def measure_func(n: int) -> None:
            n = int(n)
            tasks = {}
            # One running task claimed by our worker
            tasks["my-task"] = Task(
                id="my-task",
                description="My task",
                status=TaskStatus.RUNNING,
                dependencies=[],
                claimed_by="worker-1",
                started_at=datetime.now(UTC),
            )
            # Many other pending tasks
            for i in range(n):
                tasks[f"other-{i}"] = Task(
                    id=f"other-{i}",
                    description=f"Other task {i}",
                    status=TaskStatus.PENDING,
                    dependencies=[],
                )
            state = WorkflowState(tasks=tasks)
            # This should be O(1) via _worker_index
            state.get_task_for_worker("worker-1")

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=10000,
            n_measures=10,
            n_repeats=3,
        )

        # Includes state creation overhead, so expect linearithmic
        # Polynomial with exponent <= 1.5 is acceptable (essentially linear)
        acceptable = (
            big_o.complexities.Constant,
            big_o.complexities.Linear,
            big_o.complexities.Linearithmic,
        )
        if isinstance(best, big_o.complexities.Polynomial):
            # Check that the exponent is <= 1.5 (essentially linear or sublinear)
            assert best.coeff <= 1.5, f"Expected O(n) or better, got O(n^{best.coeff:.2f})"
        else:
            assert isinstance(best, acceptable), f"Expected O(n log n) or better, got {best}"


class TestValidateDagComplexity:
    """Verify validate_dag is O(V + E)."""

    @pytest.mark.slow
    def test_validate_dag_linear_sparse(self) -> None:
        """validate_dag on sparse graph should be O(V)."""

        def create_sparse_dag(n: int) -> WorkflowState:
            n = int(n)
            tasks = {}
            for i in range(n):
                # Each task depends only on previous (sparse: E = V - 1)
                deps = [f"task-{i - 1}"] if i > 0 else []
                tasks[f"task-{i}"] = Task(
                    id=f"task-{i}",
                    description=f"Task {i}",
                    status=TaskStatus.PENDING,
                    dependencies=deps,
                )
            return WorkflowState(tasks=tasks)

        def measure_func(n: int) -> None:
            state = create_sparse_dag(n)
            state.validate_dag()

        best, _ = big_o.big_o(
            measure_func,
            big_o.datagen.n_,
            min_n=100,
            max_n=5000,
            n_measures=10,
            n_repeats=3,
        )

        acceptable = (
            big_o.complexities.Constant,
            big_o.complexities.Linear,
            big_o.complexities.Linearithmic,
        )
        assert isinstance(best, acceptable), f"Expected O(V + E) ≈ O(n), got {best}"


# Mark all tests in this module as slow
pytestmark = pytest.mark.slow
