import pytest

from rpa import config, flows


@pytest.fixture(autouse=True)
def tmp_flows_dir(tmp_path, monkeypatch):
    d = tmp_path / "flows"
    d.mkdir()
    monkeypatch.setattr(config, "FLOWS_DIR", d)
    return d


def graph(nodes, edges=None, **extra):
    return {"format": "graph", "nodes": nodes, "edges": edges or [], "settings": {}, **extra}


def test_crud_round_trip():
    assert flows.list_flows() == []

    flows.save_flow("mitt flöde", graph([{"id": "n1", "type": "goto", "url": "https://x.se"}]))
    assert flows.list_flows() == ["mitt flöde"]

    loaded = flows.load_flow("mitt flöde")
    assert loaded["nodes"][0]["url"] == "https://x.se"
    assert loaded["settings"] == {}

    assert flows.delete_flow("mitt flöde") is True
    assert flows.list_flows() == []
    assert flows.delete_flow("mitt flöde") is False


def test_old_steps_flow_is_migrated_on_load(tmp_flows_dir):
    import json

    (tmp_flows_dir / "gammalt.json").write_text(
        json.dumps({"steps": [{"type": "goto", "url": "https://x.se"}], "settings": {}}),
        encoding="utf-8",
    )
    loaded = flows.load_flow("gammalt")
    assert loaded["format"] == "graph"
    assert [n["type"] for n in loaded["nodes"]] == ["goto"]
    assert loaded["nodes"][0]["url"] == "https://x.se"


def test_invalid_name_rejected():
    with pytest.raises(flows.InvalidFlowName):
        flows.flow_path("../secret")
    with pytest.raises(flows.InvalidFlowName):
        flows.flow_path("a" * 200)


def test_load_missing_raises():
    with pytest.raises(flows.FlowNotFound):
        flows.load_flow("nope")


def test_save_lenient_by_default_strict_on_request():
    half = graph([{"id": "n1", "type": "goto"}])  # url saknas
    flows.save_flow("wip", half)  # lenient - ok
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("wip", half, strict=True)


def test_save_rejects_structural_errors_even_lenient():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("bad", graph([{"id": "n1", "type": "nonesuch"}]))


def test_save_rejects_dangling_edge():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("bad", graph(
            [{"id": "n1", "type": "wait", "seconds": 1}],
            [{"id": "e1", "source": "n1", "target": "ghost"}],
        ))


def test_node_positions_round_trip():
    flow = graph([{"id": "abc123", "type": "goto", "url": "https://x.se",
                   "position": {"x": 40, "y": 120}}])
    flows.save_flow("laid out", flow)
    loaded = flows.load_flow("laid out")
    assert loaded["nodes"][0]["id"] == "abc123"
    assert loaded["nodes"][0]["position"] == {"x": 40, "y": 120}


def test_inputs_and_sets_round_trip():
    flow = graph(
        [{"id": "n1", "type": "goto", "url": "https://x.se/{org_id}"}],
        inputs=[{"name": "org_id", "label": "Org", "default": "4"}],
        input_sets={"Test": {"org_id": "9"}},
    )
    flows.save_flow("params", flow)
    loaded = flows.load_flow("params")
    assert loaded["inputs"][0]["name"] == "org_id"
    assert loaded["input_sets"] == {"Test": {"org_id": "9"}}


def test_input_without_name_rejected():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("bad", graph([], inputs=[{"label": "x"}]))


def test_missing_flow_defaults_inputs():
    flows.save_flow("plain", graph([]))
    loaded = flows.load_flow("plain")
    assert loaded["inputs"] == [] and loaded["input_sets"] == {}


def test_layout_omitted_when_empty():
    flows.save_flow("nolayout", graph([], layout={}))
    import json

    raw = json.loads((flows.config.FLOWS_DIR / "nolayout.json").read_text(encoding="utf-8"))
    assert "layout" not in raw


# --- subflöden ---
def _with_subflows(**extra):
    return graph(
        [{"id": "n1", "type": "call_flow", "target": "#hjalp",
          "in": [{"name": "x", "value": "1"}], "out": [{"from": "y", "to": "r"}]}],
        subflows=[{
            "id": "hjalp", "name": "Hjälpare",
            "nodes": [{"id": "s1", "type": "set_var", "var": "y", "text": "{x}"}],
            "edges": [], "start": "s1", "inputs": [{"name": "x"}],
        }],
        **extra,
    )


def test_subflows_round_trip():
    flows.save_flow("m", _with_subflows())
    loaded = flows.load_flow("m")
    assert loaded["subflows"][0]["id"] == "hjalp"
    assert loaded["subflows"][0]["nodes"][0]["type"] == "set_var"
    assert loaded["nodes"][0]["type"] == "call_flow"


def test_subflow_id_slugged_from_name_when_missing():
    g = graph([], subflows=[{"name": "Hämta Bilagor",
                             "nodes": [], "edges": []}])
    flows.save_flow("s", g)
    assert flows.load_flow("s")["subflows"][0]["id"] == "h-mta-bilagor"


def test_subflows_omitted_when_empty():
    import json

    flows.save_flow("nosub", graph([], subflows=[]))
    raw = json.loads((flows.config.FLOWS_DIR / "nosub.json").read_text(encoding="utf-8"))
    assert "subflows" not in raw


def test_call_flow_to_unknown_local_subflow_rejected():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("bad", graph(
            [{"id": "n1", "type": "call_flow", "target": "#saknas"}],
            subflows=[{"id": "finns", "name": "F", "nodes": [], "edges": []}],
        ))


def test_cyclic_local_subflows_rejected():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("cyc", graph(
            [{"id": "n1", "type": "call_flow", "target": "#a"}],
            subflows=[
                {"id": "a", "name": "A", "nodes": [{"id": "x", "type": "call_flow", "target": "#b"}], "edges": []},
                {"id": "b", "name": "B", "nodes": [{"id": "y", "type": "call_flow", "target": "#a"}], "edges": []},
            ],
        ))


def test_broken_subflow_node_rejected():
    with pytest.raises(flows.InvalidFlow):
        flows.save_flow("bs", graph([], subflows=[
            {"id": "s", "name": "S", "nodes": [{"id": "n", "type": "nonesuch"}], "edges": []},
        ]))
