/*****************************************************************************
 * clock/clock.c: test for the vlc clock
 *****************************************************************************
 * Copyright (C) 2023 VLC authors, VideoLAN and Videolabs SAS
 * Copyright (C) 2024 Videolabs
 *
 * Authors: Thomas Guillem <thomas@gllm.fr>
 *          Alexandre Janniaux <ajanni@videolabs.io>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <vlc_common.h>
#include <vlc_tick.h>
#include <vlc_es.h>
#include <vlc_tracer.h>

#include "../../../src/clock/clock.h"

#include <vlc/vlc.h>
#include "../../libvlc/test.h"
#include "../../../lib/libvlc_internal.h"

#define MODULE_NAME test_clock_clock
#undef VLC_DYNAMIC_PLUGIN
#include <vlc_plugin.h>
#include <vlc_vector.h>

/* Define a builtin module for mocked parts */
const char vlc_module_name[] = MODULE_STRING;

struct clock_ctx;

struct clock_scenario
{
    const char *name;
    const char *desc;

    enum {
        CLOCK_SCENARIO_UPDATE,
        CLOCK_SCENARIO_RUN,
    } type;

    vlc_tick_t stream_start;
    vlc_tick_t system_start; /* VLC_TICK_INVALID for vlc_tick_now() */

    union {
        struct {
            vlc_tick_t duration;
            vlc_tick_t stream_increment; /* VLC_TICK_INVALID for manual increment */
            unsigned video_fps;
            vlc_tick_t total_drift_duration; /* VLC_TICK_INVALID for non-drift test */

            double coeff_epsilon; /* Valid for lowprecision/burst checks */
            void (*update)(const struct clock_ctx *ctx, size_t index,
                           vlc_tick_t *system, vlc_tick_t stream);
            void (*check)(const struct clock_ctx *ctx, size_t update_count,
                          vlc_tick_t expected_system_end, vlc_tick_t stream_end);
        };
        struct {
            void (*run)(const struct clock_ctx *ctx);
            bool disable_jitter;
        };
    };
};

struct clock_ctx
{
    vlc_clock_main_t *mainclk;
    vlc_clock_t *master;
    vlc_clock_t *slave;
    vlc_clock_t *input;

    vlc_tick_t system_start;
    vlc_tick_t stream_start;

    const struct clock_scenario *scenario;
};

enum tracer_event_type {
    TRACER_EVENT_TYPE_UPDATE,
    TRACER_EVENT_TYPE_RENDER_VIDEO,
    TRACER_EVENT_TYPE_STATUS,
};

enum tracer_event_status  {
    TRACER_EVENT_STATUS_RESET_USER,
    TRACER_EVENT_STATUS_RESET_BADSOURCE,
};

struct tracer_event
{
    enum tracer_event_type type;

    union {
        struct {
            double coeff;
            vlc_tick_t offset;
        } update;

        struct {
            vlc_tick_t play_date;
            vlc_tick_t pts;
        } render_video;

        enum tracer_event_status status;
    };
};

struct tracer_ctx
{
    vlc_tick_t forced_ts;

    struct VLC_VECTOR(struct tracer_event) events;
};

static struct tracer_ctx tracer_ctx;

static void tracer_ctx_Reset(struct tracer_ctx *ctx)
{
    ctx->events.size = 0;
    ctx->forced_ts = VLC_TICK_INVALID;
}

static void tracer_ctx_Init(struct tracer_ctx *ctx)
{
    tracer_ctx_Reset(ctx);
    vlc_vector_init(&ctx->events);
};

static void tracer_ctx_Destroy(struct tracer_ctx *ctx)
{
    vlc_vector_destroy(&ctx->events);
}

static void tracer_ctx_PushUpdate(struct tracer_ctx *ctx,
                                  double coeff, vlc_tick_t offset)
{
    struct tracer_event event = {
        .type = TRACER_EVENT_TYPE_UPDATE,
        .update = {
            .coeff = coeff,
            .offset = offset,
        },
    };
    bool ret = vlc_vector_push(&ctx->events, event);
    assert(ret);
}

static void tracer_ctx_PushRenderVideo(struct tracer_ctx *ctx,
                                       vlc_tick_t play_date, vlc_tick_t pts)
{
    struct tracer_event event = {
        .type = TRACER_EVENT_TYPE_RENDER_VIDEO,
        .render_video = {
            .play_date = play_date,
            .pts = pts,
        },
    };
    bool ret = vlc_vector_push(&ctx->events, event);
    assert(ret);
}

static void tracer_ctx_PushStatus(struct tracer_ctx *ctx,
                                  enum tracer_event_status status)
{
    struct tracer_event event = {
        .type = TRACER_EVENT_TYPE_STATUS,
        .status = status,
    };
    bool ret = vlc_vector_push(&ctx->events, event);
    assert(ret);
}

static void TracerTrace(void *opaque, vlc_tick_t ts,
                        const struct vlc_tracer_trace *trace)
{
    struct vlc_tracer *libvlc_tracer = opaque;

    const struct vlc_tracer_entry *cursor = trace->entries;

    bool is_render = false, is_render_video = false, is_status = false;
    unsigned nb_update = 0;
    double coeff = 0.0f;
    vlc_tick_t offset = VLC_TICK_INVALID;
    vlc_tick_t render_video_pts = VLC_TICK_INVALID;
    enum tracer_event_status status = 0;

    while (cursor->key != NULL)
    {
        const struct vlc_tracer_entry *entry = cursor;
        cursor++;

        switch (entry->type)
        {
            case VLC_TRACER_INT:
                if (!is_render)
                    continue;
                assert(!is_status);

                if (strcmp(entry->key, "offset") == 0)
                {
                    nb_update++;
                    offset = VLC_TICK_FROM_NS(entry->value.integer);
                }
                else if (strcmp(entry->key, "render_pts") == 0)
                    render_video_pts = VLC_TICK_FROM_NS(entry->value.integer);
                break;
            case VLC_TRACER_DOUBLE:
                if (!is_render)
                    continue;
                assert(!is_status);

                if (strcmp(entry->key, "coeff") == 0)
                {
                    nb_update++;
                    coeff = entry->value.double_;
                }
                break;
            case VLC_TRACER_STRING:
                if (strcmp(entry->key, "type") == 0)
                {
                    if (strcmp(entry->value.string, "RENDER") == 0)
                        is_render = true;
                }
                else if (strcmp(entry->key, "id") == 0)
                {
                    if (strcmp(entry->value.string, "video") == 0)
                        is_render_video= true;
                }

                if (!is_render)
                    continue;
                /* Assert that there is no "reset_bad_source" */
                if (strcmp(entry->key, "event") == 0)
                {
                    is_status = true;
                    if (strcmp(entry->value.string, "reset_bad_source") == 0)
                        status = TRACER_EVENT_STATUS_RESET_BADSOURCE;
                    else if (strcmp(entry->value.string, "reset_user") == 0)
                        status = TRACER_EVENT_STATUS_RESET_USER;
                    else
                        vlc_assert_unreachable();
                }

                break;
            default: vlc_assert_unreachable();
        }
    }

    if (libvlc_tracer != NULL)
    {
        /* If the user specified a tracer, forward directly to it after faking
         * the system ts */
        assert(tracer_ctx.forced_ts != VLC_TICK_INVALID);

        /* Use the original ts for the "render_pts" entry */
        vlc_tick_t tracer_ts = render_video_pts != VLC_TICK_INVALID ?
                               ts : tracer_ctx.forced_ts;
        (vlc_tracer_TraceWithTs)(libvlc_tracer, tracer_ts, trace);
    }

    if (!is_render)
        return;

    if (is_status)
        tracer_ctx_PushStatus(&tracer_ctx, status);
    else if (nb_update > 0)
    {
        assert(nb_update == 2);
        tracer_ctx_PushUpdate(&tracer_ctx, coeff, offset);
    }
    else if (is_render_video && render_video_pts != VLC_TICK_INVALID)
        tracer_ctx_PushRenderVideo(&tracer_ctx, ts, render_video_pts);
}

/* Used to check for some trace value and hack the ts to the user tracer */
static const struct vlc_tracer_operations *
OpenTracer(vlc_object_t *obj, void **restrict sysp)
{
    static const struct vlc_tracer_operations ops =
    {
        .trace = TracerTrace,
    };

    *sysp = vlc_object_get_tracer(obj);

    return &ops;
}

vlc_module_begin()
    set_callback(OpenTracer)
    set_capability("tracer", 0)
vlc_module_end()

VLC_EXPORT const vlc_plugin_cb vlc_static_modules[] = {
    VLC_SYMBOL(vlc_entry),
    NULL
};

static void play_scenario(libvlc_int_t *vlc, struct vlc_tracer *tracer,
                          struct clock_scenario *scenario)
{
    fprintf(stderr, "[%s]: checking that %s\n", scenario->name, scenario->desc);

    assert((scenario->type == CLOCK_SCENARIO_UPDATE && scenario->update != NULL)
        || (scenario->type == CLOCK_SCENARIO_RUN && scenario->run != NULL));

    tracer_ctx_Reset(&tracer_ctx);

    struct vlc_logger *logger = vlc->obj.logger;

    vlc_clock_main_t *mainclk = vlc_clock_main_New(logger, tracer);
    assert(mainclk != NULL);

    vlc_clock_main_Lock(mainclk);
    vlc_clock_t *input = vlc_clock_main_CreateInputSlave(mainclk);
    assert(input != NULL);

    vlc_clock_t *master = vlc_clock_main_CreateMaster(mainclk, scenario->name,
                                                      NULL, NULL);
    assert(master != NULL);

    char *slave_name;
    int ret = asprintf(&slave_name, "%s/video", scenario->name);
    assert(ret != -1);
    vlc_clock_t *slave = vlc_clock_main_CreateSlave(mainclk, slave_name, VIDEO_ES,
                                                    NULL, NULL);
    assert(slave != NULL);
    if (scenario->type == CLOCK_SCENARIO_RUN && scenario->disable_jitter)
    {
        /* Don't add any delay for the first monotonic ref point  */
        vlc_clock_main_SetInputDejitter(mainclk, 0);
        vlc_clock_main_SetDejitter(mainclk, 0);
    }

    vlc_tick_t system_start = VLC_TICK_0 + VLC_TICK_FROM_MS(15000);
    vlc_tick_t stream_start = VLC_TICK_0 + VLC_TICK_FROM_MS(15000);
    if (scenario->type == CLOCK_SCENARIO_UPDATE)
    {
        system_start = scenario->system_start;
        stream_start = scenario->stream_start;
    }

    vlc_clock_Start(input, system_start, stream_start);

    if (scenario->type == CLOCK_SCENARIO_UPDATE)
        vlc_clock_Start(master, system_start, stream_start);
    vlc_clock_main_Unlock(mainclk);

    const struct clock_ctx ctx = {
        .mainclk = mainclk,
        .input = input,
        .master = master,
        .slave = slave,
        .scenario = scenario,
        .system_start = system_start,
        .stream_start = stream_start,
    };

    if (scenario->type == CLOCK_SCENARIO_RUN)
    {
        scenario->run(&ctx);
        goto end;
    }

    vlc_tick_t stream_end = scenario->stream_start + scenario->duration;
    vlc_tick_t stream = scenario->stream_start;
    if (scenario->system_start == VLC_TICK_INVALID)
        scenario->system_start = vlc_tick_now();
    vlc_tick_t system = scenario->system_start;
    vlc_tick_t expected_system = scenario->system_start;

    vlc_tick_t video_increment = 0;
    vlc_tick_t video_ts = scenario->stream_start;
    if (scenario->video_fps != 0)
        video_increment = vlc_tick_rate_duration(scenario->video_fps);

    tracer_ctx.forced_ts = expected_system;
    size_t index = 0;

    for(; stream < stream_end;
        stream += scenario->stream_increment, ++index)
    {
        scenario->update(&ctx, index, &system, stream);

        vlc_tick_t video_system = expected_system;

        if (video_increment > 0)
        {
            while (video_system < expected_system + scenario->stream_increment)
            {
                vlc_clock_Lock(ctx.slave);
                vlc_tick_t play_date =
                    vlc_clock_ConvertToSystem(ctx.slave, video_system, video_ts,
                                              1.0f, NULL);
                vlc_clock_Update(ctx.slave, play_date, video_ts, 1.0f);
                vlc_clock_Unlock(ctx.slave);
                video_system += video_increment;
                video_ts += video_increment;
            }
        }

        expected_system += scenario->stream_increment;
        tracer_ctx.forced_ts = expected_system;
    }

    if (scenario->check != NULL)
    {
        assert(expected_system == scenario->system_start + scenario->duration);

        scenario->check(&ctx, index, expected_system, stream_end);
    }

end:
    vlc_clock_Delete(master);
    vlc_clock_Delete(slave);
    vlc_clock_Delete(input);
    free(slave_name);
    vlc_clock_main_Delete(mainclk);
}

static void run_scenarios(int argc, const char *argv[],
                          struct clock_scenario *scenarios, size_t count)
{
    /* Skip argv[0] */
    argc--;
    argv++;

    const char *scenario_name = NULL;
    vlc_tick_t forced_duration = VLC_TICK_INVALID;
    if (argc > 0)
    {
        /* specific test run from the user with custom options */
        scenario_name = argv[0];
        argc--;
        argv++;

        if (argc > 0)
        {
            long long val = atoll(argv[0]);
            if (val > 0)
            {
                forced_duration = val;
                argc--;
                argv++;
            }
        }
    }

    libvlc_instance_t *vlc = libvlc_new(argc, argv);
    assert(vlc != NULL);

    tracer_ctx_Init(&tracer_ctx);

    struct vlc_tracer *tracer = vlc_tracer_Create(VLC_OBJECT(vlc->p_libvlc_int),
                                                  MODULE_STRING);
    assert(tracer != NULL);

    for (size_t i = 0; i < count; ++i)
    {
        if (scenario_name == NULL
         || strcmp(scenario_name, scenarios[i].name) == 0)
        {
            struct clock_scenario *scenario = &scenarios[i];
            if (forced_duration != VLC_TICK_INVALID)
                scenario->duration = forced_duration;
            play_scenario(vlc->p_libvlc_int, tracer, scenario);
        }
    }

    vlc_tracer_Destroy(tracer);

    tracer_ctx_Destroy(&tracer_ctx);

    libvlc_release(vlc);
}

static void normal_update(const struct clock_ctx *ctx, size_t index,
                          vlc_tick_t *system, vlc_tick_t stream)
{
    (void) index;
    const struct clock_scenario *scenario = ctx->scenario;

    vlc_clock_Lock(ctx->master);
    vlc_tick_t drift =
        vlc_clock_Update(ctx->master, *system, stream, 1.0f);
    vlc_clock_Unlock(ctx->master);
    /* The master can't drift */
    assert(drift == VLC_TICK_INVALID);

    *system += scenario->stream_increment;
}

static void check_no_event_error(size_t expected_update_count)
{
    /* assert that there is no error/status */
    assert(tracer_ctx.events.size > 0);

    size_t update_count = 0;
    for (size_t i = 0; i < tracer_ctx.events.size; ++i)
    {
        struct tracer_event event = tracer_ctx.events.data[i];
        switch (event.type)
        {
            case TRACER_EVENT_TYPE_UPDATE:
                update_count++;
                break;
            case TRACER_EVENT_TYPE_RENDER_VIDEO:
                break;
            case TRACER_EVENT_TYPE_STATUS:
                switch (event.status)
                {
                    case TRACER_EVENT_STATUS_RESET_USER:
                    case TRACER_EVENT_STATUS_RESET_BADSOURCE:
                        assert("clock reset not expected" == NULL);
                        break;
                }
                break;
        }
    }

    assert(update_count == expected_update_count);
}

static void normal_check(const struct clock_ctx *ctx, size_t update_count,
                         vlc_tick_t expected_system_end,
                         vlc_tick_t stream_end)
{
    const struct clock_scenario *scenario = ctx->scenario;

    check_no_event_error(update_count);
    vlc_tick_t last_video_date = VLC_TICK_INVALID;
    vlc_tick_t video_increment = vlc_tick_rate_duration(scenario->video_fps);

    for (size_t i = 0; i < tracer_ctx.events.size; ++i)
    {
        struct tracer_event event = tracer_ctx.events.data[i];

        switch (event.type)
        {
            case TRACER_EVENT_TYPE_UPDATE:
                assert(event.update.coeff == 1.0f);
                assert(event.update.offset == 0);
                break;
            case TRACER_EVENT_TYPE_RENDER_VIDEO:
                if (last_video_date != VLC_TICK_INVALID)
                    assert(event.render_video.play_date - last_video_date == video_increment);
                last_video_date = event.render_video.play_date;
                break;
            default:
                break;
        }
    }

    vlc_clock_Lock(ctx->slave);
    vlc_tick_t converted =
        vlc_clock_ConvertToSystem(ctx->slave, expected_system_end,
                                  stream_end, 1.0f, NULL);
    vlc_clock_Unlock(ctx->slave);
    fprintf(stderr, "%s:%d: converted=%" PRId64 " == expected_system_end=%" PRId64 "\n",
            __func__, __LINE__, converted, expected_system_end);
    assert(converted == expected_system_end);
}

static void lowprecision_update(const struct clock_ctx *ctx, size_t index,
                                vlc_tick_t *system, vlc_tick_t stream)
{
    (void) index;
    const struct clock_scenario *scenario = ctx->scenario;

    vlc_tick_t base_system = stream - scenario->stream_start
                           + scenario->system_start;
    /* random imprecision (seed based on stream) */
    srand(stream);
    vlc_tick_t imprecision = rand() % VLC_TICK_FROM_MS(5);
    *system = base_system + imprecision;

    vlc_clock_Lock(ctx->master);
    vlc_tick_t drift =
        vlc_clock_Update(ctx->master, *system, stream, 1.0f);
    vlc_clock_Unlock(ctx->master);
    /* The master can't drift */
    assert(drift == VLC_TICK_INVALID);
}

static void lowprecision_check(const struct clock_ctx *ctx, size_t update_count,
                               vlc_tick_t expected_system_end,
                               vlc_tick_t stream_end)
{
    const struct clock_scenario *scenario = ctx->scenario;
    (void) expected_system_end; (void) stream_end;

    check_no_event_error(update_count);

    for (size_t i = 0; i < tracer_ctx.events.size; ++i)
    {
        struct tracer_event event = tracer_ctx.events.data[i];
        switch (event.type)
        {
            case TRACER_EVENT_TYPE_UPDATE:
                assert(fabs(event.update.coeff - 1.0f) <= scenario->coeff_epsilon);
                break;
            default:
                break;
        }
    }
}

static void drift_update(const struct clock_ctx *ctx, size_t index,
                         vlc_tick_t *system, vlc_tick_t stream)
{
    (void) index;
    const struct clock_scenario *scenario = ctx->scenario;

    vlc_clock_Lock(ctx->master);
    vlc_tick_t drift =
        vlc_clock_Update(ctx->master, *system, stream, 1.0f);
    vlc_clock_Unlock(ctx->master);
    /* The master can't drift */
    assert(drift == VLC_TICK_INVALID);

    *system += scenario->stream_increment;

    /* Simulate 1us drift every stream_increment */
    if (scenario->total_drift_duration > 0)
        *system += VLC_TICK_FROM_US(1);
    else
        *system -= VLC_TICK_FROM_US(1);
}

static void drift_check(const struct clock_ctx *ctx, size_t update_count,
                        vlc_tick_t expected_system_end,
                        vlc_tick_t stream_end)
{
    const struct clock_scenario *scenario = ctx->scenario;

    check_no_event_error(update_count);

    vlc_clock_Lock(ctx->slave);
    vlc_tick_t converted =
        vlc_clock_ConvertToSystem(ctx->slave, expected_system_end,
                                  stream_end, 1.0f, NULL);
    vlc_clock_Unlock(ctx->slave);

    assert(converted - expected_system_end == scenario->total_drift_duration);
}

static void drift_sudden_update(const struct clock_ctx *ctx, size_t index,
                                vlc_tick_t *system, vlc_tick_t stream)
{
    (void) index;
    const struct clock_scenario *scenario = ctx->scenario;

    vlc_clock_Lock(ctx->slave);
    vlc_tick_t drift =
        vlc_clock_Update(ctx->master, *system, stream, 1.0f);
    vlc_clock_Unlock(ctx->slave);
    /* The master can't drift */
    assert(drift == VLC_TICK_INVALID);

    *system += scenario->stream_increment;

    if (stream - scenario->stream_start >= scenario->duration * 3 / 4)
    {
        /* Simulate a sudden high drift */
        *system += VLC_TICK_FROM_US(4);
    }
}

static void pause_common(const struct clock_ctx *ctx, vlc_clock_t *updater)
{
    const vlc_tick_t system_start = ctx->system_start;
    const vlc_tick_t pause_duration = VLC_TICK_FROM_MS(20);
    vlc_tick_t system = system_start;

    vlc_clock_Lock(updater);
    vlc_clock_Start(updater, ctx->system_start, ctx->stream_start);
    vlc_clock_Update(updater, system, ctx->stream_start, 1.0f);
    vlc_clock_Unlock(updater);

    {
        vlc_clock_Lock(ctx->slave);
        vlc_tick_t converted = vlc_clock_ConvertToSystem(ctx->slave, system, ctx->stream_start, 1.0f, NULL);
        assert(converted == system);
        vlc_clock_Unlock(ctx->slave);
    }

    system += VLC_TICK_FROM_MS(10);

    vlc_clock_main_Lock(ctx->mainclk);
    vlc_clock_main_ChangePause(ctx->mainclk, system, true);
    vlc_clock_main_Unlock(ctx->mainclk);

    system += pause_duration;

    vlc_clock_main_Lock(ctx->mainclk);
    vlc_clock_main_ChangePause(ctx->mainclk, system, false);
    vlc_clock_main_Unlock(ctx->mainclk);
    system += 1;

    vlc_clock_Lock(ctx->slave);
    vlc_tick_t converted = vlc_clock_ConvertToSystem(ctx->slave, system, ctx->stream_start, 1.0f, NULL);
    vlc_clock_Unlock(ctx->slave);
    assert(converted == system_start + pause_duration);
}

static void master_pause_run(const struct clock_ctx *ctx)
{
    pause_common(ctx, ctx->master);
}

static void monotonic_pause_run(const struct clock_ctx *ctx)
{
    pause_common(ctx, ctx->slave);
}

static void convert_paused_common(const struct clock_ctx *ctx, vlc_clock_t *updater)
{
    const vlc_tick_t system_start = ctx->system_start;
    vlc_tick_t system = system_start;

    vlc_clock_main_Lock(ctx->mainclk);
    vlc_clock_main_SetFirstPcr(ctx->mainclk, ctx->system_start, ctx->stream_start);
    vlc_clock_main_Unlock(ctx->mainclk);

    vlc_clock_Lock(updater);
    vlc_clock_Start(updater, ctx->system_start, ctx->stream_start);
    vlc_clock_Update(updater, ctx->system_start, ctx->stream_start, 1.0f);
    vlc_clock_Unlock(updater);

    system += VLC_TICK_FROM_MS(10);

    vlc_clock_main_Lock(ctx->mainclk);
    vlc_clock_main_ChangePause(ctx->mainclk, system, true);
    vlc_clock_main_Unlock(ctx->mainclk);
    system += 100;

    vlc_clock_Lock(ctx->slave);
    vlc_tick_t converted = vlc_clock_ConvertToSystem(ctx->slave, system, ctx->stream_start, 1.0f, NULL);
    vlc_clock_Unlock(ctx->slave);

    fprintf(stderr, "%s:%d converted=%" PRId64 " == system_start=%" PRId64 "\n",
            __func__, __LINE__, converted, system_start);
    assert(converted == system_start);
}

static void master_convert_paused_run(const struct clock_ctx *ctx)
{
    convert_paused_common(ctx, ctx->master);
}

static void monotonic_convert_paused_run(const struct clock_ctx *ctx)
{
    convert_paused_common(ctx, ctx->slave);
}

static void contexts_run(const struct clock_ctx *ctx)
{
    vlc_tick_t converted;
    vlc_tick_t system = ctx->system_start;
    vlc_tick_t stream_context0 = 1;
    uint32_t clock_id;

    vlc_clock_main_Lock(ctx->mainclk);

    /* Initial SetFirstPcr, that will initialise the default and main context */
    vlc_clock_main_SetFirstPcr(ctx->mainclk, system, stream_context0);

    /* Check that the converted point is valid */
    converted = vlc_clock_ConvertToSystem(ctx->slave, system, stream_context0,
                                          1.0f, &clock_id);
    fprintf(stderr, "%s:%d: clock_id=%" PRIu32 " == 0\n", __func__, __LINE__, clock_id);
    assert(clock_id == 0);
    fprintf(stderr, "%s:%d: converted=%"PRId64 " == system=%" PRId64 "\n",
            __func__, __LINE__, converted, system);
    assert(converted == system);
    vlc_clock_Update(ctx->slave, system, stream_context0, 1.0f);

    /* Discontinuity from 1us to 30 sec */
    vlc_tick_t system_context0 = system;
    vlc_tick_t stream_context1 = VLC_TICK_FROM_SEC(30);
    system += VLC_TICK_FROM_MS(100);
    vlc_clock_main_SetFirstPcr(ctx->mainclk, system, stream_context1);

    /* Check that we can use the new context (or new origin) */
    converted = vlc_clock_ConvertToSystem(ctx->slave, system, stream_context1,
                                          1.0f, &clock_id);
    fprintf(stderr, "%s:%d: clock_id=%" PRIu32 " == 1\n", __func__, __LINE__, clock_id);
    assert(clock_id == 1);
    fprintf(stderr, "%s:%d: converted=%"PRId64 " == system=%" PRId64 "\n", __func__, __LINE__, converted, system);
    assert(converted == system);

    /* Check that we can still use the old context when converting a point
     * closer to the original context */
    converted = vlc_clock_ConvertToSystem(ctx->slave, system,
                                          VLC_TICK_FROM_MS(10) + stream_context0,
                                          1.0f, &clock_id);
    fprintf(stderr, "%s:%d: clock_id=%" PRIu32 " == 0\n", __func__, __LINE__, clock_id);
    assert(clock_id == 0);
    fprintf(stderr, "%s:%d: converted=%"PRId64 " == system=%" PRId64 "\n",
            __func__, __LINE__, converted, system_context0 + VLC_TICK_FROM_MS(10));
    assert(converted == system_context0 + VLC_TICK_FROM_MS(10));

    /* Update on the newest context will cause previous contexts to be removed */
    vlc_clock_Update(ctx->slave, system, stream_context1, 1.0f);

    /* Discontinuity back to 1us */
    system += VLC_TICK_FROM_MS(100);
    vlc_tick_t stream_context2 = 1;
    vlc_clock_main_SetFirstPcr(ctx->mainclk, system, stream_context2);

    /* Check that we can use the new context (or new origin) */
    converted = vlc_clock_ConvertToSystem(ctx->slave, system, stream_context2,
                                          1.0f, &clock_id);
    fprintf(stderr, "%s:%d: clock_id=%" PRIu32 " == 2\n", __func__, __LINE__, clock_id);
    assert(clock_id == 2);
    fprintf(stderr, "%s:%d: converted=%"PRId64 " == system=%" PRId64 "\n",
            __func__, __LINE__, converted, system);
    assert(converted == system);
    /* Update on the newest context will cause previous contexts to be removed */
    vlc_clock_Update(ctx->slave, system, stream_context2, 1.0f);

    /* Check that the same conversion will output a different result now that
     * the old contexts are removed */
    system += VLC_TICK_FROM_MS(100);
    converted = vlc_clock_ConvertToSystem(ctx->slave, system, stream_context1, 1.0f,
                                          &clock_id);
    fprintf(stderr, "%s:%d: clock_id=%" PRIu32 " == 2\n", __func__, __LINE__, clock_id);
    assert(clock_id == 2);
    fprintf(stderr, "%s:%d: converted=%"PRId64 " == system=%" PRId64 "\n",
            __func__, __LINE__, converted, system_context0);
    assert(converted != system_context0);

    vlc_clock_main_Unlock(ctx->mainclk);
}

#define VLC_TICK_12H VLC_TICK_FROM_SEC(12 * 60 * 60)
#define VLC_TICK_2H VLC_TICK_FROM_SEC(2 * 60 * 60)
#define DEFAULT_STREAM_INCREMENT VLC_TICK_FROM_MS(100)

#define INIT_SYSTEM_STREAM_TIMING(duration_, increment_, video_fps_) \
    .stream_start = VLC_TICK_0 + VLC_TICK_FROM_MS(31000000), \
    .system_start = VLC_TICK_0, \
    .duration = duration_, \
    .stream_increment = increment_, \
    .video_fps = video_fps_

static struct clock_scenario clock_scenarios[] = {
{
    .name = "normal",
    .desc = "normal update has a coeff of 1.0f",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_2H, DEFAULT_STREAM_INCREMENT, 60),
    .update = normal_update,
    .check = normal_check,
},
{
    .name = "lowprecision",
    .desc = "low precision update has a coeff near 1.0f",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_2H, DEFAULT_STREAM_INCREMENT, 60),
    .coeff_epsilon = 0.005,
    .update = lowprecision_update,
    .check = lowprecision_check,
},
{
    .name = "drift_72",
    .desc = "a drift of 72ms in 2h is handled",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_2H, DEFAULT_STREAM_INCREMENT, 60),
    .total_drift_duration = VLC_TICK_FROM_MS(72),
    .update = drift_update,
    .check = drift_check,
},
{
    .name = "drift_-72",
    .desc = "a drift of -72ms in 2h is handled",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_2H, DEFAULT_STREAM_INCREMENT, 60),
    .total_drift_duration = -VLC_TICK_FROM_MS(72),
    .update = drift_update,
    .check = drift_check,
},
{
    .name = "drift_432",
    .desc = "a drift of 432ms in 12h is handled",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_12H, DEFAULT_STREAM_INCREMENT, 0),
    .total_drift_duration = VLC_TICK_FROM_MS(432),
    .update = drift_update,
    .check = drift_check,
},
{
    .name = "drift_-432",
    .desc = "a drift of -432ms in 12h is handled",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_12H, DEFAULT_STREAM_INCREMENT, 0),
    .total_drift_duration = -VLC_TICK_FROM_MS(432),
    .update = drift_update,
    .check = drift_check,
},
{
    .name = "drift_sudden",
    .desc = "a sudden drift is handled",
    .type = CLOCK_SCENARIO_UPDATE,
    INIT_SYSTEM_STREAM_TIMING(VLC_TICK_12H, DEFAULT_STREAM_INCREMENT, 0),
    .total_drift_duration = VLC_TICK_FROM_MS(432),
    .update = drift_sudden_update,
    .check = drift_check,
},
{
    .name = "master_pause",
    .desc = "pause + resume is delaying the next conversion",
    .type = CLOCK_SCENARIO_RUN,
    .run = master_pause_run,
    .system_start = VLC_TICK_FROM_MS(100),
    .stream_start = VLC_TICK_FROM_MS(1000),
},
{
    .name = "monotonic_pause",
    .desc = "pause + resume is delaying the next conversion",
    .type = CLOCK_SCENARIO_RUN,
    .run = monotonic_pause_run,
    .disable_jitter = true,
    .system_start = VLC_TICK_FROM_MS(100),
    .stream_start = VLC_TICK_FROM_MS(1000),
},
{
    .name = "master_convert_paused",
    .desc = "it is possible to convert ts while paused",
    .type = CLOCK_SCENARIO_RUN,
    .run = master_convert_paused_run,
    .system_start = VLC_TICK_FROM_MS(100),
    .stream_start = VLC_TICK_FROM_MS(1000),
},
{
    .name = "monotonic_convert_paused",
    .desc = "it is possible to convert ts while paused",
    .type = CLOCK_SCENARIO_RUN,
    .run = monotonic_convert_paused_run,
    .disable_jitter = true,
    .system_start = VLC_TICK_FROM_MS(100),
    .stream_start = VLC_TICK_FROM_MS(1000),
},
{
    .name = "contexts",
    .desc = "switching contexts is handled",
    .type = CLOCK_SCENARIO_RUN,
    .run = contexts_run,
    .disable_jitter = true,
    .system_start = VLC_TICK_FROM_MS(100),
    .stream_start = VLC_TICK_FROM_MS(1000),
},
};

int main(int argc, const char *argv[])
{
    test_init();
    run_scenarios(argc, argv, clock_scenarios, ARRAY_SIZE(clock_scenarios));

    return EXIT_SUCCESS;
}
