"""
Tests for Phase 36: Multi-Step Workflow Engine.

Covers: creation, DAG validation, execution, step completion/failure,
progress tracking, parallel groups, templates, listing, history, and
edge cases.
"""

import json
from pathlib import Path

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all workflow engine storage to a temp directory."""
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    cursors = tmp_path / "cursors"
    workflows = tmp_path / "workflows"
    templates = workflows / "templates"

    for d in (channels, sessions, cursors, workflows, templates):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)
    monkeypatch.setattr(srv, "CURSORS_DIR", cursors)

    import brynq.workflow_engine as we
    monkeypatch.setattr(we, "WORKFLOWS_DIR", workflows)
    monkeypatch.setattr(we, "TEMPLATES_DIR", templates)
    monkeypatch.setattr(we, "HISTORY_FILE", workflows / "history.jsonl")


# ── Helpers ────────────────────────────────────────────────────────────────


def _linear_steps():
    """A -> B -> C linear chain."""
    return [
        {"id": "A", "name": "Step A", "depends_on": []},
        {"id": "B", "name": "Step B", "depends_on": ["A"]},
        {"id": "C", "name": "Step C", "depends_on": ["B"]},
    ]


def _parallel_steps():
    """A and B run in parallel, then C depends on both."""
    return [
        {"id": "A", "name": "Step A", "depends_on": []},
        {"id": "B", "name": "Step B", "depends_on": []},
        {"id": "C", "name": "Step C", "depends_on": ["A", "B"]},
    ]


def _diamond_steps():
    """Diamond: start -> left + right -> end."""
    return [
        {"id": "start", "name": "Start", "depends_on": []},
        {"id": "left", "name": "Left", "depends_on": ["start"]},
        {"id": "right", "name": "Right", "depends_on": ["start"]},
        {"id": "end", "name": "End", "depends_on": ["left", "right"]},
    ]


# ── Test: Workflow Creation ────────────────────────────────────────────────


class TestWorkflowCreation:
    def test_create_linear_workflow(self):
        from brynq.workflow_engine import create_workflow
        wf = create_workflow("Linear", "A then B then C", _linear_steps())
        assert "workflow_id" in wf
        assert wf["name"] == "Linear"
        assert wf["state"] == "draft"
        assert len(wf["steps"]) == 3

    def test_create_parallel_workflow(self):
        from brynq.workflow_engine import create_workflow
        wf = create_workflow("Parallel", "A+B then C", _parallel_steps())
        assert len(wf["steps"]) == 3
        # A and B have no deps
        a_step = next(s for s in wf["steps"] if s["id"] == "A")
        assert a_step["depends_on"] == []

    def test_create_empty_workflow(self):
        from brynq.workflow_engine import create_workflow
        wf = create_workflow("Empty", "No steps")
        assert wf["state"] == "draft"
        assert wf["steps"] == []

    def test_create_no_name_rejected(self):
        from brynq.workflow_engine import create_workflow
        result = create_workflow("")
        assert "error" in result

    def test_duplicate_step_id_rejected(self):
        from brynq.workflow_engine import create_workflow
        steps = [
            {"id": "dup", "name": "First"},
            {"id": "dup", "name": "Second"},
        ]
        result = create_workflow("Bad", steps=steps)
        assert "error" in result
        assert "Duplicate" in result["error"]

    def test_step_missing_id_rejected(self):
        from brynq.workflow_engine import create_workflow
        result = create_workflow("Bad", steps=[{"name": "No ID"}])
        assert "error" in result

    def test_unknown_dependency_rejected(self):
        from brynq.workflow_engine import create_workflow
        steps = [{"id": "A", "depends_on": ["nonexistent"]}]
        result = create_workflow("Bad", steps=steps)
        assert "error" in result
        assert "unknown" in result["error"]

    def test_default_timeout(self):
        from brynq.workflow_engine import create_workflow
        wf = create_workflow("Timeout", steps=[{"id": "s1"}])
        assert wf["steps"][0]["timeout_minutes"] == 30

    def test_custom_timeout(self):
        from brynq.workflow_engine import create_workflow
        wf = create_workflow("Custom", steps=[{"id": "s1", "timeout_minutes": 60}])
        assert wf["steps"][0]["timeout_minutes"] == 60

    def test_step_config_preserved(self):
        from brynq.workflow_engine import create_workflow
        steps = [{"id": "s1", "config": {"model": "gpt-4", "retries": 3}}]
        wf = create_workflow("Config", steps=steps)
        assert wf["steps"][0]["config"]["model"] == "gpt-4"
        assert wf["steps"][0]["config"]["retries"] == 3

    def test_required_skills_preserved(self):
        from brynq.workflow_engine import create_workflow
        steps = [{"id": "s1", "required_skills": ["python", "testing"]}]
        wf = create_workflow("Skills", steps=steps)
        assert wf["steps"][0]["required_skills"] == ["python", "testing"]


# ── Test: DAG Validation ──────────────────────────────────────────────────


class TestDAGValidation:
    def test_cycle_detection_self_loop(self):
        from brynq.workflow_engine import create_workflow
        steps = [{"id": "A", "depends_on": ["A"]}]
        result = create_workflow("Cyclic", steps=steps)
        assert "error" in result
        assert "Cycle" in result["error"] or "cycle" in result["error"].lower()

    def test_cycle_detection_two_node(self):
        from brynq.workflow_engine import create_workflow
        steps = [
            {"id": "A", "depends_on": ["B"]},
            {"id": "B", "depends_on": ["A"]},
        ]
        result = create_workflow("Cyclic", steps=steps)
        assert "error" in result

    def test_cycle_detection_three_node(self):
        from brynq.workflow_engine import create_workflow
        steps = [
            {"id": "A", "depends_on": ["C"]},
            {"id": "B", "depends_on": ["A"]},
            {"id": "C", "depends_on": ["B"]},
        ]
        result = create_workflow("Cyclic", steps=steps)
        assert "error" in result

    def test_valid_dag_passes(self):
        from brynq.workflow_engine import _validate_dag
        steps = [
            {"id": "A", "depends_on": []},
            {"id": "B", "depends_on": ["A"]},
            {"id": "C", "depends_on": ["A", "B"]},
        ]
        assert _validate_dag(steps) is None


# ── Test: Workflow Execution ───────────────────────────────────────────────


class TestWorkflowExecution:
    def test_start_workflow_marks_first_steps_pending(self):
        from brynq.workflow_engine import create_workflow, start_workflow
        wf = create_workflow("Start", steps=_linear_steps())
        started = start_workflow(wf["workflow_id"])
        assert started["state"] == "running"
        step_a = next(s for s in started["steps"] if s["id"] == "A")
        step_b = next(s for s in started["steps"] if s["id"] == "B")
        assert step_a["state"] == "pending"
        assert step_b["state"] == "pending"  # B stays pending, deps not met

    def test_start_parallel_marks_both_pending(self):
        from brynq.workflow_engine import create_workflow, start_workflow
        wf = create_workflow("Par", steps=_parallel_steps())
        started = start_workflow(wf["workflow_id"])
        states = {s["id"]: s["state"] for s in started["steps"]}
        assert states["A"] == "pending"
        assert states["B"] == "pending"
        assert states["C"] == "pending"  # C waits for A + B

    def test_start_nonexistent_workflow(self):
        from brynq.workflow_engine import start_workflow
        result = start_workflow("nonexistent")
        assert "error" in result

    def test_start_already_running(self):
        from brynq.workflow_engine import create_workflow, start_workflow
        wf = create_workflow("Twice", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        result = start_workflow(wf["workflow_id"])
        assert "error" in result

    def test_get_ready_steps_returns_unblocked(self):
        from brynq.workflow_engine import create_workflow, start_workflow, get_ready_steps
        wf = create_workflow("Ready", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        ready = get_ready_steps(wf["workflow_id"])
        ready_ids = [s["id"] for s in ready]
        assert "A" in ready_ids
        assert "B" not in ready_ids
        assert "C" not in ready_ids

    def test_complete_step_unlocks_downstream(self):
        from brynq.workflow_engine import create_workflow, start_workflow, complete_step
        wf = create_workflow("Unlock", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        result = complete_step(wf["workflow_id"], "A", "Done")
        assert "B" in result["newly_ready"]

    def test_complete_all_steps_completes_workflow(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, get_workflow_status,
        )
        wf = create_workflow("Full", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "A")
        complete_step(wf["workflow_id"], "B")
        result = complete_step(wf["workflow_id"], "C")
        assert result["workflow_state"] == "completed"

        status = get_workflow_status(wf["workflow_id"])
        assert status["state"] == "completed"
        assert status["progress"]["percent"] == 100.0

    def test_parallel_unlock_only_when_all_deps_met(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, get_ready_steps,
        )
        wf = create_workflow("Diamond", steps=_diamond_steps())
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "start")

        # Complete only left — end should NOT be ready yet
        complete_step(wf["workflow_id"], "left")
        ready = get_ready_steps(wf["workflow_id"])
        ready_ids = [s["id"] for s in ready]
        assert "end" not in ready_ids

        # Complete right — end should now be ready
        result = complete_step(wf["workflow_id"], "right")
        assert "end" in result["newly_ready"]


# ── Test: Failure & Cascading ──────────────────────────────────────────────


class TestFailure:
    def test_fail_step_cascades_to_dependents(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, fail_step, get_workflow_status,
        )
        wf = create_workflow("Fail", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        result = fail_step(wf["workflow_id"], "A", "Boom")
        assert result["step_state"] == "failed"
        assert result["workflow_state"] == "failed"
        assert "B" in result["skipped_steps"]
        assert "C" in result["skipped_steps"]

    def test_fail_step_no_cascade(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, fail_step,
        )
        wf = create_workflow("NoCascade", steps=_diamond_steps())
        start_workflow(wf["workflow_id"])
        complete_result = fail_step(wf["workflow_id"], "start", "Err", cascade=False)
        assert complete_result["step_state"] == "failed"
        # Without cascade, other steps are NOT auto-skipped
        assert complete_result["skipped_steps"] == []

    def test_complete_step_with_success_false_delegates_to_fail(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step,
        )
        wf = create_workflow("FailViaComplete", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        result = complete_step(wf["workflow_id"], "A", result="Error occurred", success=False)
        assert result["step_state"] == "failed"
        assert result["workflow_state"] == "failed"

    def test_fail_already_completed_step_rejected(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, fail_step,
        )
        wf = create_workflow("Double", steps=[{"id": "s1"}])
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "s1")
        result = fail_step(wf["workflow_id"], "s1", "Too late")
        assert "error" in result


# ── Test: Cancel ───────────────────────────────────────────────────────────


class TestCancel:
    def test_cancel_running_workflow(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, cancel_workflow,
        )
        wf = create_workflow("Cancel", steps=_linear_steps())
        start_workflow(wf["workflow_id"])
        result = cancel_workflow(wf["workflow_id"])
        assert result["state"] == "cancelled"

    def test_cancel_draft_workflow(self):
        from brynq.workflow_engine import create_workflow, cancel_workflow
        wf = create_workflow("Cancel Draft", steps=_linear_steps())
        result = cancel_workflow(wf["workflow_id"])
        assert result["state"] == "cancelled"

    def test_cancel_completed_workflow_rejected(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, cancel_workflow,
        )
        wf = create_workflow("Done", steps=[{"id": "s1"}])
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "s1")
        result = cancel_workflow(wf["workflow_id"])
        assert "error" in result


# ── Test: Progress ─────────────────────────────────────────────────────────


class TestProgress:
    def test_progress_calculation(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, get_workflow_status,
        )
        wf = create_workflow("Progress", steps=_linear_steps())
        start_workflow(wf["workflow_id"])

        # 0 of 3 done
        status = get_workflow_status(wf["workflow_id"])
        assert status["progress"]["percent"] == 0.0

        # 1 of 3 done
        complete_step(wf["workflow_id"], "A")
        status = get_workflow_status(wf["workflow_id"])
        assert status["progress"]["percent"] == pytest.approx(33.3, abs=0.1)
        assert status["progress"]["completed"] == 1
        assert status["progress"]["total"] == 3

    def test_progress_empty_workflow(self):
        from brynq.workflow_engine import create_workflow, get_workflow_status
        wf = create_workflow("Empty")
        status = get_workflow_status(wf["workflow_id"])
        assert status["progress"]["percent"] == 100.0


# ── Test: Execution Order ──────────────────────────────────────────────────


class TestExecutionOrder:
    def test_linear_order(self):
        from brynq.workflow_engine import create_workflow, get_execution_order
        wf = create_workflow("Linear", steps=_linear_steps())
        order = get_execution_order(wf["workflow_id"])
        assert order["flat_order"] == ["A", "B", "C"]
        assert len(order["groups"]) == 3
        assert order["groups"][0] == ["A"]
        assert order["groups"][1] == ["B"]
        assert order["groups"][2] == ["C"]

    def test_parallel_groups(self):
        from brynq.workflow_engine import create_workflow, get_execution_order
        wf = create_workflow("Par", steps=_parallel_steps())
        order = get_execution_order(wf["workflow_id"])
        assert len(order["groups"]) == 2
        # First group: A and B (parallel)
        assert set(order["groups"][0]) == {"A", "B"}
        # Second group: C
        assert order["groups"][1] == ["C"]

    def test_diamond_groups(self):
        from brynq.workflow_engine import create_workflow, get_execution_order
        wf = create_workflow("Diamond", steps=_diamond_steps())
        order = get_execution_order(wf["workflow_id"])
        assert len(order["groups"]) == 3
        assert order["groups"][0] == ["start"]
        assert set(order["groups"][1]) == {"left", "right"}
        assert order["groups"][2] == ["end"]

    def test_empty_workflow_order(self):
        from brynq.workflow_engine import create_workflow, get_execution_order
        wf = create_workflow("Empty")
        order = get_execution_order(wf["workflow_id"])
        assert order["groups"] == []
        assert order["flat_order"] == []

    def test_nonexistent_workflow_order(self):
        from brynq.workflow_engine import get_execution_order
        result = get_execution_order("nope")
        assert "error" in result


# ── Test: Templates ────────────────────────────────────────────────────────


class TestTemplates:
    def test_save_and_list_template(self):
        from brynq.workflow_engine import (
            create_workflow, save_as_template, list_templates,
        )
        wf = create_workflow("Tmpl Source", steps=_linear_steps())
        tmpl = save_as_template(wf["workflow_id"], "my-pipeline")
        assert tmpl["template_name"] == "my-pipeline"
        assert tmpl["source_workflow_id"] == wf["workflow_id"]

        templates = list_templates()
        assert len(templates) == 1
        assert templates[0]["template_name"] == "my-pipeline"

    def test_create_from_template(self):
        from brynq.workflow_engine import (
            create_workflow, save_as_template, create_from_template,
        )
        wf = create_workflow("Source", steps=_linear_steps())
        save_as_template(wf["workflow_id"], "reusable")
        new_wf = create_from_template("reusable", overrides={"name": "New Pipeline"})
        assert new_wf["name"] == "New Pipeline"
        assert len(new_wf["steps"]) == 3
        assert new_wf["workflow_id"] != wf["workflow_id"]

    def test_create_from_template_with_config_overrides(self):
        from brynq.workflow_engine import (
            create_workflow, save_as_template, create_from_template,
        )
        steps = [{"id": "s1", "config": {"retries": 1}}]
        wf = create_workflow("Cfg", steps=steps)
        save_as_template(wf["workflow_id"], "cfg-tmpl")
        new_wf = create_from_template("cfg-tmpl", overrides={
            "step_configs": {"s1": {"retries": 5, "verbose": True}},
        })
        s1 = new_wf["steps"][0]
        assert s1["config"]["retries"] == 5
        assert s1["config"]["verbose"] is True

    def test_create_from_nonexistent_template(self):
        from brynq.workflow_engine import create_from_template
        result = create_from_template("nope")
        assert "error" in result

    def test_save_template_no_name_rejected(self):
        from brynq.workflow_engine import create_workflow, save_as_template
        wf = create_workflow("Source", steps=[{"id": "s1"}])
        result = save_as_template(wf["workflow_id"], "")
        assert "error" in result

    def test_template_strips_runtime_state(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, save_as_template,
        )
        wf = create_workflow("Stateful", steps=[{"id": "s1"}])
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "s1", "result data")
        tmpl = save_as_template(wf["workflow_id"], "clean")

        # Template steps should NOT have runtime fields
        s = tmpl["steps"][0]
        assert "state" not in s
        assert "result" not in s
        assert "completed_at" not in s


# ── Test: Listing & History ────────────────────────────────────────────────


class TestListingAndHistory:
    def test_list_workflows(self):
        from brynq.workflow_engine import create_workflow, list_workflows
        create_workflow("WF1", steps=[{"id": "s1"}])
        create_workflow("WF2", steps=[{"id": "s2"}])
        result = list_workflows()
        assert len(result) == 2

    def test_list_workflows_filter_by_status(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, list_workflows,
        )
        wf1 = create_workflow("Draft", steps=[{"id": "s1"}])
        wf2 = create_workflow("Running", steps=[{"id": "s2"}])
        start_workflow(wf2["workflow_id"])

        drafts = list_workflows(status="draft")
        assert len(drafts) == 1
        assert drafts[0]["state"] == "draft"

        running = list_workflows(status="running")
        assert len(running) == 1
        assert running[0]["state"] == "running"

    def test_list_workflows_limit(self):
        from brynq.workflow_engine import create_workflow, list_workflows
        for i in range(10):
            create_workflow(f"WF-{i}", steps=[{"id": "s1"}])
        result = list_workflows(limit=3)
        assert len(result) == 3

    def test_workflow_history(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, complete_step, get_workflow_history,
        )
        wf = create_workflow("Hist", steps=[{"id": "s1"}])
        start_workflow(wf["workflow_id"])
        complete_step(wf["workflow_id"], "s1", "OK")

        history = get_workflow_history(wf["workflow_id"])
        assert len(history) >= 3  # created, started, step_completed
        events = [h["event"] for h in history]
        assert "created" in events
        assert "started" in events
        assert "step_completed" in events

    def test_history_isolated_per_workflow(self):
        from brynq.workflow_engine import (
            create_workflow, start_workflow, get_workflow_history,
        )
        wf1 = create_workflow("A", steps=[{"id": "s1"}])
        wf2 = create_workflow("B", steps=[{"id": "s2"}])
        start_workflow(wf1["workflow_id"])

        h1 = get_workflow_history(wf1["workflow_id"])
        h2 = get_workflow_history(wf2["workflow_id"])
        # wf1 has created + started; wf2 has only created
        assert len(h1) == 2
        assert len(h2) == 1

    def test_get_status_nonexistent(self):
        from brynq.workflow_engine import get_workflow_status
        result = get_workflow_status("nope")
        assert "error" in result
