from __future__ import annotations

import pytest

from app.core.common.identifiers import new_governance_uid


def valid_workflow_spec():
    return {
        "schema_version": "1.0",
        "dataflow_uid": new_governance_uid(),
        "name": "daily-orders",
        "nodes": [
            {
                "id": "read_orders",
                "type": "sql.query",
                "data_source_uid": new_governance_uid(),
                "purpose": "read",
                "config": {
                    "statement": "SELECT * FROM orders WHERE day = :day",
                    "parameters": {"day": "${parameters.day}"},
                },
            },
            {
                "id": "notify_done",
                "type": "notify",
                "config": {"channel_uid": "operations"},
            },
        ],
        "edges": [{"from": "read_orders", "to": "notify_done"}],
        "parameters": {"day": {"type": "string", "required": True}},
    }


def valid_schedule_plan():
    return {
        "schema_version": "1.0",
        "timezone": "Asia/Shanghai",
        "triggers": [{"type": "cron", "expression": "0 2 * * *"}],
        "max_concurrency": 2,
        "conflict_policy": "skip",
        "timeout_seconds": 3600,
        "retry": {"max_attempts": 3, "delay_seconds": 60},
        "backfill": {"max_days": 7, "max_runs": 20},
    }


def test_valid_workflow_spec_is_normalized_and_hashed_deterministically():
    from app.core.orchestration.spec import validate_workflow_spec, workflow_spec_hash

    spec = valid_workflow_spec()
    normalized = validate_workflow_spec(spec)
    reordered = {
        "parameters": spec["parameters"],
        "edges": spec["edges"],
        "nodes": spec["nodes"],
        "name": spec["name"],
        "dataflow_uid": spec["dataflow_uid"],
        "schema_version": spec["schema_version"],
    }

    assert normalized["schema_version"] == "1.0"
    assert normalized is not spec
    assert workflow_spec_hash(spec) == workflow_spec_hash(reordered)


@pytest.mark.parametrize(
    ("mutation", "message"),
    [
        (
            lambda spec: spec["nodes"][0].update({"type": "community.arbitrary"}),
            "unsupported node type",
        ),
        (
            lambda spec: spec["edges"].append(
                {"from": "notify_done", "to": "read_orders"}
            ),
            "workflow graph must be acyclic",
        ),
        (
            lambda spec: spec["nodes"][0]["config"].update(
                {"password": "must-not-be-here"}
            ),
            "secret material is not allowed",
        ),
        (
            lambda spec: spec["nodes"][0].update(
                {"purpose": "write", "type": "sql.execute"}
            ),
            "write node requires idempotency",
        ),
    ],
)
def test_workflow_spec_rejects_unsafe_or_non_deterministic_definitions(
    mutation, message
):
    from app.core.orchestration.spec import validate_workflow_spec

    spec = valid_workflow_spec()
    mutation(spec)

    with pytest.raises(ValueError, match=message):
        validate_workflow_spec(spec)


def test_schedule_plan_rejects_unbounded_retry_and_backfill():
    from app.core.orchestration.spec import validate_schedule_plan

    plan = valid_schedule_plan()
    assert validate_schedule_plan(plan)["conflict_policy"] == "skip"

    plan["retry"]["max_attempts"] = -1
    plan["backfill"]["max_days"] = 0
    with pytest.raises(ValueError, match="max_attempts"):
        validate_schedule_plan(plan)


def test_json_schemas_declare_closed_objects_and_registered_node_types():
    from app.core.orchestration.spec import (
        SCHEDULE_PLAN_SCHEMA,
        WORKFLOW_SPEC_SCHEMA,
    )

    assert WORKFLOW_SPEC_SCHEMA["additionalProperties"] is False
    assert SCHEDULE_PLAN_SCHEMA["additionalProperties"] is False
    node_type_schema = WORKFLOW_SPEC_SCHEMA["$defs"]["node"]["properties"]["type"]
    assert {"sql.query", "sql.execute", "python", "http"} <= set(
        node_type_schema["enum"]
    )
