import copy
import re

import pytest

from app.core.common.identifiers import new_governance_uid
from app.core.orchestration.compilers import compile_kestra_flow


def workflow_spec():
    return {
        "schema_version": "1.0",
        "dataflow_uid": new_governance_uid(),
        "name": "Daily Orders / East",
        "description": "Read governed orders and notify operations.",
        "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},
            "dry_run": {"type": "boolean", "required": False},
        },
        "labels": {"domain": "orders"},
    }


def schedule_plan():
    return {
        "schema_version": "1.0",
        "timezone": "Asia/Shanghai",
        "triggers": [
            {"type": "manual"},
            {"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_compiler_is_deterministic_and_returns_stable_engine_identity():
    spec = workflow_spec()
    plan = schedule_plan()

    first = compile_kestra_flow(spec, plan, environment="test", version_no=7)
    reordered = {
        "labels": spec["labels"],
        "parameters": spec["parameters"],
        "edges": spec["edges"],
        "nodes": spec["nodes"],
        "description": spec["description"],
        "name": spec["name"],
        "dataflow_uid": spec["dataflow_uid"],
        "schema_version": spec["schema_version"],
    }
    second = compile_kestra_flow(reordered, copy.deepcopy(plan), "test", 7)

    assert first == second
    assert first.namespace == "dataops.test"
    assert first.flow_id == (
        f"df_{spec['dataflow_uid'].replace('-', '')}_v7"
    )
    assert re.fullmatch(r"[0-9a-f]{64}", first.definition_hash)


def test_compiler_emits_a_disabled_dag_that_calls_only_the_dataops_runner():
    spec = workflow_spec()
    compiled = compile_kestra_flow(
        spec,
        schedule_plan(),
        environment="development",
        version_no=1,
    )
    source = compiled.yaml

    assert "\ndisabled: true\n" in source
    assert source.count("disabled: true") == 2
    assert "io.kestra.plugin.core.flow.Dag" in source
    assert "io.kestra.plugin.core.http.Request" in source
    assert "http://dataops-runner:5600/v1/tasks/execute" in source
    assert 'dependsOn:' in source
    assert '"read_orders"' in source
    assert spec["dataflow_uid"] in source
    assert spec["nodes"][0]["data_source_uid"] in source
    assert "SELECT * FROM orders WHERE day = :day" in source
    assert "contentType: \"application/json\"" in source
    assert 'id: "dataops_task_tokens"' in source
    assert 'type: "JSON"' in source
    assert "inputs.dataops_task_tokens.read_orders" in source
    assert "maxAttempts: 3" in source
    assert "interval: \"PT60S\"" in source
    assert "timeout: \"PT3600S\"" in source
    assert "behavior: \"CANCEL\"" in source
    assert "cron: \"0 2 * * *\"" in source
    assert "timezone: \"Asia/Shanghai\"" in source

    for forbidden in (
        "postgresql://",
        "mysql://",
        "jdbc:",
        "password",
        "connectionString",
        "authorization",
        "source-test-password",
    ):
        assert forbidden not in source.lower()


def test_manual_plan_creates_no_automatic_trigger():
    plan = schedule_plan()
    plan["triggers"] = [{"type": "manual"}]

    source = compile_kestra_flow(
        workflow_spec(), plan, "development", 1
    ).yaml

    assert "\ntriggers:" not in source


@pytest.mark.parametrize(
    ("mutation", "message"),
    [
        (
            lambda plan: plan.update({"conflict_policy": "cancel_previous"}),
            "cancel_previous",
        ),
        (
            lambda plan: plan.update(
                {"triggers": [{"type": "event", "event_type": "orders.ready"}]}
            ),
            "event",
        ),
        (
            lambda plan: plan.update(
                {"triggers": [{"type": "at", "at": "2026-07-20T01:00:00Z"}]}
            ),
            "at",
        ),
    ],
)
def test_compiler_fails_closed_when_kestra_semantics_are_not_equivalent(
    mutation, message
):
    plan = schedule_plan()
    mutation(plan)

    with pytest.raises(ValueError, match=message):
        compile_kestra_flow(workflow_spec(), plan, "test", 1)
