# tests/test_workflow_config.py
"""Tests for workflow YAML config parsing."""
import pytest
from logos_adk.errors import ConfigError
from logos_adk.state_store import PostgreSQLStateStore
from logos_adk.workflow.config import (
    load_graph_config,
    load_graph_from_dict,
    load_pipeline_from_dict,
    parse_expression,
)


class TestExpressionParser:
    def test_simple_comparison(self):
        fn = parse_expression("score > 0.8")
        from logos_adk.workflow.context import PipelineContext
        ctx = PipelineContext(pipeline_id="p1")
        ctx["score"] = 0.9
        assert fn(ctx) is True
        ctx["score"] = 0.5
        assert fn(ctx) is False

    def test_dotted_access(self):
        fn = parse_expression("facts.quality > 0.8")
        from logos_adk.workflow.context import PipelineContext
        ctx = PipelineContext(pipeline_id="p1")
        ctx["facts"] = {"quality": 0.9}
        assert fn(ctx) is True

    def test_equality(self):
        fn = parse_expression("status == 'approved'")
        from logos_adk.workflow.context import PipelineContext
        ctx = PipelineContext(pipeline_id="p1")
        ctx["status"] = "approved"
        assert fn(ctx) is True
        ctx["status"] = "rejected"
        assert fn(ctx) is False

    def test_invalid_expression(self):
        with pytest.raises(ConfigError):
            parse_expression("import os")

    def test_function_call_rejected(self):
        with pytest.raises(ConfigError):
            parse_expression("len(x) > 0")


class TestLoadPipelineFromDict:
    def test_missing_name(self):
        with pytest.raises(ConfigError, match="name"):
            load_pipeline_from_dict({"steps": []})

    def test_missing_steps(self):
        with pytest.raises(ConfigError, match="steps"):
            load_pipeline_from_dict({"name": "test"})

    def test_approval_step(self):
        data = {
            "name": "test",
            "steps": [
                {"approval": {"name": "review", "message": "Check"}},
            ],
        }
        p = load_pipeline_from_dict(data)
        assert p.name == "test"
        assert len(p._steps_config) == 1

    def test_retry_config(self):
        data = {
            "name": "test",
            "retry": {"max_attempts": 5, "backoff": "fixed"},
            "steps": [
                {"approval": {"name": "a"}},
            ],
        }
        p = load_pipeline_from_dict(data)
        assert p._default_retry is not None
        assert p._default_retry.max_attempts == 5

    def test_conditional_step(self):
        data = {
            "name": "test",
            "steps": [
                {
                    "conditional": {
                        "if": "score > 0.5",
                        "then": {"approval": {"name": "yes"}},
                        "else": {"approval": {"name": "no"}},
                        "name": "branch",
                    }
                },
            ],
        }
        p = load_pipeline_from_dict(data)
        assert len(p._steps_config) == 1

    def test_postgresql_state_store(self):
        p = load_pipeline_from_dict(
            {
                "name": "test",
                "state_store": "postgresql",
                "state_store_dsn": "postgresql://state-db/logos",
                "steps": [{"approval": {"name": "review"}}],
            }
        )
        assert isinstance(p._state_store, PostgreSQLStateStore)
        assert p._state_store.dsn == "postgresql://state-db/logos"

    def test_postgresql_state_store_uses_env_dsn(self, monkeypatch):
        monkeypatch.setenv("DATABASE_URL", "postgresql://env-state/logos")
        p = load_pipeline_from_dict(
            {
                "name": "test",
                "state_store": "postgresql",
                "steps": [{"approval": {"name": "review"}}],
            }
        )
        assert isinstance(p._state_store, PostgreSQLStateStore)
        assert p._state_store.dsn == "postgresql://env-state/logos"


class TestLoadGraphFromDict:
    @pytest.mark.asyncio
    async def test_load_graph_from_dict_with_function_routes(self):
        async def draft(ctx):
            ctx["needs_review"] = True
            return "draft"

        async def review(ctx):
            return f"{ctx['_last']}-reviewed"

        graph = load_graph_from_dict(
            {
                "name": "release_graph",
                "nodes": [
                    {"name": "draft", "function": "draft"},
                    {"name": "review", "function": "review"},
                    {"name": "publish", "approval": {"message": "publish now"}},
                ],
                "edges": [
                    {"from": "draft", "to": "review", "when": "needs_review == True", "label": "review"},
                    {"from": "draft", "to": "publish", "label": "default"},
                    {"from": "review", "to": "publish"},
                ],
                "entrypoint": "draft",
                "terminal_nodes": ["publish"],
            },
            functions={"draft": draft, "review": review},
        )

        response = await graph.run("ship it")

        assert response.raw["graph_result"]["routing_table"]["draft"][0]["target"] == "review"
        assert response.raw["graph_result"]["status"] == "paused"

    def test_load_graph_from_dict_rejects_unknown_function_reference(self):
        with pytest.raises(ConfigError, match="unknown function"):
            load_graph_from_dict(
                {
                    "name": "bad_graph",
                    "nodes": [{"name": "draft", "function": "missing"}],
                },
                functions={},
            )

    def test_load_graph_from_dict_rejects_unknown_parallel_substep_function(self):
        with pytest.raises(ConfigError, match="parallel sub-step 'missing-step' references unknown function 'missing'"):
            load_graph_from_dict(
                {
                    "name": "bad_parallel_graph",
                    "nodes": [
                        {
                            "name": "parallel_node",
                            "parallel": {
                                "steps": [
                                    {"name": "missing-step", "function": "missing"},
                                ]
                            },
                        }
                    ],
                },
                functions={},
            )

    @pytest.mark.asyncio
    async def test_load_graph_config_reads_yaml_file(self, tmp_path):
        config_path = tmp_path / "graph.yaml"
        config_path.write_text(
            """
name: yaml_graph
entrypoint: first
terminal_nodes:
  - second
nodes:
  - name: first
    function: first
  - name: second
    function: second
edges:
  - from: first
    to: second
""".strip()
        )

        async def first(ctx):
            return "one"

        async def second(ctx):
            return f"{ctx['_last']}-two"

        graph = load_graph_config(
            str(config_path),
            functions={"first": first, "second": second},
        )
        response = await graph.run("go")

        assert response.content == "one-two"

    def test_load_graph_config_preserves_node_timeout_and_retry_metadata(self, tmp_path):
        config_path = tmp_path / "graph.yaml"
        config_path.write_text(
            """
name: yaml_graph
entrypoint: first
terminal_nodes:
  - first
nodes:
  - name: first
    function: first
    metadata:
      timeout: 12.5
      retry:
        max_attempts: 2
        base_delay: 0.5
edges: []
""".strip()
        )

        async def first(ctx):
            return "one"

        graph = load_graph_config(str(config_path), functions={"first": first})

        assert graph._nodes["first"].metadata["timeout"] == 12.5
        assert graph._nodes["first"].metadata["retry"]["max_attempts"] == 2

    def test_loop_step(self):
        data = {
            "name": "test",
            "steps": [
                {
                    "loop": {
                        "step": {"approval": {"name": "body"}},
                        "until": "done == True",
                        "max_iterations": 3,
                        "name": "repeat",
                    }
                },
            ],
        }
        p = load_pipeline_from_dict(data)
        assert len(p._steps_config) == 1

    def test_parallel_step(self):
        data = {
            "name": "test",
            "steps": [
                {
                    "parallel": {
                        "steps": [
                            {"approval": {"name": "a"}},
                            {"approval": {"name": "b"}},
                        ],
                        "name": "par",
                    }
                },
            ],
        }
        p = load_pipeline_from_dict(data)
        assert len(p._steps_config) == 1
