#include <addons.h>

static ECS_DECLARE(TagA);
static ECS_DECLARE(TagB);
ECS_COMPONENT_DECLARE(Position);
ECS_COMPONENT_DECLARE(Velocity);

static int sys_a_invoked;
static int sys_b_invoked;
static int sys_c_invoked;
static int sys_d_invoked;
static int sys_e_invoked;
static int sys_f_invoked;

static bool sys_a_real_world;
static bool sys_b_real_world;
static bool sys_c_real_world;
static bool sys_d_real_world;
static bool sys_e_real_world;
static bool sys_f_real_world;

static bool sys_a_world_readonly;
static bool sys_b_world_readonly;
static bool sys_c_world_readonly;
static bool sys_d_world_readonly;
static bool sys_e_world_readonly;
static bool sys_f_world_readonly;

static ecs_ftime_t sys_a_delta_time = 0;
static ecs_ftime_t sys_b_delta_time = 0;

void SysA(ecs_iter_t *it) { 
    ecs_os_ainc(&sys_a_invoked);
    sys_a_real_world = it->world == it->real_world;
    sys_a_world_readonly = ecs_stage_is_readonly(it->real_world);
    sys_a_delta_time = it->delta_time;
}
void SysB(ecs_iter_t *it) { 
    test_assert(sys_a_invoked != 0);
    ecs_os_ainc(&sys_b_invoked);
    sys_b_real_world = it->world == it->real_world;
    sys_b_world_readonly = ecs_stage_is_readonly(it->real_world);
    sys_b_delta_time = it->delta_time;
}
void SysC(ecs_iter_t *it) { 
    test_assert(sys_b_invoked != 0);
    ecs_os_ainc(&sys_c_invoked);
    sys_c_real_world = it->world == it->real_world;
    sys_c_world_readonly = ecs_stage_is_readonly(it->real_world);
}
void SysD(ecs_iter_t *it) { 
    test_assert(sys_c_invoked != 0);
    ecs_os_ainc(&sys_d_invoked);
    sys_d_real_world = it->world == it->real_world;
    sys_d_world_readonly = ecs_stage_is_readonly(it->real_world);
}
void SysE(ecs_iter_t *it) { 
    test_assert(sys_d_invoked != 0);
    ecs_os_ainc(&sys_e_invoked);
    sys_e_real_world = it->world == it->real_world;
    sys_e_world_readonly = ecs_stage_is_readonly(it->real_world);
}
void SysF(ecs_iter_t *it) { 
    test_assert(sys_d_invoked != 0);
    ecs_os_ainc(&sys_f_invoked);
    sys_f_real_world = it->world == it->real_world;
    sys_f_world_readonly = ecs_stage_is_readonly(it->real_world);
}

void Pipeline_system_order_same_phase(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_same_phase_after_disable(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    ecs_enable(world, SysB, false);
    test_assert( ecs_has_id(world, SysB, EcsDisabled));
    ecs_enable(world, SysB, true);
    test_assert( !ecs_has_id(world, SysB, EcsDisabled));

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_different_phase(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysC, EcsPostUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysA, EcsPreUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_different_phase_after_disable(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysC, EcsPostUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysA, EcsPreUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_enable(world, SysB, false);
    test_assert( ecs_has_id(world, SysB, EcsDisabled));
    ecs_enable(world, SysB, true);
    test_assert( !ecs_has_id(world, SysB, EcsDisabled));

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_same_phase_after_activate(void) {    
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Velocity); /* System is deactivated */
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);
    
    test_assert( ecs_has_id(world, SysB, EcsEmpty));
    ecs_add(world, E, Velocity);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    test_assert( !ecs_has_id(world, SysB, EcsEmpty));

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_different_phase_after_activate(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysC, EcsPostUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Velocity); /* System is deactivated */
    ECS_SYSTEM(world, SysA, EcsPreUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);
    
    test_assert( ecs_has_id(world, SysB, EcsEmpty));
    ecs_add(world, E, Velocity);

    ecs_progress(world, 1);

    test_assert( !ecs_has_id(world, SysB, EcsEmpty));

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_after_new_system_lower_id(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ecs_entity_t Sys = ecs_new(world, 0);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Create new system with Sys id */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.id = Sys, .name = "SysA", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA
    });

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_after_new_system_inbetween_id(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ecs_entity_t Sys = ecs_new(world, 0);
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Create new system with Sys id */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.id = Sys, .name = "SysB", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysB
    });

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_system_order_after_new_system_higher_id(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ecs_entity_t Sys = ecs_new(world, 0);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Create new system with Sys id */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.id = Sys, .name = "SysC", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysC
    });

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

static int sys_out_invoked;
static int sys_in_invoked;

static void SysOut(ecs_iter_t *it) {
    ecs_id_t ecs_id(Velocity) = ecs_field_id(it, 2);

    sys_out_invoked ++;

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_set(it->world, it->entities[i], Velocity, {10, 20});
    }
}

static void SysOutMain(ecs_iter_t *it) {
    Velocity *v = ecs_field(it, Velocity, 2);

    sys_out_invoked ++;

    int i;
    for (i = 0; i < it->count; i ++) {
        v[i].x = 10;
        v[i].y = 20;
    }
}

static void SysIn(ecs_iter_t *it) {
    ecs_id_t ecs_id(Velocity) = ecs_field_id(it, 1);

    test_assert(sys_out_invoked != 0);
    sys_in_invoked ++;
    
    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t e = it->entities[i];
        test_assert( ecs_has(it->world, e, Velocity));

        const Velocity *v_ptr = ecs_get(it->world, e, Velocity);
        test_int(v_ptr->x, 10);
        test_int(v_ptr->y, 20);
    }
}

static void SysInMain(ecs_iter_t *it) {
    Velocity *v = ecs_field(it, Velocity, 1);
    ecs_id_t ecs_id(Velocity) = ecs_field_id(it, 1);

    test_assert(sys_out_invoked != 0);
    sys_in_invoked ++;
    
    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t e = it->entities[i];
        test_assert( ecs_has(it->world, e, Velocity));

        test_int(v[i].x, 10);
        test_int(v[i].y, 20);
    }
}

void Pipeline_merge_after_staged_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysOut, EcsOnUpdate, Position, [out] Velocity());
    ECS_SYSTEM(world, SysInMain, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 2);
    test_int(stats->pipeline_build_count_total, 2);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 2);

    ecs_fini(world);
}

void Pipeline_merge_after_not_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysOut, EcsOnUpdate, Position, [out] !Velocity);
    ECS_SYSTEM(world, SysInMain, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 2);
    test_int(stats->pipeline_build_count_total, 2);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 2);

    ecs_fini(world);
}

void Pipeline_no_merge_after_main_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position, Velocity);

    ECS_SYSTEM(world, SysOutMain, EcsOnUpdate, Position, Velocity);
    ECS_SYSTEM(world, SysInMain, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_merge_after_staged_in_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position, Velocity);

    /* Requires merge, because getting value in 2nd system cannot access data
     * written to stage from first system */
    ECS_SYSTEM(world, SysOut, EcsOnUpdate, Position, [out] Velocity());
    ECS_SYSTEM(world, SysIn, EcsOnUpdate, [in] Velocity());

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 2);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_merge_after_staged_inout_main_implicit_inout(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position, Velocity());
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 1);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_merge_after_staged_inout_main_inout(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position, Velocity);

    ECS_SYSTEM(world, SysOut, EcsOnUpdate, Position, [inout] Velocity());
    ECS_SYSTEM(world, SysIn, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 2);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 1);

    ecs_fini(world);
}

void Pipeline_merge_after_staged_out_before_owned(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysOut, EcsOnUpdate, Position, [out] Velocity());
    ECS_SYSTEM(world, SysInMain, EcsOnUpdate, Velocity);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 1);

    test_int(stats->systems_ran_frame, 2);
    test_int(stats->merge_count_total, 2);
    test_int(stats->pipeline_build_count_total, 2);

    test_int(sys_out_invoked, 1);
    test_int(sys_in_invoked, 1);

    ecs_progress(world, 1);
    test_int(stats->pipeline_build_count_total, 2);

    ecs_fini(world);
}

void Pipeline_switch_pipeline(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysC, EcsOnUpdate, Position);

    ecs_add(world, ecs_id(SysC), Tag);

    ECS_PIPELINE(world, P1, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), !Tag);

    ecs_progress(world, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_set_pipeline(world, P1);

    ecs_progress(world, 1);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 2);
    test_int(sys_c_invoked, 1);

    ecs_fini(world);
}

void Pipeline_run_pipeline(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysC, EcsPostUpdate, Position);

    ecs_add(world, ecs_id(SysC), Tag);

    ECS_PIPELINE(world, P1, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), !Tag);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(stats->systems_ran_frame, 3);
    test_int(stats->merge_count_total, 1);
    test_int(stats->pipeline_build_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_run_pipeline(world, P1, 1);

    test_int(stats->pipeline_build_count_total, 2);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 2);
    test_int(sys_c_invoked, 1);

    ecs_fini(world);
}

void Pipeline_get_pipeline_from_stage(void) {
    ecs_world_t *world = ecs_init();

    ecs_entity_t pipeline = ecs_get_pipeline(world);
    test_assert(pipeline != 0);

    /* Get default stage */
    ecs_world_t *stage = ecs_get_stage(world, 0);
    test_assert(stage != NULL);

    test_assert(pipeline == ecs_get_pipeline(stage));

    ecs_fini(world);
}

void Pipeline_3_systems_3_types(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA
    });
    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = NULL, .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysB
    });
    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position()",
        .callback = SysC
    });

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);

    ecs_add_id(world, s3, Tag);

    ecs_new(world, Position);

    ecs_progress(world, 1);  

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_fini(world);
}

static
void RandomWrite(ecs_iter_t *it) {
    ecs_entity_t ecs_id(Position) = ecs_field_id(it, 2);

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_set(it->world, it->entities[i], Position, {1, 2});
    }
}

static
void RandomRead(ecs_iter_t *it) {
    ecs_entity_t ecs_id(Position) = ecs_lookup(it->world, "Position");

    int i;
    for (i = 0; i < it->count; i ++) {
        const Position *p = ecs_get(it->world, it->entities[i], Position);
        test_assert(p != NULL);
        test_int(p->x, 1);
        test_int(p->y, 2);
    }
}

static
void RandomReadWrite(ecs_iter_t *it) {
    ecs_entity_t ecs_id(Position) = ecs_lookup(it->world, "Position");

    int i;
    for (i = 0; i < it->count; i ++) {
        const Position *p = ecs_get(it->world, it->entities[i], Position);
        test_assert(p != NULL);
        test_int(p->x, 1);
        test_int(p->y, 2);

        ecs_set(it->world, it->entities[i], Position, {p->x + 1, p->y + 1});
    }
}

static
void RandomReadAfterRW(ecs_iter_t *it) {
    ecs_entity_t ecs_id(Position) = ecs_lookup(it->world, "Position");

    int i;
    for (i = 0; i < it->count; i ++) {
        const Position *p = ecs_get(it->world, it->entities[i], Position);
        test_assert(p != NULL);
        test_int(p->x, 2);
        test_int(p->y, 3);
    }
}

static
void RandomRead_Not(ecs_iter_t *it) {
    ecs_entity_t ecs_id(Position) = ecs_field_id(it, 2);

    int i;
    for (i = 0; i < it->count; i ++) {
        test_assert(!ecs_has(it->world, it->entities[i], Position));
        const Position *p = ecs_get(it->world, it->entities[i], Position);
        test_assert(p == NULL);
    }
}

void Pipeline_random_read_after_random_write_out_in(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] Position());
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [in] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);  

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_inout_in(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [inout] Position());
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [in] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);  

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_out_inout(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] Position());
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [inout] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);  

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_inout_inout(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [inout] Position());
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [inout] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);  

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_w_not_write(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] !Position);
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [in] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);  

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_w_not_read(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] Position());
    ECS_SYSTEM(world, RandomRead_Not, EcsOnUpdate, Tag, [in] !Position);
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_read_after_random_write_w_wildcard(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] Position());
    ECS_SYSTEM(world, RandomRead, EcsOnUpdate, Tag, [in] *());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 1);
    test_int(p->y, 2);

    ecs_fini(world);
}

void Pipeline_random_in_after_random_inout_after_random_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ECS_SYSTEM(world, RandomWrite, EcsOnUpdate, Tag, [out] Position());
    ECS_SYSTEM(world, RandomReadWrite, EcsOnUpdate, Tag, [inout] Position());
    ECS_SYSTEM(world, RandomReadAfterRW, EcsOnUpdate, Tag, [in] Position());
    
    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 1);

    test_assert(ecs_has(world, e, Position));
    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 2);
    test_int(p->y, 3);

    ecs_fini(world);
}

static
void cb_first(ecs_iter_t *it) {
    int32_t *count = it->ctx;
    test_int(count[0], 0);
    count[0] ++;
}

static
void cb_second(ecs_iter_t *it) {
    int32_t *count = it->ctx;
    test_int(count[0], 1);
    count[0] ++;
}

static
void cb_third(ecs_iter_t *it) {
    int32_t *count = it->ctx;
    test_int(count[0], 2);
    count[0] ++;
}

void Pipeline_system_reverse_order_by_phase_custom_pipeline(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);

    ecs_entity_t PreFrame = ecs_new_w_id(world, EcsPhase);
    ecs_entity_t OnFrame = ecs_new_w_id(world, EcsPhase);
    ecs_entity_t PostFrame = ecs_new_w_id(world, EcsPhase);
    ecs_add_pair(world, OnFrame, EcsDependsOn, PreFrame);
    ecs_add_pair(world, PostFrame, EcsDependsOn, OnFrame);

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);

    int count = 0;

    ecs_system_init(world, &(ecs_system_desc_t){
        .callback = cb_third,
        .ctx = &count,
        .entity = ecs_entity(world, {.add = {Tag, ecs_pair(EcsDependsOn, PostFrame)}})
    });

    ecs_system_init(world, &(ecs_system_desc_t){
        .callback = cb_second,
        .ctx = &count,
        .entity = ecs_entity(world, {.add = {Tag, ecs_pair(EcsDependsOn, OnFrame)}})
    });

    ecs_system_init(world, &(ecs_system_desc_t){
        .callback = cb_first,
        .ctx = &count,
        .entity = ecs_entity(world, {.add = {Tag, ecs_pair(EcsDependsOn, PreFrame)}})
    });

    test_int(count, 0);

    ecs_set_pipeline(world, P);

    ecs_progress(world, 0);

    test_int(count, 3);

    ecs_fini(world);
}

void Pipeline_stage_write_before_read(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA
    });
    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "[out] Position(), Position",
        .callback = SysB
    });
    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysC
    });

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);
    ecs_set_pipeline(world, P);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);

    ecs_new(world, Position);

    ecs_progress(world, 1);  

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_pipeline_stats_t stats = {0};
    test_bool(ecs_pipeline_stats_get(world, 
        ecs_get_pipeline(world), &stats), true);

    test_int(ecs_vec_count(&stats.systems), 5);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[0], s1);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[1], s2);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[2], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[3], s3);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[4], 0); /* merge */

    ecs_pipeline_stats_fini(&stats);

    ecs_fini(world);
}

static
void Pipeline_mixed_multithreaded_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA,
        .multi_threaded = true
    });
    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysB
    });
    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysC
    });
    ecs_entity_t s4 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysD", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysD,
        .multi_threaded = true
    });
    ecs_entity_t s5 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysE", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysE,
        .multi_threaded = true
    });
    ecs_entity_t s6 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysF", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysF
    });

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);
    ecs_set_pipeline(world, P);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);
    test_assert(s4 != 0);
    test_assert(s5 != 0);
    test_assert(s6 != 0);

    ecs_new(world, Position);
    ecs_new(world, Position);

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
    	ecs_set_threads(world, 2);
    }

    ecs_progress(world, 1);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);
    test_int(sys_d_invoked, 2);
    test_int(sys_e_invoked, 2);
    test_int(sys_f_invoked, 1);

    ecs_pipeline_stats_t stats = {0};
    test_bool(ecs_pipeline_stats_get(world, 
        ecs_get_pipeline(world), &stats), true);

    test_int(ecs_vec_count(&stats.systems), 10);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[0], s1);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[1], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[2], s2);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[3], s3);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[4], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[5], s4);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[6], s5);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[7], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[8], s6);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[9], 0); /* merge */

    ecs_pipeline_stats_fini(&stats);

    ecs_fini(world);
}

void Pipeline_mixed_multithreaded(void) {
    Pipeline_mixed_multithreaded_internal(false);
}

void Pipeline_mixed_multithreaded_tasks(void) {
    Pipeline_mixed_multithreaded_internal(true);
}

void Pipeline_mixed_staging(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA,
        .no_readonly = true
    });
    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysB
    });
    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysC
    });
    ecs_entity_t s4 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysD", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysD,
        .no_readonly = true
    });
    ecs_entity_t s5 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysE", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysE,
        .no_readonly = true
    });
    ecs_entity_t s6 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysF", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysF
    });

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);
    ecs_set_pipeline(world, P);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);
    test_assert(s4 != 0);
    test_assert(s5 != 0);
    test_assert(s6 != 0);

    ecs_new(world, Position);
    ecs_new(world, Position);

    ecs_progress(world, 1);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);
    test_int(sys_d_invoked, 1);
    test_int(sys_e_invoked, 1);
    test_int(sys_f_invoked, 1);
    test_int(sys_a_real_world, true);
    test_int(sys_b_real_world, false);
    test_int(sys_c_real_world, false);
    test_int(sys_d_real_world, true);
    test_int(sys_e_real_world, true);
    test_int(sys_f_real_world, false);
    test_int(sys_a_world_readonly, false);
    test_int(sys_b_world_readonly, true);
    test_int(sys_c_world_readonly, true);
    test_int(sys_d_world_readonly, false);
    test_int(sys_e_world_readonly, false);
    test_int(sys_f_world_readonly, true);

    ecs_progress(world, 1);
    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 2);
    test_int(sys_c_invoked, 2);
    test_int(sys_d_invoked, 2);
    test_int(sys_e_invoked, 2);
    test_int(sys_f_invoked, 2);
    test_int(sys_a_real_world, true);
    test_int(sys_b_real_world, false);
    test_int(sys_c_real_world, false);
    test_int(sys_d_real_world, true);
    test_int(sys_e_real_world, true);
    test_int(sys_f_real_world, false);
    test_int(sys_a_world_readonly, false);
    test_int(sys_b_world_readonly, true);
    test_int(sys_c_world_readonly, true);
    test_int(sys_d_world_readonly, false);
    test_int(sys_e_world_readonly, false);
    test_int(sys_f_world_readonly, true);

    ecs_pipeline_stats_t stats = {0};
    test_bool(ecs_pipeline_stats_get(world, 
        ecs_get_pipeline(world), &stats), true);

    test_int(ecs_vec_count(&stats.systems), 11);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[0], s1);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[1], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[2], s2);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[3], s3);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[4], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[5], s4);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[6], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[7], s5);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[8], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[9], s6);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[10], 0); /* merge */

    ecs_pipeline_stats_fini(&stats);

    ecs_fini(world);
}

static
void WritePosition(ecs_iter_t *it) {
    if (*(bool*)it->ctx) {
        ecs_entity_t ecs_id(Position) = ecs_field_id(it, 2);
        for (int i = 0; i < it->count; i ++) {
            ecs_add(it->world, it->entities[i], Position);
        }
    }
}

void Pipeline_single_threaded_pipeline_change(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    bool write_position = false;

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, !Position",
        .callback = SysA
    });

    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, Position",
        .callback = SysB
    });

    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "WritePosition", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, [out] Position()",
        .callback = WritePosition,
        .ctx = &write_position
    });

    ecs_entity_t s4 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, Position",
        .callback = SysC
    });

    ecs_entity_t s5 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysD", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, !Position",
        .callback = SysD
    });

    /* Initialize to 1 so asserts inside system won't trigger */
    sys_a_invoked = 1;
    sys_b_invoked = 1;
    sys_c_invoked = 1;
    sys_d_invoked = 1;

    ecs_new(world, Tag);
    ecs_new(world, Tag);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);
    test_assert(s4 != 0);
    test_assert(s5 != 0);

    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 3);
    test_int(sys_b_invoked, 1 + 0);
    test_int(sys_c_invoked, 1 + 0);
    test_int(sys_d_invoked, 1 + 3);

    write_position = true;

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 4);
    test_int(sys_b_invoked, 1 + 0);
    test_int(sys_c_invoked, 1 + 1);
    test_int(sys_d_invoked, 1 + 3);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 4);
    test_int(sys_b_invoked, 1 + 1);
    test_int(sys_c_invoked, 1 + 2);
    test_int(sys_d_invoked, 1 + 3);

    ecs_fini(world);
}

static
void Pipeline_multi_threaded_pipeline_change_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    bool write_position = false;

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, !Position",
        .callback = SysA,
        .multi_threaded = true
    });

    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, Position, [out] Position()",
        .callback = SysB,
        .multi_threaded = true
    });

    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "WritePosition", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, [out] Position(), [in] ?Position",
        .callback = WritePosition,
        .ctx = &write_position,
        .multi_threaded = true
    });

    ecs_entity_t s4 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, Position",
        .callback = SysC,
        .multi_threaded = true
    });

    ecs_entity_t s5 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysD", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, !Position",
        .callback = SysD,
        .multi_threaded = true
    });

    if (task_threads) {
        ecs_set_task_threads(world, 2);
    } else {
        ecs_set_threads(world, 2);
    }

    /* Initialize to 1 so asserts inside system won't trigger */
    sys_a_invoked = 1;
    sys_b_invoked = 1;
    sys_c_invoked = 1;
    sys_d_invoked = 1;

    ecs_new(world, Tag);
    ecs_new(world, Tag);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);
    test_assert(s4 != 0);
    test_assert(s5 != 0);

    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 3 * 2);
    test_int(sys_b_invoked, 1 + 0 * 2);
    test_int(sys_c_invoked, 1 + 0 * 2);
    test_int(sys_d_invoked, 1 + 3 * 2);

    write_position = true;

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 4 * 2);
    test_int(sys_b_invoked, 1 + 0 * 2);
    test_int(sys_c_invoked, 1 + 1 * 2);
    test_int(sys_d_invoked, 1 + 3 * 2);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1 + 4 * 2);
    test_int(sys_b_invoked, 1 + 1 * 2);
    test_int(sys_c_invoked, 1 + 2 * 2);
    test_int(sys_d_invoked, 1 + 3 * 2);

    ecs_fini(world);
}

void Pipeline_multi_threaded_pipeline_change(void) {
    Pipeline_multi_threaded_pipeline_change_internal(false);
}

void Pipeline_multi_threaded_pipeline_change_tasks(void) {
    Pipeline_multi_threaded_pipeline_change_internal(true);
}

void Pipeline_activate_after_add(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    bool write_position = false;

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "WritePosition", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Tag, [out] Position()",
        .callback = WritePosition,
        .ctx = &write_position
    });

    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA
    });

    ecs_new(world, Tag);

    test_assert(s1 != 0);
    test_assert(s2 != 0);

    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);

    test_int(sys_a_invoked, 0);

    write_position = true;

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);

    ecs_fini(world);
}

static ecs_query_t *q_result;

static
void CreateQuery(ecs_iter_t *it) {
    test_assert(it->real_world == it->world);
    q_result = ecs_query_new(it->world, "Position");
}

void Pipeline_no_staging_system_create_query(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ecs_entity_t s = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "CreateQuery", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .callback = CreateQuery,
        .no_readonly = true
    });

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);
    ecs_set_pipeline(world, P);

    test_assert(s != 0);

    ecs_progress(world, 1.0);

    test_assert(q_result != NULL);

    ecs_pipeline_stats_t stats = {0};
    test_bool(ecs_pipeline_stats_get(world, 
        ecs_get_pipeline(world), &stats), true);

    test_int(ecs_vec_count(&stats.systems), 2);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[0], s);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[1], 0); /* merge */

    ecs_pipeline_stats_fini(&stats);

    ecs_fini(world);
}

static int set_singleton_invoked = 0;
static int match_singleton_invoked = 0;
static int match_all_invoked = 0;

static void set_singleton(ecs_iter_t *it) {
    ecs_singleton_add(it->world, TagB);
    set_singleton_invoked ++;
}

static void match_singleton(ecs_iter_t *it) {
    match_singleton_invoked ++;
}

static void match_all(ecs_iter_t *it) {
    match_all_invoked ++;
}

void Pipeline_match_all_after_pipeline_rebuild(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG_DEFINE(world, TagA);
    ECS_TAG_DEFINE(world, TagB);

    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "[out] TagB()",
        .callback = set_singleton
    });

    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "TagB",
        .callback = match_singleton
    });

    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .add = {ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "?TagA",
        .callback = match_all
    });

    ecs_progress(world, 0);

    test_int(set_singleton_invoked, 1);
    test_int(match_singleton_invoked, 1);
    test_assert(match_all_invoked >= 1);

    ecs_fini(world);
}

void Pipeline_empty_pipeline(void) {
    ecs_world_t *world = ecs_mini();

    ECS_IMPORT(world, FlecsPipeline);

    const ecs_world_info_t *info = ecs_get_world_info(world);
    test_int(info->frame_count_total, 0);

    ecs_progress(world, 0);

    test_int(info->frame_count_total, 1);

    ecs_fini(world);
}

void Pipeline_custom_pipeline_w_system_macro(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_PIPELINE(world, P, flecs.system.System, Tag);

    ECS_SYSTEM(world, SysA, Tag, Position);
    ECS_SYSTEM(world, SysB, Tag, Position);
    ECS_SYSTEM(world, SysC, Tag, Position);

    ecs_set_pipeline(world, P);
    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    ecs_fini(world);
}

void Pipeline_pipeline_w_short_notation(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);

    ecs_entity_t p = ecs_pipeline(world, {
        .query.filter.expr = "flecs.system.System, Tag"
    });

    test_assert(p != 0);

    ecs_fini(world);
}

void Pipeline_stack_allocator_after_progress(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);

    ecs_filter_t *f = ecs_filter(world, {
        .terms = {{ ecs_id(Position) }}
    });

    ecs_iter_t it = ecs_filter_iter(world, f);
    ecs_stack_cursor_t cursor = *it.priv.cache.stack_cursor;
    ecs_iter_fini(&it);

    ecs_progress(world, 1);
    test_int(sys_a_invoked, 1);

    it = ecs_filter_iter(world, f);
    test_assert(it.priv.cache.stack_cursor->page == cursor.page);
    test_assert(it.priv.cache.stack_cursor->sp == cursor.sp);
    ecs_iter_fini(&it);

    ecs_filter_fini(f);

    ecs_fini(world);
}

void Pipeline_stack_allocator_after_progress_w_pipeline_change(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, E, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Position);
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Position);

    ecs_filter_t *f = ecs_filter(world, {
        .terms = {{ ecs_id(Position) }}
    });

    ecs_iter_t it = ecs_filter_iter(world, f);
    ecs_stack_cursor_t cursor = *it.priv.cache.stack_cursor;
    ecs_iter_fini(&it);

    ecs_progress(world, 1);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);

    it = ecs_filter_iter(world, f);
    test_assert(it.priv.cache.stack_cursor->page == cursor.page);
    test_assert(it.priv.cache.stack_cursor->sp == cursor.sp);
    ecs_iter_fini(&it);

    ecs_enable(world, SysB, false);

    ecs_progress(world, 1);
    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 1);

    it = ecs_filter_iter(world, f);
    test_assert(it.priv.cache.stack_cursor->page == cursor.page);
    test_assert(it.priv.cache.stack_cursor->sp == cursor.sp);
    ecs_iter_fini(&it);

    ecs_filter_fini(f);

    ecs_fini(world);
}

static
void Sys_w_MainWorldIter(ecs_iter_t *it) {
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);
    ecs_filter_t *f = ecs_filter(it->real_world, {
        .terms = {{ ecs_id(Position) }}
    });

    ecs_iter_t fit = ecs_filter_iter(it->real_world, f);
    test_bool(true, ecs_filter_next(&fit));
    test_int(1, fit.count);
    test_bool(false, ecs_filter_next(&fit));
    ecs_filter_fini(f);
}

static
void Pipeline_iter_from_world_in_singlethread_system_multitead_app_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);

    ecs_new(world, Position);

    ECS_SYSTEM(world, Sys_w_MainWorldIter, EcsOnUpdate, Position());

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
        ecs_set_threads(world, 2);
    }

    ecs_progress(world, 0);

    ecs_fini(world);
}

void Pipeline_iter_from_world_in_singlethread_system_multitead_app(void) {
    Pipeline_iter_from_world_in_singlethread_system_multitead_app_internal(false);
}

void Pipeline_iter_from_world_in_singlethread_system_multitead_app_tasks(void) {
    Pipeline_iter_from_world_in_singlethread_system_multitead_app_internal(true);
}

static int staging_system_invoked = 0;
static void StagingSystem(ecs_iter_t *it) {
    test_assert( ecs_stage_is_readonly(it->real_world));
    staging_system_invoked ++;
}

static int no_staging_system_invoked = 0;
static void NoStagingSystem(ecs_iter_t *it) {
    test_assert( !ecs_stage_is_readonly(it->real_world));
    no_staging_system_invoked ++;
}

void Pipeline_no_staging_after_inactive_system(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_TAG(world, Tag);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ Tag, .oper = EcsNot }},
        .callback = StagingSystem
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }, { Tag, .oper = EcsNot }},
        .callback = StagingSystem
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystem,
        .no_readonly = true
    });

    ecs_progress(world, 0);
    test_assert(staging_system_invoked != 0);
    test_int(no_staging_system_invoked, 1);

    staging_system_invoked = 0;
    no_staging_system_invoked = 0;

    ecs_progress(world, 0);
    test_assert(staging_system_invoked != 0);
    test_int(no_staging_system_invoked, 1);

    ecs_fini(world);
}

static ecs_entity_t create_position_e = 0;
static ecs_entity_t create_velocity_e = 0;

static int no_staging_create_position_invoked = 0;
static int no_staging_create_velocity_invoked = 0;

static void NoStagingSystemCreatePosition(ecs_iter_t *it) {
    ecs_defer_end(it->world);
    
    create_position_e = ecs_new_id(it->world);
    ecs_set(it->world, create_position_e, Position, {0, 0});
    
    ecs_filter_t *f = ecs_filter(it->world, {
        .terms = {{ ecs_id(Position) }}
    });

    ecs_iter_t fit = ecs_filter_iter(it->world, f);
    test_bool(true, ecs_filter_next(&fit));
    test_int(fit.count, 1);
    test_uint(fit.entities[0], create_position_e);
    test_bool(false, ecs_filter_next(&fit));

    ecs_filter_fini(f);

    ecs_defer_begin(it->world);
    no_staging_create_position_invoked ++;
}

static void NoStagingSystemCreateVelocity(ecs_iter_t *it) {
    ecs_defer_end(it->world);
    
    create_velocity_e = ecs_new_id(it->world);
    ecs_set(it->world, create_velocity_e, Velocity, {0, 0});
    
    ecs_filter_t *f = ecs_filter(it->world, {
        .terms = {{ ecs_id(Velocity) }}
    });

    ecs_iter_t fit = ecs_filter_iter(it->world, f);
    test_bool(true, ecs_filter_next(&fit));
    test_int(fit.count, 1);
    test_uint(fit.entities[0], create_velocity_e);
    test_bool(false, ecs_filter_next(&fit));

    ecs_filter_fini(f);

    ecs_defer_begin(it->world);
    no_staging_create_velocity_invoked ++;
}

static int32_t read_position_invoked = 0;
static void ReadPosition(ecs_iter_t *it) {
    ecs_os_ainc(&read_position_invoked);
}

static int32_t read_velocity_invoked = 0;
static void ReadVelocity(ecs_iter_t *it) {
    ecs_os_ainc(&read_velocity_invoked);
}

void Pipeline_inactive_system_after_no_staging_system_no_defer_w_filter(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreatePosition,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }},
        .callback = ReadPosition
    });

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 1);
    test_int(read_position_invoked, 1);

    test_assert(create_position_e != 0);
    test_assert(ecs_has(world, create_position_e, Position));
    ecs_delete(world, create_position_e);
    
    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 2);
    test_int(read_position_invoked, 2);

    ecs_fini(world);
}

void Pipeline_inactive_system_after_no_staging_system_no_defer_w_filter_w_no_staging_at_end(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreatePosition,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }},
        .callback = ReadPosition
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = SysA,
        .no_readonly = true
    });

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 1);
    test_int(read_position_invoked, 1);

    test_assert(create_position_e != 0);
    test_assert(ecs_has(world, create_position_e, Position));
    ecs_delete(world, create_position_e);
    
    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 2);
    test_int(read_position_invoked, 2);

    ecs_fini(world);
}

void Pipeline_inactive_system_after_2_no_staging_system_no_defer_w_filter(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_COMPONENT_DEFINE(world, Velocity);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreatePosition,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreateVelocity,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }},
        .callback = ReadPosition
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Velocity) }},
        .callback = ReadVelocity
    });

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 1);
    test_int(no_staging_create_velocity_invoked, 1);
    test_int(read_position_invoked, 1);
    test_int(read_velocity_invoked, 1);

    test_assert(create_position_e != 0);
    test_assert(ecs_has(world, create_position_e, Position));
    ecs_delete(world, create_position_e);
    
    test_assert(create_velocity_e != 0);
    test_assert(ecs_has(world, create_velocity_e, Velocity));
    ecs_delete(world, create_velocity_e);

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 2);
    test_int(no_staging_create_velocity_invoked, 2);
    test_int(read_position_invoked, 2);
    test_int(read_velocity_invoked, 2);

    ecs_fini(world);
}

static
void Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_COMPONENT_DEFINE(world, Velocity);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreatePosition,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }},
        .callback = ReadPosition,
        .multi_threaded = true
    });

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
        ecs_set_threads(world, 2);
    }

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 1);
    test_int(read_position_invoked, 1);

    test_assert(create_position_e != 0);
    test_assert(ecs_has(world, create_position_e, Position));
    ecs_delete(world, create_position_e);

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 2);
    test_int(read_position_invoked, 2);

    ecs_fini(world);
}

void Pipeline_inactive_multithread_system_after_no_staging_system_no_defer(void) {
    Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_internal(false);
}

void Pipeline_inactive_multithread_tasks_system_after_no_staging_system_no_defer(void) {
    Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_internal(true);
}

static
void Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_w_no_staging_at_end_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_COMPONENT_DEFINE(world, Velocity);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = NoStagingSystemCreatePosition,
        .no_readonly = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .query.filter.terms = {{ ecs_id(Position) }},
        .callback = ReadPosition,
        .multi_threaded = true
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate )}
        }),
        .callback = ReadPosition,
        .no_readonly = true
    });

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
        ecs_set_threads(world, 2);
    }

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 1);
    test_int(read_position_invoked, 2);

    test_assert(create_position_e != 0);
    test_assert(ecs_has(world, create_position_e, Position));
    ecs_delete(world, create_position_e);

    ecs_progress(world, 0);
    test_int(no_staging_create_position_invoked, 2);
    test_int(read_position_invoked, 4);

    ecs_fini(world);
}

void Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_w_no_staging_at_end(void) {
    Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_w_no_staging_at_end_internal(false);
}

void Pipeline_inactive_multithread_tasks_system_after_no_staging_system_no_defer_w_no_staging_at_end(void) {
    Pipeline_inactive_multithread_system_after_no_staging_system_no_defer_w_no_staging_at_end_internal(true);
}

static int add_id_invoked = 0;
static int foo_system_invoked = 0;

static void AddId(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    ecs_id_t id = ecs_field_id(it, 1);

    int i;
    for (i = 0; i < it->count; i++) {
        ecs_add_id(world, it->entities[i], id);
    }

    add_id_invoked ++;
}

static void FooSystem(ecs_iter_t *it) {
    foo_system_invoked ++;
}

static
void Pipeline_multi_threaded_pipeline_change_w_only_singlethreaded_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, FooSystem,   EcsOnUpdate, Position);
    ECS_SYSTEM(world, AddId,       EcsOnUpdate, !Position, Tag);

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
        ecs_set_threads(world, 2);
    }

    ecs_entity_t e = ecs_new(world, Tag);

    ecs_progress(world, 0);

    test_assert(ecs_has(world, e, Position));
    test_int(foo_system_invoked, 0);
    test_int(add_id_invoked, 1);

    ecs_fini(world);
}

void Pipeline_multi_threaded_pipeline_change_w_only_singlethreaded(void) {
    Pipeline_multi_threaded_pipeline_change_w_only_singlethreaded_internal(false);
}

void Pipeline_multi_threaded_tasks_pipeline_change_w_only_singlethreaded(void) {
    Pipeline_multi_threaded_pipeline_change_w_only_singlethreaded_internal(true);
}

static void SetPosition(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);

    for (int i = 0; i < it->count; i ++) {
        p[i].x = 10;
        p[i].y = 20;
    }
}

void Pipeline_sync_after_not_out_for_out(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, AddId,       EcsOnUpdate, [out] !Position, Tag);
    ECS_SYSTEM(world, SetPosition, EcsOnUpdate, [out] Position);

    ecs_entity_t e = ecs_new(world, Tag);

    const ecs_world_info_t *wi = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(wi->merge_count_total, 2);

    const Position *p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 10);
    test_int(p->y, 20);

    ecs_fini(world);
}

void Pipeline_pair_wildcard_read_after_staged_write(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_TAG(world, Rel);
    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, AddId, EcsOnUpdate, [out] !(Rel, Position), Tag);
    ECS_SYSTEM(world, SysA,  EcsOnUpdate, [in]  (Rel, *));

    ecs_new(world, Tag);

    const ecs_world_info_t *wi = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(wi->merge_count_total, 2);

    test_int(add_id_invoked, 1);
    test_int(sys_a_invoked, 1);

    ecs_fini(world);
}

static int add_pair_invoked = 0;

static void AddPair(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    ecs_id_t id = ecs_field_id(it, 1);

    int i;
    for (i = 0; i < it->count; i++) {
        ecs_add_pair(world, it->entities[i], 
            ECS_PAIR_FIRST(id), ecs_id(Position));
    }

    add_pair_invoked ++;
}

void Pipeline_pair_read_after_staged_wildcard_write(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_TAG(world, Rel);
    ECS_COMPONENT_DEFINE(world, Position);

    ECS_SYSTEM(world, AddPair, EcsOnUpdate, [out] !(Rel, *), Tag);
    ECS_SYSTEM(world, SysA,    EcsOnUpdate, [in]  (Rel, Position));

    ecs_new(world, Tag);

    const ecs_world_info_t *wi = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(wi->merge_count_total, 2);

    test_int(add_pair_invoked, 1);
    test_int(sys_a_invoked, 1);

    ecs_fini(world);
}

void Pipeline_no_sync_after_pair_wildcard_read_after_unmatching_staged_write(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_TAG(world, Rel);
    ECS_TAG(world, Rel2);
    ECS_COMPONENT_DEFINE(world, Position);

    ECS_SYSTEM(world, AddPair, EcsOnUpdate, [out] !(Rel2, Position), Tag);
    ECS_SYSTEM(world, SysA,    EcsOnUpdate, [in] (Rel, *));

    ecs_new(world, Tag);

    const ecs_world_info_t *wi = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(wi->merge_count_total, 1);

    test_int(add_pair_invoked, 1);
    test_int(sys_a_invoked, 0);

    test_int(1, ecs_count_id(world, ecs_pair(Rel2, ecs_id(Position))));

    ecs_fini(world);
}

void Pipeline_no_merge_after_from_nothing_w_default_inout(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, SysA, EcsOnUpdate, Tag, Position());
    ECS_SYSTEM(world, SysB, EcsOnUpdate, Tag, Position);

    ecs_new(world, Tag);
    
    const ecs_world_info_t *wi = ecs_get_world_info(world);

    ecs_progress(world, 0);

    test_int(wi->merge_count_total, 1);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 0);

    ecs_fini(world);
}

static int sys_add_tag_invoked = 0;
static int sys_no_readonly_invoked = 0;

static void sys_add_tag(ecs_iter_t *it) {
  ecs_new(it->world, TagA);
  ecs_new(it->world, TagB);
  sys_add_tag_invoked ++;
  test_assert(sys_a_invoked == 0);
}

static void sys_no_readonly(ecs_iter_t *it) {
    test_assert(sys_a_invoked == 1);
    test_assert(it->world == it->real_world);
    test_assert(!ecs_stage_is_readonly(it->real_world));
    sys_no_readonly_invoked ++;
}

void Pipeline_on_merge_activate_system_before_merge(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG_DEFINE(world, TagA);
    ECS_TAG_DEFINE(world, TagB);

    // system is annotated with TagA but writes both TagA, TagB
    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter.terms = {{ TagA, .inout = EcsOut, .src.flags = EcsIsEntity }},
        .callback = sys_add_tag
    });

    // no merge inserted between systems, but system activates after merge
    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter.terms = {{ TagB, .inout = EcsIn }},
        .callback = SysA
    });

    // read TagA, causes insertion of merge
    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter.terms = {{ TagA, .inout = EcsIn }},
        .no_readonly = true,
        .callback = sys_no_readonly
    });

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_add_tag_invoked, 1);
    test_int(sys_no_readonly_invoked, 1);

    test_int(ecs_count(world, TagA), 1);
    test_int(ecs_count(world, TagB), 1);

    ecs_fini(world);
}

void Pipeline_disable_phase(void) {
    ecs_world_t *world = ecs_init();

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsPostUpdate) }}),
        .callback = SysB
    });

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);

    ecs_enable(world, EcsOnUpdate, false);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 2);

    ecs_enable(world, EcsOnUpdate, true);
    ecs_enable(world, EcsPostUpdate, false);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 2);

    ecs_enable(world, EcsPostUpdate, true);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 3);
    test_int(sys_b_invoked, 3);

    ecs_fini(world);
}

void Pipeline_disable_parent(void) {
    ecs_world_t *world = ecs_init();

    ecs_entity_t p1 = ecs_new_id(world);
    ecs_entity_t p2 = ecs_new_id(world);

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_childof(p1), ecs_dependson(EcsOnUpdate) }}),
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_childof(p2), ecs_dependson(EcsPostUpdate) }}),
        .callback = SysB
    });

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);

    ecs_enable(world, p1, false);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 2);

    ecs_enable(world, p1, true);
    ecs_enable(world, p2, false);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 2);

    ecs_enable(world, p2, true);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 3);
    test_int(sys_b_invoked, 3);

    ecs_fini(world);
}

static int no_staging_add_position_invoked = 0;

static void NoReadonlyAddPosition(ecs_iter_t *it) {
    test_assert(it->world == it->real_world);
    no_staging_add_position_invoked ++;

    ecs_entity_t e = ecs_new_id(it->world);
    ecs_add(it->world, e, Position);
}

static
void Pipeline_multi_threaded_no_staging_w_add_after_read_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
        ecs_set_threads(world, 2);
    }

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter = { .terms = {{ ecs_id(Position) }}},
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = NoReadonlyAddPosition,
        .no_readonly = true
    });

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 0);
    test_int(no_staging_add_position_invoked, 1);
    test_int(ecs_count(world, Position), 1);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(no_staging_add_position_invoked, 2);
    test_int(ecs_count(world, Position), 2);

    ecs_fini(world);
}

void Pipeline_multi_threaded_no_staging_w_add_after_read(void) {
    Pipeline_multi_threaded_no_staging_w_add_after_read_internal(false);
}

void Pipeline_multi_threaded_tasks_no_staging_w_add_after_read(void) {
    Pipeline_multi_threaded_no_staging_w_add_after_read_internal(true);
}

void Pipeline_1_startup_system(void) {
    ecs_world_t *world = ecs_init();

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = SysB
    });

    test_int(stats->merge_count_total, 0);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);

    test_int(stats->merge_count_total, 2);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 2);

    test_int(stats->merge_count_total, 3);
    test_assert(sys_a_delta_time == 0);

    ecs_fini(world);
}

void Pipeline_2_startup_systems(void) {
    ecs_world_t *world = ecs_init();

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .callback = SysB
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = SysC
    });

    test_int(stats->merge_count_total, 0);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    test_int(stats->merge_count_total, 2);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 2);

    test_int(stats->merge_count_total, 3);
    test_assert(sys_a_delta_time == 0);
    test_assert(sys_b_delta_time == 0);

    ecs_fini(world);
}

void Pipeline_2_startup_phases(void) {
    ecs_world_t *world = ecs_init();

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_entity_t AfterStart = ecs_new_w_id(world, EcsPhase);
    ecs_add_pair(world, AfterStart, EcsDependsOn, EcsOnStart);

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(AfterStart) }}),
        .callback = SysB
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = SysC
    });

    test_int(stats->merge_count_total, 0);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    test_int(stats->merge_count_total, 2);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 2);

    test_int(stats->merge_count_total, 3);
    test_assert(sys_a_delta_time == 0);
    test_assert(sys_b_delta_time == 0);

    ecs_fini(world);
}

void Pipeline_2_startup_systems_w_merge(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    
    ecs_new(world, Position);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .query.filter = { .terms = {{ ecs_id(Position), .src.flags = EcsIsEntity, .inout = EcsOut }}},
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnStart) }}),
        .query.filter = { .terms = {{ ecs_id(Position) }}},
        .callback = SysB
    });

    ecs_system(world, {
        .entity = ecs_entity(world, { .add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = SysC
    });

    test_int(stats->merge_count_total, 0);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);

    test_int(stats->merge_count_total, 3);

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 2);

    test_int(stats->merge_count_total, 4);

    ecs_fini(world);
}

void Pipeline_inactive_last_system_merge_count(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);
    ECS_TAG(world, TagC);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagA },
            { TagB, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagB },
            { TagC, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysB
    });

    test_int(stats->merge_count_total, 0);

    ecs_new(world, TagA);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 0);

    test_int(stats->merge_count_total, 1);

    ecs_fini(world);
}

void Pipeline_inactive_middle_system_merge_count(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);
    ECS_TAG(world, TagC);
    ECS_TAG(world, TagD);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagA },
            { TagB, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagB },
            { TagC, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysC
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagC },
            { TagD, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysD
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagD }
        },
        .callback = SysB
    });

    test_int(stats->merge_count_total, 0);

    ecs_new(world, TagA);
    ecs_new(world, TagD);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 0);
    test_int(sys_d_invoked, 0);

    test_int(stats->merge_count_total, 2);

    ecs_fini(world);
}

static
void CreateEntity(ecs_iter_t *it) {
    ecs_id_t tag = ecs_field_id(it, 1);
    ecs_new_w_id(it->world, tag);
}

void Pipeline_last_no_readonly_system_merge_count(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);
    ECS_TAG(world, TagC);
    ECS_TAG(world, TagD);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagA },
            { TagB, .src.flags = EcsIsEntity, .inout = EcsOut }
        },
        .callback = SysA
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { ecs_dependson(EcsOnUpdate) }
        }),
        .query.filter.terms = {
            { TagD }
        },
        .callback = CreateEntity,
        .no_readonly = true
    });

    test_int(stats->merge_count_total, 0);

    ecs_new(world, TagA);
    ecs_new(world, TagD);

    test_int(1, ecs_count(world, TagD));

    ecs_progress(world, 0);

    test_int(2, ecs_count(world, TagD));

    test_int(stats->merge_count_total, 1);

    ecs_fini(world);
}

void Pipeline_2_pipelines_1_system(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Phase1);
    ECS_TAG(world, Phase2);

    ecs_entity_t p1 = ecs_pipeline(world, {
        .query = {
            .filter.terms = {
                { .id = EcsSystem },
                { .id = Phase1 }
            }
        }
    });

    ecs_entity_t p2 = ecs_pipeline(world, {
        .query = {
            .filter.terms = {
                { .id = EcsSystem },
                { .id = Phase2 }
            }
        }
    });

    ecs_system(world, {
        .entity = ecs_entity(world, {
            .add = { Phase1 }
        }),
        .callback = SysA
    });

    ecs_progress(world, 0);
    test_int(sys_a_invoked, 0);

    ecs_set_pipeline(world, p1);
    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);

    ecs_set_pipeline(world, p2);
    ecs_progress(world, 0);
    test_int(sys_a_invoked, 1);

    ecs_fini(world);
}

void Pipeline_builtin_pipeline_w_self_system_term(void) {
    ecs_world_t *world = ecs_init();

    ecs_entity_t pipeline = ecs_get_pipeline(world);
    test_assert(pipeline != 0);

    const EcsPoly *p = ecs_get_pair(world, pipeline, EcsPoly, EcsQuery);
    test_assert(p != NULL);
    test_assert(ecs_poly_is(p->poly, ecs_query_t));

    ecs_query_t *q = p->poly;
    const ecs_filter_t *f = ecs_query_get_filter(q);
    test_assert(f != NULL);
    test_assert((f->terms[0].src.flags & EcsTraverseFlags) == EcsSelf);

    ecs_fini(world);
}

void Pipeline_custom_pipeline_w_self_system_term(void) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);

    ecs_entity_t pipeline = ecs_pipeline(world, {
        .query.filter.terms = {
            { EcsSystem },
            { Tag }
        }
    });
    test_assert(pipeline != 0);

    const EcsPoly *p = ecs_get_pair(world, pipeline, EcsPoly, EcsQuery);
    test_assert(p != NULL);
    test_assert(ecs_poly_is(p->poly, ecs_query_t));

    ecs_query_t *q = p->poly;
    const ecs_filter_t *f = ecs_query_get_filter(q);
    test_assert(f != NULL);
    test_assert((f->terms[0].src.flags & EcsTraverseFlags) == EcsSelf);

    ecs_fini(world);
}

void Pipeline_switch_from_threads_to_tasks(void) {
    ecs_world_t* world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);

    ecs_system(world, {
        .entity = ecs_entity(world, {.add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter = {.terms = {{ ecs_id(Position) }}},
        .callback = SysA
        });

    ecs_system(world, {
        .entity = ecs_entity(world, {.add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = NoReadonlyAddPosition,
        .no_readonly = true
        });

    ecs_set_threads(world, 2);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 0);
    test_int(no_staging_add_position_invoked, 1);
    test_int(ecs_count(world, Position), 1);

    ecs_set_task_threads(world, 2);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(no_staging_add_position_invoked, 2);
    test_int(ecs_count(world, Position), 2);

    ecs_fini(world);
}

void Pipeline_switch_from_tasks_to_threads(void) {
    ecs_world_t* world = ecs_init();

    ECS_COMPONENT_DEFINE(world, Position);

    ecs_system(world, {
        .entity = ecs_entity(world, {.add = { ecs_dependson(EcsOnUpdate) }}),
        .query.filter = {.terms = {{ ecs_id(Position) }}},
        .callback = SysA
        });

    ecs_system(world, {
        .entity = ecs_entity(world, {.add = { ecs_dependson(EcsOnUpdate) }}),
        .callback = NoReadonlyAddPosition,
        .no_readonly = true
        });

    ecs_set_threads(world, 2);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 0);
    test_int(no_staging_add_position_invoked, 1);
    test_int(ecs_count(world, Position), 1);

    ecs_set_task_threads(world, 2);

    ecs_progress(world, 0);

    test_int(sys_a_invoked, 1);
    test_int(no_staging_add_position_invoked, 2);
    test_int(ecs_count(world, Position), 2);

    ecs_fini(world);
}

void Pipeline_run_pipeline_multithreaded_internal(bool task_threads) {
    ecs_world_t *world = ecs_init();

    ECS_TAG(world, Tag);
    ECS_COMPONENT(world, Position);

    ecs_entity_t s1 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysA", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysA,
        .multi_threaded = true
    });
    ecs_entity_t s2 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysB", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysB
    });
    ecs_entity_t s3 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysC", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysC
    });
    ecs_entity_t s4 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysD", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysD,
        .multi_threaded = true
    });
    ecs_entity_t s5 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysE", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysE,
        .multi_threaded = true
    });
    ecs_entity_t s6 = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "SysF", .add = {Tag, ecs_pair(EcsDependsOn, EcsOnUpdate)} }),
        .query.filter.expr = "Position",
        .callback = SysF
    });

    ECS_PIPELINE(world, P, flecs.system.System, flecs.pipeline.Phase(cascade(DependsOn)), Tag);

    test_assert(s1 != 0);
    test_assert(s2 != 0);
    test_assert(s3 != 0);
    test_assert(s4 != 0);
    test_assert(s5 != 0);
    test_assert(s6 != 0);

    ecs_new(world, Position);
    ecs_new(world, Position);

    if (task_threads)
    {
        ecs_set_task_threads(world, 2);
    }
    else
    {
    	ecs_set_threads(world, 2);
    }

    ecs_run_pipeline(world, P, 1);

    test_int(sys_a_invoked, 2);
    test_int(sys_b_invoked, 1);
    test_int(sys_c_invoked, 1);
    test_int(sys_d_invoked, 2);
    test_int(sys_e_invoked, 2);
    test_int(sys_f_invoked, 1);

    ecs_pipeline_stats_t stats = {0};
    test_bool(ecs_pipeline_stats_get(world, P, &stats), true);

    test_int(ecs_vec_count(&stats.systems), 10);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[0], s1);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[1], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[2], s2);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[3], s3);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[4], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[5], s4);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[6], s5);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[7], 0); /* merge */
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[8], s6);
    test_int(ecs_vec_get_t(&stats.systems, ecs_entity_t, 0)[9], 0); /* merge */

    ecs_pipeline_stats_fini(&stats);

    ecs_fini(world);
}

void Pipeline_run_pipeline_multithreaded(void) {
    Pipeline_run_pipeline_multithreaded_internal(false);
}

void Pipeline_run_pipeline_multithreaded_tasks(void) {
    Pipeline_run_pipeline_multithreaded_internal(true);
}

void Pipeline_pipeline_init_no_terms(void) {
    install_test_abort();

    ecs_world_t *world = ecs_init();

    test_expect_abort();
    ecs_pipeline(world, { 0 });
}

void Pipeline_pipeline_init_no_system_term(void) {
    install_test_abort();

    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);

    test_expect_abort();
    ecs_pipeline(world, {
        .query.filter.terms = {{ ecs_id(Position) }}
    });
}
