from __future__ import annotations

import re
import sqlite3
import tempfile
from pathlib import Path

import pytest

from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.binding import BindingManager
from sync_state_machine.common.persistence import PersistenceBackend
from sync_state_machine.common.sqlite_backend import SQLitePersistenceBackend
from sync_state_machine.common.types import BindingStatus, SyncAction, SyncStatus
from sync_state_machine.engine import StateMachineConfig, StateMachineRuntime
from sync_state_machine.engine.events import (
    decision_note,
    e01_bootstrap,
    e10_bind_core,
    e15_bind_dependency,
    e16_bind_auto,
    e20_create_prepare,
    e21_create_prepare,
    e22_delete_prepare,
    e30_update_prepare,
    e41_post_create_ready,
    e40_sync_execute,
)
from sync_state_machine.engine.state_apply import apply_state_to_node
from sync_state_machine.sync_system.strategy_ops.reset_ops import run_phase2_cleanup
from tests.mocks.mock_datasource import MockSyncNode
from sync_state_machine.domain.project.sync_node import ProjectSyncNode

CFG_PATH = Path(__file__).resolve().parents[2] / "sync_state_machine" / "config" / "node_state_machine.yaml"
REPORT_DIR = Path(__file__).resolve().parents[3] / "test_results" / "sync_state_machine"


def _new_node(
    node_id: str,
    *,
    data_id: str = "seed-id",
    action: SyncAction = SyncAction.NONE,
    status: SyncStatus = SyncStatus.PENDING,
    binding_status: BindingStatus = BindingStatus.UNCHECKED,
) -> MockSyncNode:
    return MockSyncNode(
        node_id=node_id,
        data_id=data_id,
        data={"id": node_id, "name": "mock"},
        action=action,
        status=status,
        binding_status=binding_status,
    )


def _set_state(runtime: StateMachineRuntime, node: MockSyncNode, state_id: str, *, data_id: str) -> None:
    apply_state_to_node(
        runtime,
        node,
        state_id=state_id,
        reason="test setup",
        fields={"binding_status", "action", "status"},
    )
    apply_state_to_node(
        runtime,
        node,
        state_id=state_id,
        reason="test setup",
        fields={"data_id"},
        data_id=data_id,
    )


def _collect_transition_lines(node: MockSyncNode) -> list[str]:
    text = node.sync_log or ""
    return [line for line in text.splitlines() if "状态机流转:" in line]


def _dump_db_sync_logs(db_path: Path, *, collection_id: str) -> str:
    conn = sqlite3.connect(str(db_path))
    try:
        cursor = conn.execute(
            "SELECT node_id, node_type, action, status, binding_status, sync_log "
            "FROM nodes WHERE collection_id = ? ORDER BY node_id",
            (collection_id,),
        )
        rows = cursor.fetchall()
    finally:
        conn.close()

    lines = [
        "node_id\tnode_type\taction\tstatus\tbinding_status\tsync_log",
    ]
    for row in rows:
        node_id, node_type, action, status, binding_status, sync_log = row
        lines.append(
            f"{node_id}\t{node_type}\t{action}\t{status}\t{binding_status}\t{(sync_log or '').replace(chr(10), ' | ')}"
        )
    return "\n".join(lines)


@pytest.mark.asyncio
async def test_mock_based_full_event_and_state_coverage() -> None:
    runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH))
    cfg = runtime._config

    covered_events: set[str] = set()
    covered_states: set[str] = set()
    transition_lines: list[str] = []
    transition_rows: list[str] = ["node_id\tfrom_state\tevent\toutcome_index\tto_state\treason"]

    def record(node: MockSyncNode, decision) -> None:
        covered_events.add(decision.event_id)
        covered_states.add(decision.from_state)
        if decision.to_state:
            covered_states.add(decision.to_state)
        transition_lines.extend(_collect_transition_lines(node))
        transition_rows.append(
            f"{node.node_id}\t{decision.from_state}\t{decision.event_id}\t{decision.outcome_index}\t{decision.to_state or ''}\t{decision_note(decision)}"
        )

    # E01
    node_e01_keep = _new_node("e01_keep", action=SyncAction.NONE, status=SyncStatus.PENDING)
    d = e01_bootstrap(runtime, node=node_e01_keep, is_create_zombie=False)
    record(node_e01_keep, d)

    node_e01_del = _new_node("e01_del", action=SyncAction.CREATE, status=SyncStatus.FAILED)
    d = e01_bootstrap(runtime, node=node_e01_del, is_create_zombie=True)
    record(node_e01_del, d)

    # E10 merged outcomes
    for idx, (has_binding, local_res, peer_res) in enumerate(
        [
            (True, "NORMAL", "NORMAL"),
            (True, "WARNING", "ABNORMAL"),
            (True, "ABNORMAL", "WARNING"),
            (True, "ABNORMAL", "ABNORMAL"),
            (False, None, None),
        ],
        start=1,
    ):
        node = _new_node(f"e10_{idx}")
        _set_state(runtime, node, "S00", data_id="id-e10")
        d = e10_bind_core(
            runtime,
            node=node,
            has_binding_record=has_binding,
            core_binding_result=local_res,
            peer_core_binding_result=peer_res,
        )
        record(node, d)

    # E15 outcomes
    e15_cases = [
        dict(data_present=True, has_depend_fields=False, dep_satisfied=True),
        dict(data_present=False, has_depend_fields=True, dep_satisfied=True),
        dict(data_present=True, has_depend_fields=True, dep_satisfied=False),
        dict(data_present=True, has_depend_fields=True, dep_satisfied=True),
    ]
    for idx, case in enumerate(e15_cases, start=1):
        node = _new_node(f"e15_{idx}")
        _set_state(runtime, node, "S02", data_id="id-e15")
        d = e15_bind_dependency(runtime, node=node, **case)
        record(node, d)

    node_e22 = _new_node("e22")
    _set_state(runtime, node_e22, "S02", data_id="id-e22")
    d = e22_delete_prepare(runtime, node=node_e22)
    record(node_e22, d)

    # E16 outcomes
    e16_cases = [
        dict(auto_bind_enabled=False, auto_bind_outcome=None, create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="DATA_MISSING", create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="ONE_TO_ONE", create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="AMBIGUOUS", create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="KEY_MISSING", create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="KEY_ID_RESOLVE_FAIL", create_enabled=None),
        dict(auto_bind_enabled=True, auto_bind_outcome="ZERO", create_enabled=True),
        dict(auto_bind_enabled=True, auto_bind_outcome="ZERO", create_enabled=False),
    ]
    for idx, case in enumerate(e16_cases, start=1):
        node = _new_node(f"e16_{idx}")
        _set_state(runtime, node, "S02", data_id="id-e16")
        d = e16_bind_auto(runtime, node=node, **case)
        record(node, d)

    # E20 commit outcomes from S13
    node_e20_ok = _new_node("e20_ok", action=SyncAction.CREATE, status=SyncStatus.PENDING, binding_status=BindingStatus.MISSING)
    _set_state(runtime, node_e20_ok, "S13", data_id="id-e20")
    d = e20_create_prepare(runtime, node=node_e20_ok, spawn_success=True)
    record(node_e20_ok, d)

    node_e20_commit_fail = _new_node("e20_commit_fail")
    _set_state(runtime, node_e20_commit_fail, "S13", data_id="id-e20-f")
    d = e20_create_prepare(runtime, node=node_e20_commit_fail, spawn_success=False)
    assert d.to_state is None
    record(node_e20_commit_fail, d)

    node_e21 = _new_node("e21")
    _set_state(runtime, node_e21, "S02", data_id="id-e21")
    d = e21_create_prepare(
        runtime,
        node=node_e21,
        create_enabled=True,
        data_present=True,
        create_id_resolve_ok=False,
    )
    record(node_e21, d)

    # E41: create success normalize to update entry
    node_e41 = _new_node("e41", action=SyncAction.CREATE, status=SyncStatus.SUCCESS, binding_status=BindingStatus.NORMAL)
    _set_state(runtime, node_e41, "S11C", data_id="id-e41")
    d = e41_post_create_ready(runtime, node=node_e41, execute_action="CREATE")
    record(node_e41, d)
    assert d.to_state == "S01"

    # E30 outcomes (from S01 only)
    source = _new_node("e30_source_1")
    target = _new_node("e30_target_1")
    _set_state(runtime, source, "S01", data_id="id-s1")
    _set_state(runtime, target, "S01", data_id="id-t1")
    d = e30_update_prepare(
        runtime,
        source_node=source,
        target_node=target,
        update_enabled=True,
        target_has_data_id=False,
        update_id_resolve_ok=False,
        has_diff=False,
    )
    record(target, d)

    source = _new_node("e30_source_2")
    target = _new_node("e30_target_2")
    _set_state(runtime, source, "S01", data_id="id-s2")
    _set_state(runtime, target, "S01", data_id="id-t2")
    d = e30_update_prepare(
        runtime,
        source_node=source,
        target_node=target,
        update_enabled=True,
        target_has_data_id=True,
        update_id_resolve_ok=False,
        has_diff=False,
    )
    record(target, d)

    source = _new_node("e30_source_3")
    target = _new_node("e30_target_3")
    _set_state(runtime, source, "S01", data_id="id-s3")
    _set_state(runtime, target, "S01", data_id="id-t3")
    d = e30_update_prepare(
        runtime,
        source_node=source,
        target_node=target,
        update_enabled=True,
        target_has_data_id=True,
        update_id_resolve_ok=True,
        has_diff=True,
    )
    record(target, d)

    # E40 outcomes (all branches)
    node = _new_node("e40_timeout", action=SyncAction.CREATE, status=SyncStatus.IN_PROGRESS)
    _set_state(runtime, node, "S10C", data_id="")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="FAILED",
        action="CREATE",
        poll_timeout=True,
    )
    assert d is not None
    assert d.to_state == "S12C"
    record(node, d)

    node = _new_node("e40_success", action=SyncAction.CREATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S06", data_id="")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SUCCESS",
        action="CREATE",
        poll_timeout=False,
        result_data_id="created-001",
    )
    assert d is not None
    assert node.data_id == "created-001"
    record(node, d)

    node = _new_node("e40_failed", action=SyncAction.UPDATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S07", data_id="id-upd")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="FAILED",
        action="UPDATE",
        poll_timeout=False,
    )
    assert d is not None
    assert d.to_state == "S12U"
    record(node, d)

    node = _new_node("e40_delete_failed", action=SyncAction.DELETE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S09", data_id="id-del-failed")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="FAILED",
        action="DELETE",
        poll_timeout=False,
    )
    assert d is not None
    assert d.to_state == "S12D"
    record(node, d)

    node = _new_node("e40_update_success", action=SyncAction.UPDATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S07", data_id="id-upd-success")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SUCCESS",
        action="UPDATE",
        poll_timeout=False,
    )
    assert d is not None
    record(node, d)

    node = _new_node("e40_inprogress", action=SyncAction.CREATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S06", data_id="")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="IN_PROGRESS",
        action="CREATE",
        poll_timeout=False,
    )
    assert d is not None
    assert d.to_state == "S10C"
    record(node, d)

    node = _new_node("e40_update_inprogress", action=SyncAction.UPDATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S07", data_id="id-upd-ip")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="IN_PROGRESS",
        action="UPDATE",
        poll_timeout=False,
    )
    assert d is not None
    assert d.to_state == "S10U"
    record(node, d)

    node = _new_node("e40_delete_inprogress", action=SyncAction.DELETE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S09", data_id="id-del-ip")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="IN_PROGRESS",
        action="DELETE",
        poll_timeout=False,
    )
    assert d is not None
    assert d.to_state == "S10D"
    record(node, d)

    node = _new_node("e40_update_success_from_ip", action=SyncAction.UPDATE, status=SyncStatus.IN_PROGRESS)
    _set_state(runtime, node, "S10U", data_id="id-upd-ip2")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SUCCESS",
        action="UPDATE",
        poll_timeout=False,
    )
    assert d is not None
    record(node, d)

    node = _new_node("e40_delete_success_from_ip", action=SyncAction.DELETE, status=SyncStatus.IN_PROGRESS)
    _set_state(runtime, node, "S10D", data_id="id-del-ip2")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SUCCESS",
        action="DELETE",
        poll_timeout=False,
    )
    assert d is not None
    record(node, d)

    node = _new_node("e40_skip_update", action=SyncAction.UPDATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S07", data_id="id-upd")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SKIPPED",
        action="UPDATE",
        poll_timeout=False,
    )
    assert d is not None
    record(node, d)

    node_e40_delete_success = _new_node("e40_delete_success", action=SyncAction.DELETE, status=SyncStatus.PENDING)
    _set_state(runtime, node_e40_delete_success, "S09", data_id="id-e40-del")
    d = e40_sync_execute(
        runtime,
        node=node_e40_delete_success,
        handler_result="SUCCESS",
        action="DELETE",
        poll_timeout=False,
    )
    record(node_e40_delete_success, d)
    assert d is not None and d.to_state == "S11D"

    node_e40_delete_skipped = _new_node("e40_delete_skipped", action=SyncAction.DELETE, status=SyncStatus.PENDING)
    _set_state(runtime, node_e40_delete_skipped, "S09", data_id="id-e40-del-s")
    d = e40_sync_execute(
        runtime,
        node=node_e40_delete_skipped,
        handler_result="SKIPPED",
        action="DELETE",
        poll_timeout=False,
    )
    assert d is None

    node = _new_node("e40_skip_create", action=SyncAction.CREATE, status=SyncStatus.PENDING)
    _set_state(runtime, node, "S06", data_id="")
    d = e40_sync_execute(
        runtime,
        node=node,
        handler_result="SKIPPED",
        action="CREATE",
        poll_timeout=False,
    )
    assert d is None

    expected_events = set(cfg.transitions.keys())
    expected_states: set[str] = set()
    for transition in cfg.transitions.values():
        expected_states.update(transition.from_states)
        for outcome in transition.outcomes:
            if outcome.to:
                expected_states.add(outcome.to)

    assert covered_events == expected_events
    assert expected_states <= covered_states

    assert transition_lines, "state-machine transition log should not be empty"
    pattern = re.compile(r"状态机流转: S\d{2}[A-Z]? --E\d{2}\[\d+\]--> S\d{2}[A-Z]?")
    assert any(pattern.search(line) for line in transition_lines)

    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    report_file = REPORT_DIR / "mock_full_event_state_transitions.txt"
    report_file.write_text("\n".join(transition_lines), encoding="utf-8")
    report_table = REPORT_DIR / "mock_full_event_state_transitions_table.tsv"
    report_table.write_text("\n".join(transition_rows), encoding="utf-8")
    print(f"[TEST-REPORT] Full event/state transition log written to: {report_file}")
    print(f"[TEST-REPORT] Full event/state transition table written to: {report_table}")


@pytest.mark.asyncio
async def test_sync_log_cleared_on_initial_load(tmp_path: Path) -> None:
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    db = tmp_path / "sync_log_clear_state.db"

    persistence = SQLitePersistenceBackend(str(db))
    await persistence.initialize()
    collection = DataCollection("project-log-clear", persistence=persistence, auto_persist=False)

    node = ProjectSyncNode(
        node_id="log-node",
        data_id="proj-1",
        data={
            "id": "proj-1",
            "type": "project",
            "name": "Log Project",
            "code": "P-LOG",
            "status": "active",
            "progress_type": "C",
            "project_type": "onshore_wind",
        },
        action=SyncAction.CREATE,
        status=SyncStatus.PENDING,
        binding_status=BindingStatus.NORMAL,
    )
    node.append_log("old lifecycle")

    runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH))
    d1 = e01_bootstrap(runtime, node=node, is_create_zombie=False)
    d2 = e10_bind_core(runtime, node=node, has_binding_record=False)
    assert d1.to_state == "S00"
    assert d2.to_state == "S02"

    await collection.add(node)
    await collection.persist()

    before_reload_report = REPORT_DIR / "db_sync_log_before_reload.txt"
    before_reload_report.write_text(
        _dump_db_sync_logs(db, collection_id="project-log-clear"),
        encoding="utf-8",
    )
    print(f"[TEST-REPORT] DB sync_log snapshot before reload: {before_reload_report}")

    reloaded = DataCollection("project-log-clear", persistence=persistence, auto_persist=False)
    await reloaded.load_from_persistence()
    loaded_node = reloaded.get("log-node")
    assert loaded_node is not None
    assert loaded_node.sync_log is None

    in_memory_after_reload_report = REPORT_DIR / "in_memory_sync_log_after_reload.txt"
    in_memory_after_reload_report.write_text(
        f"node_id=log-node\nsync_log={loaded_node.sync_log}\n",
        encoding="utf-8",
    )
    print(f"[TEST-REPORT] In-memory sync_log snapshot after reload: {in_memory_after_reload_report}")

    await reloaded.persist()
    after_reload_persist_report = REPORT_DIR / "db_sync_log_after_reload_persist.txt"
    after_reload_persist_report.write_text(
        _dump_db_sync_logs(db, collection_id="project-log-clear"),
        encoding="utf-8",
    )
    print(f"[TEST-REPORT] DB sync_log snapshot after reload+persist: {after_reload_persist_report}")

    await persistence.close()


@pytest.mark.asyncio
async def test_load_preserves_state_then_e01_normalizes_to_s00(tmp_path: Path) -> None:
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    db = tmp_path / "load_preserve_then_e01_normalize.db"

    persistence = SQLitePersistenceBackend(str(db))
    await persistence.initialize()

    collection = DataCollection("project-e01-normalize", persistence=persistence, auto_persist=False)
    node = ProjectSyncNode(
        node_id="normalize-node",
        data_id="proj-normalize-1",
        data={
            "id": "proj-normalize-1",
            "type": "project",
            "name": "Normalize Project",
            "code": "P-NORMALIZE",
            "status": "active",
            "progress_type": "C",
            "project_type": "onshore_wind",
        },
        action=SyncAction.UPDATE,
        status=SyncStatus.FAILED,
        binding_status=BindingStatus.WARNING,
        error="last run failed",
    )
    await collection.add(node)
    await collection.persist()

    reloaded = DataCollection("project-e01-normalize", persistence=persistence, auto_persist=False)
    await reloaded.load_from_persistence()
    loaded_node = reloaded.get("normalize-node")
    assert loaded_node is not None

    # 加载阶段保留持久化状态
    assert loaded_node.action == SyncAction.UPDATE
    assert loaded_node.status == SyncStatus.FAILED
    assert loaded_node.binding_status == BindingStatus.WARNING
    assert loaded_node.error == "last run failed"

    # E01 后归并到 S00，并清空历史 error
    runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH))
    decision = e01_bootstrap(runtime, node=loaded_node, is_create_zombie=False)
    assert decision.to_state == "S00"
    assert loaded_node.binding_status == BindingStatus.UNCHECKED
    assert loaded_node.action == SyncAction.NONE
    assert loaded_node.status == SyncStatus.PENDING
    assert loaded_node.error is None

    await persistence.close()


@pytest.mark.asyncio
async def test_invalid_persisted_enum_is_bootstrap_blocked_and_skipped_in_reset(tmp_path: Path) -> None:
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    db = tmp_path / "invalid_enum_bootstrap_blocked.db"

    persistence = SQLitePersistenceBackend(str(db))
    await persistence.initialize()

    collection_id = "project-invalid-enum"
    collection = DataCollection(collection_id, persistence=persistence, auto_persist=False)
    node = ProjectSyncNode(
        node_id="invalid-enum-node",
        data_id="proj-invalid-1",
        data={
            "id": "proj-invalid-1",
            "type": "project",
            "name": "Invalid Enum Project",
            "code": "P-INV",
            "status": "active",
            "progress_type": "C",
            "project_type": "onshore_wind",
        },
        action=SyncAction.UPDATE,
        status=SyncStatus.PENDING,
        binding_status=BindingStatus.NORMAL,
    )
    await collection.add(node)
    await collection.persist()

    conn = sqlite3.connect(str(db))
    try:
        conn.execute(
            "UPDATE nodes SET action = ?, status = ?, binding_status = ? WHERE collection_id = ? AND node_id = ?",
            ("BAD_ACTION", "BAD_STATUS", "BAD_BINDING", collection_id, "invalid-enum-node"),
        )
        conn.commit()
    finally:
        conn.close()

    reloaded = DataCollection(collection_id, persistence=persistence, auto_persist=False)
    await reloaded.load_from_persistence()
    loaded_node = reloaded.get("invalid-enum-node")
    assert loaded_node is not None

    assert reloaded.is_bootstrap_blocked(loaded_node)
    assert loaded_node.action == SyncAction.NONE
    assert loaded_node.status == SyncStatus.FAILED
    assert loaded_node.binding_status == BindingStatus.ABNORMAL
    assert loaded_node.error is not None and "LOAD_INVALID_ENUM" in loaded_node.error

    runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH))
    binding_manager = BindingManager(persistence, auto_persist=False)
    remote_collection = DataCollection("empty-remote", persistence=persistence, auto_persist=False)
    cleaned = await run_phase2_cleanup(
        node_types=["project"],
        local_collection=reloaded,
        remote_collection=remote_collection,
        binding_manager=binding_manager,
        runtime=runtime,
    )
    assert cleaned == 0
    assert reloaded.get("invalid-enum-node") is not None

    await persistence.close()


@pytest.mark.asyncio
async def test_collection_filter_by_state_ids_excludes_bootstrap_blocked_by_default() -> None:
    runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH))
    collection = DataCollection("state-filter-demo", persistence=None, auto_persist=False)
    collection.set_state_machine_runtime(runtime)

    node_s00 = MockSyncNode(
        node_id="s00-node",
        data_id="id-s00",
        data={"id": "id-s00", "name": "s00"},
        binding_status=BindingStatus.UNCHECKED,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
        context={},
    )
    blocked_s00 = MockSyncNode(
        node_id="s00-blocked",
        data_id="id-s00-blocked",
        data={"id": "id-s00-blocked", "name": "blocked"},
        binding_status=BindingStatus.UNCHECKED,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
        context={"bootstrap_blocked": True},
    )

    await collection.add(node_s00)
    await collection.add(blocked_s00)

    visible = collection.filter_by_state_ids(state_ids=["S00"])
    visible_ids = {n.node_id for n in visible}
    assert "s00-node" in visible_ids
    assert "s00-blocked" not in visible_ids

    all_with_blocked = collection.filter_by_state_ids(
        state_ids=["S00"],
        include_bootstrap_blocked=True,
    )
    all_ids = {n.node_id for n in all_with_blocked}
    assert "s00-node" in all_ids
    assert "s00-blocked" in all_ids
