#include <api.h>
#include <stdlib.h>

void World_setup(void) {
    ecs_log_set_level(-3);
}

static
void Move(ecs_iter_t *it) {
    Position *pos = ecs_field(it, Position, 1);
    Velocity *vel = ecs_field(it, Velocity, 2);
    probe_iter(it);

    int row;
    for (row = 0; row < it->count; row ++) {
        Position *p = &pos[row];
        Velocity *v = &vel[row];
        p->x += v->x * it->delta_time;
        p->y += v->y * it->delta_time;
    }
}

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

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

    ECS_ENTITY(world, e1, Position, Velocity);

    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_set(world, e1, Position, {0, 0});
    ecs_set(world, e1, Velocity, {1, 2});

    ecs_progress(world, 0);

    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, Move);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.c[0][1], ecs_id(Velocity));
    test_int(ctx.s[0][0], 0);
    test_int(ctx.s[0][1], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_assert(p->x != 0);
    test_assert(p->y != 0);

    ecs_fini(world);
}

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

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

    ECS_ENTITY(world, e1, Position, Velocity);

    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_set(world, e1, Position, {0, 0});
    ecs_set(world, e1, Velocity, {1, 2});

    ecs_progress(world, 2);

    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, Move);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.c[0][1], ecs_id(Velocity));
    test_int(ctx.s[0][0], 0);
    test_int(ctx.s[0][1], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    ecs_fini(world);
}

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

    ecs_set_entity_range(world, 5000, 0);

    ecs_entity_t e = ecs_new(world, 0);
    test_int(e, 5000);

    ecs_fini(world);
}

void World_entity_range_offset_out_of_range(void) {
    install_test_abort();

    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);

    ecs_enable_range_check(world, true);
    ecs_set_entity_range(world, 2000, 0);

    test_expect_abort();

    ecs_add(world, 1500, Position);

    ecs_fini(world);
}

void World_entity_range_limit_out_of_range(void) {
    install_test_abort();

    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);

    ecs_enable_range_check(world, true);
    ecs_set_entity_range(world, 0, 2000);

    test_expect_abort();

    ecs_add(world, 2500, Position);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_ensure(world, 4999);

    ecs_enable_range_check(world, false);
    ecs_set_entity_range(world, 5000, 10000);

    /* Validate that range is being used when issuing new ids */
    ecs_entity_t e = ecs_new(world, 0);
    test_int(e, 5000);

    /* Validate that application does not abort when changing out of range */
    ecs_entity_t e2 = ecs_set(world, 4999, Position, {10, 20});
    test_int(e2, 4999);
    test_assert( ecs_has(world, e2, Position));
    
    const Position *p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 10);
    test_int(p->y, 20);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_enable_range_check(world, true);
    ecs_set_entity_range(world, 5000, 10000);

    ecs_entity_t e = ecs_new(world, 0);
    test_assert(e != 0);
    test_assert(e == 5000);

    ecs_delete(world, e);

    e = ecs_new(world, 0);
    test_assert(e != 0);
    test_assert((uint32_t)e == 5000);

    ecs_fini(world);
}

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

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

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);
    test_assert(e < 1000);

    ecs_set_entity_range(world, 1000, 1500);

    ecs_readonly_begin(world);
    ecs_world_t *stage = ecs_get_stage(world, 0);
    ecs_add(stage, e, Velocity);
    ecs_readonly_end(world);

    ecs_fini(world);
}

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

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

    ecs_set_entity_range(world, 500, 1000);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e == 500);

    ecs_readonly_begin(world);
    ecs_world_t *stage = ecs_get_stage(world, 0);
    ecs_add(stage, e, Velocity);
    ecs_readonly_end(world);

    ecs_fini(world);
}

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

    int i;
    for (i = 0; i < it->count; i ++) {
        test_expect_abort();
        ecs_add(it->world, 1001, Velocity);
    }
}

void World_entity_range_add_out_of_range_staged(void) {
    install_test_abort();

    ecs_world_t *world = ecs_mini();

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

    ecs_enable_range_check(world, true);
    ecs_set_entity_range(world, 500, 1000);

    /* Dummy entity to invoke the system */
    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e == 500);

    ecs_readonly_begin(world);
    ecs_world_t *stage = ecs_get_stage(world, 0);
    ecs_add(stage, e, Velocity);
    ecs_readonly_end(world);

    ecs_fini(world);
}

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

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

    ecs_progress(world, 1);

    test_int(stats->frame_count_total, 1);

    ecs_progress(world, 1);

    test_int(stats->frame_count_total, 2);

    ecs_fini(world);
}

static int32_t malloc_count;

static
void *test_malloc(ecs_size_t size) {
    malloc_count ++;
    return malloc(size);
}

static
void *test_calloc(ecs_size_t size) {
    malloc_count ++;
    return calloc(size, 1);
}

static
void *test_realloc(void *old_ptr, ecs_size_t size) {
    malloc_count ++;
    return realloc(old_ptr, size);
}

void World_dim(void) {
    ecs_os_set_api_defaults();
    ecs_os_api_t os_api = ecs_os_api;
    os_api.malloc_ = test_malloc;
    os_api.calloc_ = test_calloc;
    os_api.realloc_ = test_realloc;
    ecs_os_set_api(&os_api);

    ecs_world_t *world = ecs_mini();

    ECS_COMPONENT(world, Position);

    /* Create single entity so that the table exists. This makes the allocation
     * counts more predictable, as new_w_count won't trigger table creation */
    ecs_new(world, Position);

    ecs_dim(world, 1100);

    malloc_count = 0;

    ecs_bulk_new(world, Position, 500);

    test_int(malloc_count, 2);

    malloc_count = 0;

    ecs_bulk_new(world, Position, 500);

    test_int(malloc_count, 2);

    ecs_fini(world);
}

static
void TOnLoad(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 0);
        p[i].x ++;
    }
}

static
void TPostLoad(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 1);
        p[i].x ++;
    }
}

static
void TPreUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 2);
        p[i].x ++;
    }
}

static
void TOnUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 3);
        p[i].x ++;
    }
}

static
void TOnValidate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 4);
        p[i].x ++;
    }
}

static
void TPostUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 5);
        p[i].x ++;
    }
}

static
void TPreStore(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 6);
        p[i].x ++;
    }
}

static
void TOnStore(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 7);
        p[i].x ++;
    }
}

static
void TManual(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    int i;
    for (i = 0; i < it->count; i ++) {
        test_int(p[i].x, 8);
        p[i].x ++;
    }
}

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, TOnLoad, EcsOnLoad, Position);
    ECS_SYSTEM(world, TPostLoad, EcsPostLoad, Position);
    ECS_SYSTEM(world, TPreUpdate, EcsPreUpdate, Position);
    ECS_SYSTEM(world, TOnUpdate, EcsOnUpdate, Position);
    ECS_SYSTEM(world, TOnValidate, EcsOnValidate, Position);
    ECS_SYSTEM(world, TPostUpdate, EcsPostUpdate, Position);
    ECS_SYSTEM(world, TPreStore, EcsPreStore, Position);
    ECS_SYSTEM(world, TOnStore, EcsOnStore, Position);
    ECS_SYSTEM(world, TManual, 0, Position);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);

    ecs_set(world, e, Position, {0, 0});

    ecs_progress(world, 1);

    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 8);

    ecs_run(world, TManual, 0, NULL);

    test_int(p->x, 9);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);

    ecs_set(world, e, Position, {0, 0});

    ECS_SYSTEM(world, TOnLoad, EcsOnLoad, Position);
    ECS_SYSTEM(world, TPostLoad, EcsPostLoad, Position);
    ECS_SYSTEM(world, TPreUpdate, EcsPreUpdate, Position);
    ECS_SYSTEM(world, TOnUpdate, EcsOnUpdate, Position);
    ECS_SYSTEM(world, TOnValidate, EcsOnValidate, Position);
    ECS_SYSTEM(world, TPostUpdate, EcsPostUpdate, Position);
    ECS_SYSTEM(world, TPreStore, EcsPreStore, Position);
    ECS_SYSTEM(world, TOnStore, EcsOnStore, Position);
    ECS_SYSTEM(world, TManual, 0, Position);

    ecs_progress(world, 1);

    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 8);

    ecs_run(world, TManual, 0, NULL);

    test_int(p->x, 9);

    ecs_fini(world);
}

static
void TMergeOnLoad(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergePostLoad(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergePreUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergeOnUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergeOnValidate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergePostUpdate(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergePreStore(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergeOnStore(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

static
void TMergeManual(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    ecs_id_t ecs_id(Position) = ecs_field_id(it, 1);

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

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, TMergeOnLoad, EcsOnLoad, Position, [out] Position());
    ECS_SYSTEM(world, TMergePostLoad, EcsPostLoad, Position, [out] Position());
    ECS_SYSTEM(world, TMergePreUpdate, EcsPreUpdate, Position, [out] Position());
    ECS_SYSTEM(world, TMergeOnUpdate, EcsOnUpdate, Position, [out] Position());
    ECS_SYSTEM(world, TMergeOnValidate, EcsOnValidate, Position, [out] Position());
    ECS_SYSTEM(world, TMergePostUpdate, EcsPostUpdate, Position, [out] Position());
    ECS_SYSTEM(world, TMergePreStore, EcsPreStore, Position, [out] Position());
    ECS_SYSTEM(world, TMergeOnStore, EcsOnStore, Position, [out] Position());
    ECS_SYSTEM(world, TMergeManual, 0, Position);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);

    ecs_set(world, e, Position, {0, 0});

    ecs_progress(world, 1);

    const Position *p = ecs_get(world, e, Position);
    test_int(p->x, 8);

    ecs_run(world, TMergeManual, 0, NULL);

    p = ecs_get(world, e, Position);
    test_int(p->x, 9);

    ecs_fini(world);
}

static
void TimeCheck(ecs_iter_t *it) {
    test_assert(it->delta_time > 0);
}

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, TimeCheck, EcsOnLoad, Position);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);

    int i = 0;
    for (i = 0; i < 1000; i ++) {
        ecs_progress(world, 0);
    }

    ecs_fini(world);
}

void World_control_fps(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, TimeCheck, EcsOnLoad, Position);

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);

    double start, now = 0;
    ecs_set_target_fps(world, 20);

    /* Run a few times to give the code an opportunity to calibrate */
    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);
    ecs_progress(world, 0);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run for one second */
    int count = 0;
    do {    
        ecs_progress(world, 0);
        if (!count) {
            start = stats->delta_time;
        }

        now += stats->delta_time;
        count ++;
    } while ((now - start) < 1.0);

    /* CI can be unpredictable, just make sure it's in the right ballpark */
    test_assert(count >= 15);
    test_assert(count < 25);

    ecs_fini(world);
}

static
void busy_wait(float wait_time) {
    ecs_time_t start, t;
    ecs_os_get_time(&start);

    do {
        t = start;
    } while (ecs_time_measure(&t) < wait_time);
}

static
void BusySystem(ecs_iter_t *it) {
    /* Spend 14msec doing something */
    busy_wait(0.014);
}

void World_control_fps_busy_system(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    ECS_SYSTEM(world, BusySystem, EcsOnUpdate, 0);

    double start, now = 0;
    ecs_set_target_fps(world, 20);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run for one second */
    int count = 0;
    do {    
        ecs_progress(world, 0);
        if (!count) {
            start = stats->delta_time;
        }

        now += stats->delta_time;
        count ++;
    } while ((now - start) < 1.0);

    /* FPS control relies on sleep, which relies on the OS scheduler. Therefore
     * pick a wide enough range to avoid tests failing at random. */
    test_assert(count >= 15);
    test_assert(count < 25);

    ecs_fini(world);
}

void World_control_fps_busy_app(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    double start, now = 0;
    ecs_set_target_fps(world, 20);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run for one second */
    int count = 0;
    do {    
        ecs_progress(world, 0);
        if (!count) {
            start = stats->delta_time;
        }

        now += stats->delta_time;
        count ++;

        busy_wait(0.014);
    } while ((now - start) < 1.0);

    /* FPS control relies on sleep, which relies on the OS scheduler. Therefore
     * pick a wide enough range to avoid tests failing at random. */
    test_assert(count >= 15);
    test_assert(count < 25);

    ecs_fini(world);
}

void World_measure_fps_vs_actual(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    ecs_set_target_fps(world, 60);

    /* Run 10 times, test if one second has passed */
    ecs_time_t t;
    ecs_os_get_time(&t);
    int32_t i;
    for (i = 0; i < 60; i ++) {
        ecs_progress(world, 0);
    }

    float elapsed = ecs_time_measure(&t);
    test_assert(elapsed >= 0.9);

    ecs_fini(world);
}

void World_measure_delta_time_vs_actual(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    ecs_set_target_fps(world, 60);
    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run 10 times, test if one second has passed */
    ecs_time_t t;
    float delta_time = 0;
    ecs_os_get_time(&t);
    int32_t i;
    for (i = 0; i < 60; i ++) {
        ecs_progress(world, 0);
        delta_time += stats->delta_time;
    }

    float elapsed = ecs_time_measure(&t);
    test_assert(delta_time - elapsed < 0.1);
    test_assert(elapsed >= 0.9);    

    ecs_fini(world);
}

static
void RandomSystem(ecs_iter_t *it) {
    /* wait at most 16msec */
    float rnd_time = ((float)rand() / (float)RAND_MAX) * 0.016;
    busy_wait(rnd_time);
}

void World_control_fps_random_system(void) {
    test_is_flaky();

    ecs_world_t *world = ecs_init();

    ECS_SYSTEM(world, RandomSystem, EcsOnUpdate, 0);

    double start, now = 0;
    ecs_set_target_fps(world, 20);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run for one second */
    int count = 0;
    do {    
        ecs_progress(world, 0);
        if (!count) {
            start = stats->delta_time;
        }

        now += stats->delta_time;
        count ++;
    } while ((now - start) < 1.0);

    /* FPS control relies on sleep, which relies on the OS scheduler. Therefore
     * pick a wide enough range to avoid tests failing at random. */
    test_assert(count >= 15);
    test_assert(count < 25);

    ecs_fini(world);
}

void World_control_fps_random_app(void) {
    test_is_flaky();
    
    ecs_world_t *world = ecs_init();

    double start, now = 0;
    ecs_set_target_fps(world, 20);

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    /* Run for one second */
    int count = 0;
    do {    
        ecs_progress(world, 0);
        if (!count) {
            start = stats->delta_time;
        }

        now += stats->delta_time;
        count ++;

        float rnd_time = ((float)rand() / (float)RAND_MAX) * 0.016;
        busy_wait(rnd_time);
    } while ((now - start) < 1.0);

    /* FPS control relies on sleep, which relies on the OS scheduler. Therefore
     * pick a wide enough range to avoid tests failing at random. */
    test_assert(count >= 15);
    test_assert(count < 25);

    ecs_fini(world);
}

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

    int32_t count = 0;

    while (ecs_progress(world, 0)) {
        test_int(count, 0);
        ecs_quit(world);
        count ++;
    }

    test_int(count, 1);

    ecs_fini(world);
}

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

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    test_int(stats->delta_time, 0);

    ecs_progress(world, 1.0);

    test_flt(stats->delta_time, 1.0);

    ecs_fini(world);
}

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

    const ecs_world_info_t *stats = ecs_get_world_info(world);

    test_int(stats->delta_time, 0);

    ecs_progress(world, 0);

    test_assert(stats->delta_time != 0);

    ecs_fini(world);
}

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

    test_assert(ecs_fini(world) == 0);

    world = ecs_init();

    test_assert(ecs_fini(world) == 0);
}

void World_recreate_world_w_component(void) {
    ecs_world_t *world = ecs_init();
    test_assert(world != NULL);

    {
        ECS_COMPONENT(world, Position);
        test_assert(ecs_id(Position) != 0);
    }

    test_assert(ecs_fini(world) == 0);

    {
        world = ecs_init();
        test_assert(world != NULL);

        ECS_COMPONENT(world, Position);
        test_assert(ecs_id(Position) != 0);

        test_assert(ecs_fini(world) == 0);
    }
}

void World_no_threading(void) {
    ecs_os_set_api_defaults();
    ecs_os_api_t os_api = ecs_os_api;
    os_api.mutex_new_ = NULL;
    ecs_os_set_api(&os_api);

    ecs_world_t *world = ecs_init();
    test_assert(world != NULL);
    ecs_fini(world);
}

void World_no_time(void) {
    ecs_os_set_api_defaults();
    ecs_os_api_t os_api = ecs_os_api;
    os_api.get_time_ = NULL;
    ecs_os_set_api(&os_api);

    ecs_world_t *world = ecs_init();
    test_assert(world != NULL);
    ecs_fini(world);
}

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

    ecs_entity_t e = ecs_new(world, 0);

    test_assert( ecs_has_id(world, e, EcsDisabled) == false);

    ecs_fini(world);
}

static int zero_time_scale_invoked = 0;

void ZeroTimeScale(ecs_iter_t *it) {
    test_assert(it->delta_time == 0.0);
    zero_time_scale_invoked ++;
}

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

    ECS_TAG(world, Tag);

    ecs_new_w_id(world, Tag);

    ecs_set_time_scale(world, 0);

    ECS_SYSTEM(world, ZeroTimeScale, EcsOnUpdate, Tag);

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

    const ecs_world_info_t *info = ecs_get_world_info(world);
    test_assert(info->delta_time == 0.0);

    test_int(zero_time_scale_invoked, 2);

    ecs_fini(world);
}

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

    ecs_query_t *q = ecs_query_new(world, "!(ChildOf, *)");
    ecs_iter_t it = ecs_query_iter(world, q);

    /* Make sure that the only entity in the root is the flecs module */
    test_assert(ecs_query_next(&it));
    test_int(it.count, 1);
    test_assert(it.entities[0] == EcsFlecs);

    /* Entity for the query */
    test_assert(ecs_query_next(&it));
    test_int(it.count, 1);
    test_assert(ecs_has_id(world, it.entities[0], EcsQuery));

    test_assert(!ecs_query_next(&it));

    ecs_fini(world);
}

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

    ecs_entity_t e = ecs_new_id(world);

    ecs_set_alias(world, e, "Foo");
    ecs_set_alias(world, e, "Foo");

    ecs_entity_t f = ecs_lookup(world, "Foo");
    test_assert(f == e);

    ecs_fini(world);
}

void World_register_alias_twice_different_entity(void) {
    install_test_abort();

    ecs_world_t *world = ecs_init();

    ecs_entity_t e = ecs_new_id(world);
    ecs_entity_t f = ecs_new_id(world);

    ecs_set_alias(world, e, "Foo");
    
    test_expect_abort();
    ecs_set_alias(world, f, "Foo");
}

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

    ecs_entity_t c = ecs_component_init(world, &(ecs_component_desc_t){
        .entity = ecs_entity(world, {
            .name = "flecs.core.Component",
            .symbol = "EcsComponent"
        }),
        .type.size = ECS_SIZEOF(EcsComponent),
        .type.alignment = ECS_ALIGNOF(EcsComponent)
    });

    test_assert(c == ecs_id(EcsComponent));

    ecs_fini(world);
}

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

    const ecs_world_info_t *info = ecs_get_world_info(world);
    int32_t old_empty_table_count = info->empty_table_count;

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Increase to 1 */
    test_int(deleted, 0);

    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Delete */
    test_assert(deleted != 0);
    test_int(info->empty_table_count + deleted, old_empty_table_count);

    ecs_fini(world);
}

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

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Increase to 1 */
    test_int(deleted, 0);

    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Delete */
    test_assert(deleted != 0);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);
    ecs_run_aperiodic(world, 0);

    const ecs_world_info_t *info = ecs_get_world_info(world);
    int32_t old_empty_table_count = info->empty_table_count;

    ecs_entity_t e = ecs_new(world, Tag);
    for (int i = 0; i < 1000; i ++) {
        ecs_add_id(world, e, ecs_new_id(world));
    }

    ecs_run_aperiodic(world, 0);
    test_int(info->empty_table_count, old_empty_table_count + 1000);

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Increase to 1 */
    test_int(deleted, 0);

    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0); /* Delete */
    test_assert(deleted != 0);
    test_assert(deleted >= 1000);

    test_assert(info->empty_table_count <= old_empty_table_count);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);
    ecs_run_aperiodic(world, 0);

    const ecs_world_info_t *info = ecs_get_world_info(world);
    int32_t old_empty_table_count = info->empty_table_count;

    ecs_entity_t e1 = ecs_new(world, TagA);
    for (int i = 0; i < 500; i ++) {
        ecs_add_id(world, e1, ecs_new_id(world));
    }

    ecs_entity_t e2 = ecs_new(world, TagB);
    for (int i = 0; i < 500; i ++) {
        ecs_add_id(world, e2, ecs_new_id(world));
    }

    ecs_run_aperiodic(world, 0);
    test_int(info->empty_table_count, old_empty_table_count + 1000);

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, TagA, 0, 1, 0, 0); /* Increase to 1 */
    test_int(deleted, 0);

    deleted = ecs_delete_empty_tables(world, TagA, 0, 1, 0, 0); /* Delete */
    test_assert(deleted != 0);
    test_assert(deleted >= 500);
    test_assert(deleted < 1000);

    test_assert((info->empty_table_count - 500) <= old_empty_table_count);

    ecs_fini(world);
}

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

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

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, TagA);
    ecs_add(world, e, TagB);

    ecs_remove(world, e, TagA);
    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0);
    test_assert(deleted == 0);
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0);
    test_assert(deleted != 0);
    ecs_add(world, e, TagA);

    test_assert( ecs_has(world, e, TagA));
    test_assert( ecs_has(world, e, TagB));

    ecs_fini(world);
}

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

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

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, TagA);
    ecs_add(world, e, TagB);

    ecs_remove(world, e, TagA);
    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    ecs_add(world, e, TagA);

    test_assert( ecs_has(world, e, TagA));
    test_assert( ecs_has(world, e, TagB));

    ecs_fini(world);
}

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

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

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, Position);
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));

    ecs_remove(world, e, Velocity);
    test_assert( ecs_has(world, e, Position));
    test_assert( !ecs_has(world, e, Velocity));

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0);
    test_assert(deleted == 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));

    deleted = ecs_delete_empty_tables(world, 0, 0, 1, 0, 0);
    test_assert(deleted != 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));
    
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));
    test_assert( ecs_has(world, e, Velocity));

    ecs_fini(world);
}

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

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

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, Position);
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));

    ecs_remove(world, e, Velocity);
    test_assert( ecs_has(world, e, Position));
    test_assert( !ecs_has(world, e, Velocity));

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));

    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));
    
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));
    test_assert( ecs_has(world, e, Velocity));

    ecs_fini(world);
}

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

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

    ecs_set_hooks(world, Position, { 
        .ctor = ecs_default_ctor 
    });
    ecs_set_hooks(world, Velocity, { 
        .ctor = ecs_default_ctor 
    });

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, Position);
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));

    ecs_remove(world, e, Velocity);
    test_assert( ecs_has(world, e, Position));
    test_assert( !ecs_has(world, e, Velocity));

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));

    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    test_bool(true, ecs_is_alive(world, ecs_id(Position)));
    test_bool(true, ecs_is_alive(world, ecs_id(Velocity)));
    test_assert( ecs_has(world, e, Position));
    
    ecs_add(world, e, Velocity);

    test_assert( ecs_has(world, e, Position));
    test_assert( ecs_has(world, e, Velocity));

    ecs_fini(world);
}

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

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

    int32_t deleted;
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);
    deleted = ecs_delete_empty_tables(world, 0, 1, 0, 0, 0);
    test_assert(deleted == 0);

    ecs_entity_t e = ecs_new_id(world);
    ecs_add(world, e, TagA);
    ecs_add(world, e, TagB);

    test_assert(ecs_has(world, e, TagA));
    test_assert(ecs_has(world, e, TagB));

    ecs_fini(world);
}

static ECS_COMPONENT_DECLARE(Test);

typedef struct Test {
    uint32_t value;
} Test;

static int at_fini_test_invoked = 0;

static 
void at_fini_test(
    ecs_world_t* world,
    void* context) 
{
    test_assert(ecs_singleton_get_mut(world, Test) != NULL);
    at_fini_test_invoked = 1;
}

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

    ECS_COMPONENT_DEFINE(world, Test);
    ecs_singleton_set(world, Test, { 42 });

    ecs_atfini(world, at_fini_test, NULL);

    ecs_fini(world);

    test_int(at_fini_test_invoked, 1);
}

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

    ECS_COMPONENT(world, Position);

    const ecs_type_info_t *ti = ecs_get_type_info(world, ecs_id(Position));
    test_assert(ti != NULL);
    test_int(ti->size, ECS_SIZEOF(Position));
    test_int(ti->alignment, ECS_ALIGNOF(Position));
    test_uint(ti->component, ecs_id(Position));

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_delete_with(world, ecs_id(Position));

    const ecs_type_info_t *ti = ecs_get_type_info(world, ecs_id(Position));
    test_assert(ti != NULL);
    test_int(ti->size, ECS_SIZEOF(Position));
    test_int(ti->alignment, ECS_ALIGNOF(Position));
    test_uint(ti->component, ecs_id(Position));

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_delete_with(world, ecs_id(Position));

    ecs_entity_t e = ecs_new(world, Position);
    test_assert(e != 0);
    test_assert( ecs_has(world, e, Position));

    const ecs_type_info_t *ti = ecs_get_type_info(world, ecs_id(Position));
    test_assert(ti != NULL);
    test_int(ti->size, ECS_SIZEOF(Position));
    test_int(ti->alignment, ECS_ALIGNOF(Position));
    test_uint(ti->component, ecs_id(Position));

    ecs_fini(world);
}

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

    const ecs_world_info_t *info = ecs_get_world_info(world);
    test_assert(info->name_prefix == NULL);

    ecs_fini(world);
}

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

    int ctx;
    ecs_set_ctx(world, &ctx, NULL);
    test_assert(ecs_get_ctx(world) == &ctx);
    test_assert(ecs_get_binding_ctx(world) == NULL);

    ecs_fini(world);
}

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

    int ctx;
    ecs_set_binding_ctx(world, &ctx, NULL);
    test_assert(ecs_get_ctx(world) == NULL);
    test_assert(ecs_get_binding_ctx(world) == &ctx);

    ecs_fini(world);
}

static void ctx_free(void *ptr) {
    *(int*)ptr = 10;
}

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

    int ctx = 0;
    ecs_set_ctx(world, &ctx, ctx_free);
    test_assert(ecs_get_ctx(world) == &ctx);
    test_assert(ecs_get_binding_ctx(world) == NULL);
    test_int(ctx, 0);

    ecs_fini(world);

    test_int(ctx, 10);
}

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

    int ctx = 0;
    ecs_set_binding_ctx(world, &ctx, ctx_free);
    test_assert(ecs_get_ctx(world) == NULL);
    test_assert(ecs_get_binding_ctx(world) == &ctx);
    test_int(ctx, 0);

    ecs_fini(world);

    test_int(ctx, 10);
}
