/* GStreamer
 *
 * Copyright (C) 2014-2015 Sebastian Dröge <sebastian@centricular.com>
 * Copyright (C) 2015 Brijesh Singh <brijesh.ksingh@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/* TODO:
 * - start with pause, go to playing
 * - play, pause, play
 * - set uri in play/pause
 * - play/pause after eos
 * - seek in play/pause/stopped, after eos, back to 0, after duration
 * - http buffering
 */

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

#ifdef HAVE_VALGRIND
# include <valgrind/valgrind.h>
#endif

#include <libsoup/soup.h>

#include <gst/check/gstcheck.h>

#define fail_unless_equals_int(a, b)                                    \
G_STMT_START {                                                          \
  int first = a;                                                        \
  int second = b;                                                       \
  fail_unless(first == second,                                          \
    "'" #a "' (%d) is not equal to '" #b"' (%d)", first, second);       \
} G_STMT_END;

#define fail_unless_equals_uint64(a, b)                                 \
G_STMT_START {                                                          \
  guint64 first = a;                                                    \
  guint64 second = b;                                                   \
  fail_unless(first == second,                                          \
    "'" #a "' (%" G_GUINT64_FORMAT ") is not equal to '" #b"' (%"       \
    G_GUINT64_FORMAT ")", first, second);                               \
} G_STMT_END;

#define fail_unless_equals_double(a, b)                                 \
G_STMT_START {                                                          \
  double first = a;                                                     \
  double second = b;                                                    \
  fail_unless(first == second,                                          \
    "'" #a "' (%lf) is not equal to '" #b"' (%lf)", first, second);     \
} G_STMT_END;

#include <gst/play/play.h>

START_TEST (test_create_and_free)
{
  GstPlay *player;

  player = gst_play_new (NULL);
  fail_unless (player != NULL);
  g_object_unref (player);
}

END_TEST;

START_TEST (test_set_and_get_uri)
{
  GstPlay *player;
  gchar *uri;

  player = gst_play_new (NULL);

  fail_unless (player != NULL);

  gst_play_set_uri (player, "file:///path/to/a/file");
  uri = gst_play_get_uri (player);

  fail_unless (g_strcmp0 (uri, "file:///path/to/a/file") == 0);

  g_free (uri);
  g_object_unref (player);
}

END_TEST;

START_TEST (test_set_and_get_position_update_interval)
{
  GstPlay *player;
  guint interval = 0;
  GstStructure *config;

  player = gst_play_new (NULL);

  fail_unless (player != NULL);

  config = gst_play_get_config (player);
  gst_play_config_set_position_update_interval (config, 500);
  interval = gst_play_config_get_position_update_interval (config);
  fail_unless (interval == 500);
  gst_play_set_config (player, config);

  g_object_unref (player);
}

END_TEST;

typedef enum
{
  STATE_CHANGE_BUFFERING,
  STATE_CHANGE_DURATION_CHANGED,
  STATE_CHANGE_END_OF_STREAM,
  STATE_CHANGE_ERROR,
  STATE_CHANGE_WARNING,
  STATE_CHANGE_POSITION_UPDATED,
  STATE_CHANGE_STATE_CHANGED,
  STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED,
  STATE_CHANGE_MEDIA_INFO_UPDATED,
  STATE_CHANGE_SEEK_DONE,
  STATE_CHANGE_URI_LOADED,
} TestPlayerStateChange;

static const gchar *
test_play_state_change_get_name (TestPlayerStateChange change)
{
  switch (change) {
    case STATE_CHANGE_BUFFERING:
      return "buffering";
    case STATE_CHANGE_DURATION_CHANGED:
      return "duration-changed";
    case STATE_CHANGE_END_OF_STREAM:
      return "end-of-stream";
    case STATE_CHANGE_WARNING:
      return "warning";
    case STATE_CHANGE_ERROR:
      return "error";
    case STATE_CHANGE_POSITION_UPDATED:
      return "position-updated";
    case STATE_CHANGE_STATE_CHANGED:
      return "state-changed";
    case STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED:
      return "video-dimensions-changed";
    case STATE_CHANGE_MEDIA_INFO_UPDATED:
      return "media-info-updated";
    case STATE_CHANGE_SEEK_DONE:
      return "seek-done";
    case STATE_CHANGE_URI_LOADED:
      return "uri-loaded";
    default:
      g_assert_not_reached ();
      break;
  }
}

typedef struct _TestPlayerState TestPlayerState;
struct _TestPlayerState
{
  gint buffering_percent;
  guint64 position, duration, seek_done_position;
  gboolean end_of_stream, is_error, is_warning, seek_done;
  GstPlayState state;
  guint width, height;
  GstPlayMediaInfo *media_info;
  gchar *uri_loaded;
  GstClockTime last_position;
  gboolean done;
  GError *error;
  GstStructure *error_details;

  void (*test_callback) (GstPlay * player, TestPlayerStateChange change,
      TestPlayerState * old_state, TestPlayerState * new_state);
  gpointer test_data;
};

static void process_play_messages (GstPlay * player, TestPlayerState * state);

static void
test_play_state_change_debug (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  GST_DEBUG_OBJECT (player, "Changed %s:\n"
      "\tbuffering %d%% -> %d%%\n"
      "\tposition %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT "\n"
      "\tduration %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT "\n"
      "\tseek position %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT "\n"
      "\tend-of-stream %d -> %d\n"
      "\terror %d -> %d\n"
      "\tseek_done %d -> %d\n"
      "\tstate %s -> %s\n"
      "\twidth/height %d/%d -> %d/%d\n"
      "\tmedia_info %p -> %p\n"
      "\turi_loaded %s -> %s",
      test_play_state_change_get_name (change),
      old_state->buffering_percent, new_state->buffering_percent,
      GST_TIME_ARGS (old_state->position), GST_TIME_ARGS (new_state->position),
      GST_TIME_ARGS (old_state->duration), GST_TIME_ARGS (new_state->duration),
      GST_TIME_ARGS (old_state->seek_done_position),
      GST_TIME_ARGS (new_state->seek_done_position), old_state->end_of_stream,
      new_state->end_of_stream, old_state->is_error, new_state->is_error,
      old_state->seek_done, new_state->seek_done,
      gst_play_state_get_name (old_state->state),
      gst_play_state_get_name (new_state->state), old_state->width,
      old_state->height, new_state->width, new_state->height,
      old_state->media_info, new_state->media_info,
      old_state->uri_loaded, new_state->uri_loaded);
}

static void
test_play_state_reset (GstPlay * player, TestPlayerState * state)
{
  state->buffering_percent = 100;
  state->position = state->duration = state->seek_done_position = -1;
  state->end_of_stream = state->is_error = state->seek_done = FALSE;
  state->state = GST_PLAY_STATE_STOPPED;
  state->width = state->height = 0;
  if (state->media_info)
    g_object_unref (state->media_info);
  state->media_info = NULL;
  state->last_position = GST_CLOCK_TIME_NONE;
  state->done = FALSE;
  g_clear_pointer (&state->uri_loaded, g_free);
  g_clear_error (&state->error);
  gst_clear_structure (&state->error_details);
}

static GstPlay *
test_play_new (TestPlayerState * state)
{
  GstPlay *player;
  GstElement *playbin, *fakesink;

  player = gst_play_new (NULL);
  fail_unless (player != NULL);

  test_play_state_reset (player, state);

  playbin = gst_play_get_pipeline (player);
  fakesink = gst_element_factory_make ("fakesink", "audio-sink");
  g_object_set (fakesink, "sync", TRUE, NULL);
  g_object_set (playbin, "audio-sink", fakesink, NULL);
  fakesink = gst_element_factory_make ("fakesink", "video-sink");
  g_object_set (fakesink, "sync", TRUE, NULL);
  g_object_set (playbin, "video-sink", fakesink, NULL);
  gst_object_unref (playbin);

  return player;
}

static void
test_play_stopped_cb (GstPlay * player, TestPlayerStateChange change,
    TestPlayerState * old_state, TestPlayerState * new_state)
{
  if (new_state->state == GST_PLAY_STATE_STOPPED) {
    new_state->done = TRUE;
  }
}

static void
stop_player (GstPlay * player, TestPlayerState * state)
{
  if (state->state != GST_PLAY_STATE_STOPPED) {
    /* Make sure all pending operations are finished so the player won't be
     * appear as 'leaked' to leak detection tools. */
    state->test_callback = test_play_stopped_cb;
    gst_play_stop (player);
    state->done = FALSE;
    process_play_messages (player, state);
  }
  test_play_state_reset (player, state);
}

static void
test_play_audio_video_eos_cb (GstPlay * player, TestPlayerStateChange change,
    TestPlayerState * old_state, TestPlayerState * new_state)
{
  gint step = GPOINTER_TO_INT (new_state->test_data);
  gboolean video;

  video = !!(step & 0x10);
  step = (step & (~0x10));

  switch (step) {
    case 0:
      fail_unless_equals_int (change, STATE_CHANGE_URI_LOADED);
      if (video)
        fail_unless (g_str_has_suffix (new_state->uri_loaded,
                "audio-video-short.ogg"));
      else
        fail_unless (g_str_has_suffix (new_state->uri_loaded,
                "audio-short.ogg"));
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 1:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_STOPPED);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_BUFFERING);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 2:
      fail_unless_equals_int (change, STATE_CHANGE_MEDIA_INFO_UPDATED);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 3:
      fail_unless_equals_int (change, STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED);
      if (video) {
        fail_unless_equals_int (new_state->width, 320);
        fail_unless_equals_int (new_state->height, 240);
      } else {
        fail_unless_equals_int (new_state->width, 0);
        fail_unless_equals_int (new_state->height, 0);
      }
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 4:
      fail_unless_equals_int (change, STATE_CHANGE_DURATION_CHANGED);
      fail_unless_equals_uint64 (new_state->duration,
          G_GUINT64_CONSTANT (464399092));
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 5:
      fail_unless_equals_int (change, STATE_CHANGE_MEDIA_INFO_UPDATED);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 6:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_BUFFERING);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_PLAYING);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 7:
      fail_unless_equals_int (change, STATE_CHANGE_POSITION_UPDATED);
      g_assert_cmpuint (new_state->position, <=, old_state->duration);
      if (new_state->position == old_state->duration)
        new_state->test_data =
            GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 8:
      fail_unless_equals_int (change, STATE_CHANGE_END_OF_STREAM);
      fail_unless_equals_uint64 (new_state->position, old_state->duration);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      break;
    case 9:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_PLAYING);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_STOPPED);
      new_state->test_data =
          GINT_TO_POINTER ((video ? 0x10 : 0x00) | (step + 1));
      new_state->done = TRUE;
      break;
    default:
      fail ();
      break;
  }
}

void
process_play_messages (GstPlay * player, TestPlayerState * state)
{
  GstBus *bus = gst_play_get_message_bus (player);
  do {
    GstMessage *msg =
        gst_bus_timed_pop_filtered (bus, -1, GST_MESSAGE_APPLICATION);
    GST_INFO ("message: %" GST_PTR_FORMAT, msg);

    if (gst_play_is_play_message (msg)) {
      TestPlayerState old_state = *state;
      GstPlayMessage type;

      gst_play_message_parse_type (msg, &type);
      switch (type) {
        case GST_PLAY_MESSAGE_URI_LOADED:
          g_clear_pointer (&state->uri_loaded, g_free);
          state->uri_loaded = gst_play_get_uri (player);
          state->test_callback (player, STATE_CHANGE_URI_LOADED, &old_state,
              state);
          break;
        case GST_PLAY_MESSAGE_POSITION_UPDATED:
          gst_play_message_parse_position_updated (msg, &state->position);
          test_play_state_change_debug (player, STATE_CHANGE_POSITION_UPDATED,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_POSITION_UPDATED,
              &old_state, state);
          break;
        case GST_PLAY_MESSAGE_DURATION_CHANGED:{
          GstClockTime duration;
          gst_play_message_parse_duration_updated (msg, &duration);
          state->duration = duration;
          test_play_state_change_debug (player, STATE_CHANGE_DURATION_CHANGED,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_DURATION_CHANGED,
              &old_state, state);
          break;
        }
        case GST_PLAY_MESSAGE_STATE_CHANGED:{
          GstPlayState play_state;
          gst_play_message_parse_state_changed (msg, &play_state);

          state->state = play_state;

          if (play_state == GST_PLAY_STATE_STOPPED)
            test_play_state_reset (player, state);

          test_play_state_change_debug (player, STATE_CHANGE_STATE_CHANGED,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_STATE_CHANGED, &old_state,
              state);
          break;
        }
        case GST_PLAY_MESSAGE_BUFFERING:{
          guint percent;
          gst_play_message_parse_buffering_percent (msg, &percent);

          state->buffering_percent = percent;
          test_play_state_change_debug (player, STATE_CHANGE_BUFFERING,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_BUFFERING, &old_state,
              state);
          break;
        }
        case GST_PLAY_MESSAGE_END_OF_STREAM:
          state->end_of_stream = TRUE;
          test_play_state_change_debug (player, STATE_CHANGE_END_OF_STREAM,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_END_OF_STREAM, &old_state,
              state);
          break;
        case GST_PLAY_MESSAGE_ERROR:{
          g_clear_error (&state->error);
          gst_clear_structure (&state->error_details);
          gst_play_message_parse_error (msg, &state->error,
              &state->error_details);
          GST_DEBUG ("error: %s details: %" GST_PTR_FORMAT,
              state->error ? state->error->message : "", state->error_details);
          state->is_error = TRUE;
          state->is_warning = FALSE;
          test_play_state_change_debug (player, STATE_CHANGE_ERROR,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_ERROR, &old_state, state);
          break;
        }
        case GST_PLAY_MESSAGE_WARNING:{
          g_clear_error (&state->error);
          gst_clear_structure (&state->error_details);
          gst_play_message_parse_warning (msg, &state->error,
              &state->error_details);
          GST_DEBUG ("error: %s details: %" GST_PTR_FORMAT,
              state->error ? state->error->message : "", state->error_details);
          state->is_warning = TRUE;
          state->is_error = FALSE;
          test_play_state_change_debug (player, STATE_CHANGE_WARNING,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_WARNING, &old_state,
              state);
          break;
        }
        case GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED:{
          guint width, height;
          gst_play_message_parse_video_dimensions_changed (msg, &width,
              &height);

          state->width = width;
          state->height = height;
          test_play_state_change_debug (player,
              STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED, &old_state, state);
          state->test_callback (player, STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED,
              &old_state, state);
          break;
        }
        case GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED:{
          GstPlayMediaInfo *media_info;
          gst_play_message_parse_media_info_updated (msg, &media_info);

          if (state->media_info)
            g_object_unref (state->media_info);
          state->media_info = media_info;
          test_play_state_change_debug (player,
              STATE_CHANGE_MEDIA_INFO_UPDATED, &old_state, state);
          state->test_callback (player, STATE_CHANGE_MEDIA_INFO_UPDATED,
              &old_state, state);
          break;
        }
        case GST_PLAY_MESSAGE_VOLUME_CHANGED:{
          gdouble volume;
          gst_play_message_parse_volume_changed (msg, &volume);
          break;
        }
        case GST_PLAY_MESSAGE_MUTE_CHANGED:{
          gboolean is_muted;
          gst_play_message_parse_muted_changed (msg, &is_muted);
          break;
        }
        case GST_PLAY_MESSAGE_SEEK_DONE:
          state->seek_done = TRUE;
          state->seek_done_position = gst_play_get_position (player);
          test_play_state_change_debug (player, STATE_CHANGE_SEEK_DONE,
              &old_state, state);
          state->test_callback (player, STATE_CHANGE_SEEK_DONE, &old_state,
              state);
          break;
      }
    }
    gst_message_unref (msg);
    if (state->done)
      break;
  } while (1);
  gst_object_unref (bus);
}

START_TEST (test_play_audio_eos)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_audio_video_eos_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio-short.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 10);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_audio_info (GstPlayMediaInfo * media_info)
{
  gint i = 0;
  GList *list;

  for (list = gst_play_media_info_get_audio_streams (media_info);
      list != NULL; list = list->next) {
    GstPlayStreamInfo *stream = (GstPlayStreamInfo *) list->data;
    GstPlayAudioInfo *audio_info = (GstPlayAudioInfo *) stream;

    fail_unless (gst_play_stream_info_get_tags (stream) != NULL);
    fail_unless (gst_play_stream_info_get_caps (stream) != NULL);
    fail_unless_equals_string (gst_play_stream_info_get_stream_type (stream),
        "audio");

    if (i == 0) {
      fail_unless_equals_string (gst_play_stream_info_get_codec (stream),
          "MPEG-1 Layer 3 (MP3)");
      fail_unless_equals_int (gst_play_audio_info_get_sample_rate
          (audio_info), 48000);
      fail_unless_equals_int (gst_play_audio_info_get_channels (audio_info), 2);
      fail_unless_equals_int (gst_play_audio_info_get_max_bitrate
          (audio_info), 192000);
      fail_unless (gst_play_audio_info_get_language (audio_info) != NULL);
    } else {
      fail_unless_equals_string (gst_play_stream_info_get_codec (stream),
          "MPEG-4 AAC");
      fail_unless_equals_int (gst_play_audio_info_get_sample_rate
          (audio_info), 48000);
      fail_unless_equals_int (gst_play_audio_info_get_channels (audio_info), 6);
      fail_unless (gst_play_audio_info_get_language (audio_info) != NULL);
    }

    i++;
  }
}

static void
test_video_info (GstPlayMediaInfo * media_info)
{
  GList *list;

  for (list = gst_play_media_info_get_video_streams (media_info);
      list != NULL; list = list->next) {
    gint fps_d, fps_n;
    guint par_d, par_n;
    GstPlayStreamInfo *stream = (GstPlayStreamInfo *) list->data;
    GstPlayVideoInfo *video_info = (GstPlayVideoInfo *) stream;

    fail_unless (gst_play_stream_info_get_tags (stream) != NULL);
    fail_unless (gst_play_stream_info_get_caps (stream) != NULL);
    fail_unless_equals_int (gst_play_stream_info_get_index (stream), 0);
    fail_unless (strstr (gst_play_stream_info_get_codec (stream),
            "H.264") != NULL
        || strstr (gst_play_stream_info_get_codec (stream), "H264") != NULL);
    fail_unless_equals_int (gst_play_video_info_get_width (video_info), 320);
    fail_unless_equals_int (gst_play_video_info_get_height (video_info), 240);
    gst_play_video_info_get_framerate (video_info, &fps_n, &fps_d);
    fail_unless_equals_int (fps_n, 24);
    fail_unless_equals_int (fps_d, 1);
    gst_play_video_info_get_pixel_aspect_ratio (video_info, &par_n, &par_d);
    fail_unless_equals_int (par_n, 33);
    fail_unless_equals_int (par_d, 20);
  }
}

static void
test_subtitle_info (GstPlayMediaInfo * media_info)
{
  GList *list;

  for (list = gst_play_media_info_get_subtitle_streams (media_info);
      list != NULL; list = list->next) {
    GstPlayStreamInfo *stream = (GstPlayStreamInfo *) list->data;
    GstPlaySubtitleInfo *sub = (GstPlaySubtitleInfo *) stream;

    fail_unless_equals_string (gst_play_stream_info_get_stream_type (stream),
        "subtitle");
    fail_unless (gst_play_stream_info_get_tags (stream) != NULL);
    fail_unless (gst_play_stream_info_get_caps (stream) != NULL);
    fail_unless_equals_string (gst_play_stream_info_get_codec (stream),
        "Timed Text");
    fail_unless (gst_play_subtitle_info_get_language (sub) != NULL);
  }
}

static void
test_media_info_object (GstPlay * player, GstPlayMediaInfo * media_info)
{
  GList *list;

  /* global tag */
  fail_unless (gst_play_media_info_is_seekable (media_info) == TRUE);
  fail_unless (gst_play_media_info_get_tags (media_info) != NULL);
  fail_unless_equals_string (gst_play_media_info_get_title (media_info),
      "Sintel");
  fail_unless_equals_string (gst_play_media_info_get_container_format
      (media_info), "Matroska");
  fail_unless (gst_play_media_info_get_image_sample (media_info) == NULL);
  fail_unless (strstr (gst_play_media_info_get_uri (media_info),
          "sintel.mkv") != NULL);

  /* number of streams */
  list = gst_play_media_info_get_stream_list (media_info);
  fail_unless (list != NULL);
  fail_unless_equals_int (g_list_length (list), 10);

  list = gst_play_media_info_get_video_streams (media_info);
  fail_unless (list != NULL);
  fail_unless_equals_int (g_list_length (list), 1);

  list = gst_play_media_info_get_audio_streams (media_info);
  fail_unless (list != NULL);
  fail_unless_equals_int (g_list_length (list), 2);

  list = gst_play_media_info_get_subtitle_streams (media_info);
  fail_unless (list != NULL);
  fail_unless_equals_int (g_list_length (list), 7);

  /* test subtitle */
  test_subtitle_info (media_info);

  /* test audio */
  test_audio_info (media_info);

  /* test video */
  test_video_info (media_info);
}

static void
test_play_media_info_cb (GstPlay * player, TestPlayerStateChange change,
    TestPlayerState * old_state, TestPlayerState * new_state)
{
  gint completed = GPOINTER_TO_INT (new_state->test_data);

  if (change == STATE_CHANGE_MEDIA_INFO_UPDATED) {
    test_media_info_object (player, new_state->media_info);
    new_state->test_data = GINT_TO_POINTER (completed + 1);
    new_state->done = TRUE;
  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  }
}

START_TEST (test_play_media_info)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_media_info_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 1);
  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_stream_disable_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data) & 0xf;
  gint mask = GPOINTER_TO_INT (new_state->test_data) & 0xf0;

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    new_state->test_data = GINT_TO_POINTER (0x10 + steps + 1);
    gst_play_set_audio_track_enabled (player, FALSE);

  } else if (mask == 0x10 && change == STATE_CHANGE_POSITION_UPDATED) {
    GstPlayAudioInfo *audio;

    audio = gst_play_get_current_audio_track (player);
    fail_unless (audio == NULL);
    new_state->test_data = GINT_TO_POINTER (0x20 + steps + 1);
    gst_play_set_subtitle_track_enabled (player, FALSE);

  } else if (mask == 0x20 && change == STATE_CHANGE_POSITION_UPDATED) {
    GstPlaySubtitleInfo *sub;

    sub = gst_play_get_current_subtitle_track (player);
    fail_unless (sub == NULL);
    new_state->test_data = GINT_TO_POINTER (0x30 + steps + 1);
    new_state->done = TRUE;

  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  }
}

START_TEST (test_play_stream_disable)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_stream_disable_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 0x33);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_stream_switch_audio_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    gint ret;

    new_state->test_data = GINT_TO_POINTER (steps + 1);
    ret = gst_play_set_audio_track (player, 1);
    fail_unless_equals_int (ret, 1);

  } else if (steps && change == STATE_CHANGE_POSITION_UPDATED) {
    gint index;
    GstPlayAudioInfo *audio;

    audio = gst_play_get_current_audio_track (player);
    fail_unless (audio != NULL);
    index = gst_play_stream_info_get_index ((GstPlayStreamInfo *) audio);
    fail_unless_equals_int (index, 1);
    g_object_unref (audio);

    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;

  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  }
}

START_TEST (test_play_stream_switch_audio)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_stream_switch_audio_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_stream_switch_subtitle_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    gint ret;

    new_state->test_data = GINT_TO_POINTER (steps + 1);
    ret = gst_play_set_subtitle_track (player, 5);
    fail_unless_equals_int (ret, 1);

  } else if (steps && change == STATE_CHANGE_POSITION_UPDATED) {
    gint index;
    GstPlaySubtitleInfo *sub;

    sub = gst_play_get_current_subtitle_track (player);
    fail_unless (sub != NULL);
    index = gst_play_stream_info_get_index ((GstPlayStreamInfo *) sub);
    fail_unless_equals_int (index, 5);
    g_object_unref (sub);

    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;
  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  }
}

START_TEST (test_play_stream_switch_subtitle)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_stream_switch_subtitle_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_error_invalid_external_suburi_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    gchar *suburi;

    suburi = gst_filename_to_uri (TEST_PATH "/foo.srt", NULL);
    fail_unless (suburi != NULL);

    new_state->test_data = GINT_TO_POINTER (steps + 1);
    /* load invalid suburi */
    gst_play_set_subtitle_uri (player, suburi);
    g_free (suburi);

  } else if (steps && change == STATE_CHANGE_WARNING) {
    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;
  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;
  }
}

START_TEST (test_play_error_invalid_external_suburi)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_error_invalid_external_suburi_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio-video.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static gboolean
has_subtitle_stream (TestPlayerState * new_state)
{
  if (gst_play_media_info_get_subtitle_streams (new_state->media_info))
    return TRUE;

  return FALSE;
}

static void
test_play_external_suburi_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    gchar *suburi;

    suburi = gst_filename_to_uri (TEST_PATH "/test_sub.srt", NULL);
    fail_unless (suburi != NULL);

    gst_play_set_subtitle_uri (player, suburi);
    g_free (suburi);
    new_state->test_data = GINT_TO_POINTER (steps + 1);

  } else if (change == STATE_CHANGE_MEDIA_INFO_UPDATED &&
      has_subtitle_stream (new_state)) {
    gchar *current_suburi, *suburi;

    current_suburi = gst_play_get_subtitle_uri (player);
    fail_unless (current_suburi != NULL);
    suburi = gst_filename_to_uri (TEST_PATH "/test_sub.srt", NULL);
    fail_unless (suburi != NULL);

    fail_unless_equals_int (g_strcmp0 (current_suburi, suburi), 0);

    g_free (current_suburi);
    g_free (suburi);
    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;

  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR)
    new_state->done = TRUE;
}

START_TEST (test_play_external_suburi)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_external_suburi_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio-video.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_rate_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data) & 0xf;
  gint mask = GPOINTER_TO_INT (new_state->test_data) & 0xf0;

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    guint64 dur = -1, pos = -1;

    g_object_get (player, "position", &pos, "duration", &dur, NULL);
    pos = pos + dur * 0.2;      /* seek 20% */
    gst_play_seek (player, pos);

    /* default rate should be 1.0 */
    fail_unless_equals_double (gst_play_get_rate (player), 1.0);
    new_state->test_data = GINT_TO_POINTER (mask + steps + 1);
  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  } else if (steps == 1 && change == STATE_CHANGE_SEEK_DONE) {
    if (mask == 0x10)
      gst_play_set_rate (player, 1.5);
    else if (mask == 0x20)
      gst_play_set_rate (player, -1.0);

    new_state->test_data = GINT_TO_POINTER (mask + steps + 1);
  } else if (steps && (change == STATE_CHANGE_POSITION_UPDATED)) {
    if (steps == 10) {
      new_state->done = TRUE;
    } else {
      if (mask == 0x10 && (new_state->position > old_state->position))
        new_state->test_data = GINT_TO_POINTER (mask + steps + 1);
      else if (mask == 0x20 && (new_state->position < old_state->position))
        new_state->test_data = GINT_TO_POINTER (mask + steps + 1);
    }
  }
}

START_TEST (test_play_forward_rate)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_rate_cb;
  state.test_data = GINT_TO_POINTER (0x10);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data) & 0xf, 10);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

START_TEST (test_play_backward_rate)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_rate_cb;
  state.test_data = GINT_TO_POINTER (0x20);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data) & 0xf, 10);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

START_TEST (test_play_audio_video_eos)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_audio_video_eos_cb;
  state.test_data = GINT_TO_POINTER (0x10);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio-video-short.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data) & (~0x10), 10);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_error_invalid_uri_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint step = GPOINTER_TO_INT (new_state->test_data);

  switch (step) {
    case 0:
      fail_unless_equals_int (change, STATE_CHANGE_URI_LOADED);
      fail_unless_equals_string (new_state->uri_loaded, "foo://bar");
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 1:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_STOPPED);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_BUFFERING);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 2:
      fail_unless_equals_int (change, STATE_CHANGE_ERROR);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 3:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_BUFFERING);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_STOPPED);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      new_state->done = TRUE;
      break;
    default:
      fail ();
      break;
  }
}

START_TEST (test_play_error_invalid_uri)
{
  GstPlay *player;
  TestPlayerState state;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_error_invalid_uri_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  gst_play_set_uri (player, "foo://bar");

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 4);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_error_invalid_uri_and_play_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint step = GPOINTER_TO_INT (new_state->test_data);
  gchar *uri;

  switch (step) {
    case 0:
      fail_unless_equals_int (change, STATE_CHANGE_URI_LOADED);
      fail_unless_equals_string (new_state->uri_loaded, "foo://bar");
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 1:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_STOPPED);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_BUFFERING);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 2:
      fail_unless_equals_int (change, STATE_CHANGE_ERROR);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 3:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_BUFFERING);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_STOPPED);
      new_state->test_data = GINT_TO_POINTER (step + 1);

      uri = gst_filename_to_uri (TEST_PATH "/audio-short.ogg", NULL);
      fail_unless (uri != NULL);
      gst_play_set_uri (player, uri);
      g_free (uri);

      gst_play_play (player);
      break;
    case 4:
      fail_unless_equals_int (change, STATE_CHANGE_URI_LOADED);
      fail_unless (g_str_has_suffix (new_state->uri_loaded, "audio-short.ogg"));
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 5:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_STOPPED);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_BUFFERING);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 6:
      fail_unless_equals_int (change, STATE_CHANGE_MEDIA_INFO_UPDATED);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 7:
      fail_unless_equals_int (change, STATE_CHANGE_VIDEO_DIMENSIONS_CHANGED);
      fail_unless_equals_int (new_state->width, 0);
      fail_unless_equals_int (new_state->height, 0);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 8:
      fail_unless_equals_int (change, STATE_CHANGE_DURATION_CHANGED);
      fail_unless_equals_uint64 (new_state->duration,
          G_GUINT64_CONSTANT (464399092));
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 9:
      fail_unless_equals_int (change, STATE_CHANGE_MEDIA_INFO_UPDATED);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      break;
    case 10:
      fail_unless_equals_int (change, STATE_CHANGE_STATE_CHANGED);
      fail_unless_equals_int (old_state->state, GST_PLAY_STATE_BUFFERING);
      fail_unless_equals_int (new_state->state, GST_PLAY_STATE_PLAYING);
      new_state->test_data = GINT_TO_POINTER (step + 1);
      new_state->done = TRUE;
      break;
    default:
      fail ();
      break;
  }
}

START_TEST (test_play_error_invalid_uri_and_play)
{
  GstPlay *player;
  TestPlayerState state;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_error_invalid_uri_and_play_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  gst_play_set_uri (player, "foo://bar");

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 11);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_seek_done_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint step = GPOINTER_TO_INT (new_state->test_data) & (~0x10);

  if (new_state->state == GST_PLAY_STATE_PAUSED && !step) {
    gst_play_seek (player, 0);
    new_state->test_data = GINT_TO_POINTER (step + 1);
  } else if (change == STATE_CHANGE_SEEK_DONE && step == 1) {
    fail_unless_equals_int (change, STATE_CHANGE_SEEK_DONE);
    fail_unless_equals_uint64 (new_state->seek_done_position,
        G_GUINT64_CONSTANT (0));
    new_state->test_data = GINT_TO_POINTER (step + 1);
    new_state->done = TRUE;
  }
}

START_TEST (test_play_audio_video_seek_done)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_seek_done_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/audio-video.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_pause (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data) & (~0x10), 2);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_play_position_update_interval_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (new_state->state == GST_PLAY_STATE_PLAYING && !steps) {
    new_state->test_data = GINT_TO_POINTER (steps + 1);
  } else if (steps && change == STATE_CHANGE_POSITION_UPDATED) {
    GstStructure *config = gst_play_get_config (player);
    guint update_interval =
        gst_play_config_get_position_update_interval (config);
    GstClockTime position = gst_play_get_position (player);
    new_state->test_data = GINT_TO_POINTER (steps + 1);

    gst_structure_free (config);
    if (GST_CLOCK_TIME_IS_VALID (old_state->last_position)) {
      GstClockTime delta = GST_CLOCK_DIFF (old_state->last_position, position);
      GST_DEBUG_OBJECT (player,
          "current delta: %" GST_TIME_FORMAT " interval: %" GST_TIME_FORMAT,
          GST_TIME_ARGS (delta), GST_TIME_ARGS (update_interval * GST_MSECOND));

      if (update_interval > 10) {
        fail_unless (delta > ((update_interval - 10) * GST_MSECOND)
            && delta < ((update_interval + 10) * GST_MSECOND));
      }
    }

    new_state->last_position = position;

    if (position >= 2000 * GST_MSECOND) {
      new_state->done = TRUE;
    }
  } else if (change == STATE_CHANGE_END_OF_STREAM ||
      change == STATE_CHANGE_ERROR) {
    new_state->done = TRUE;
  }
}

START_TEST (test_play_position_update_interval)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;
  GstStructure *config;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_play_position_update_interval_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  config = gst_play_get_config (player);
  gst_play_config_set_position_update_interval (config, 600);
  gst_play_set_config (player, config);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 5);

  /* Disable position updates */
  gst_play_stop (player);

  config = gst_play_get_config (player);
  gst_play_config_set_position_update_interval (config, 0);
  gst_play_set_config (player, config);
  state.last_position = GST_CLOCK_TIME_NONE;

  gst_play_play (player);
  process_play_messages (player, &state);

  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 6);

  stop_player (player, &state);
  g_object_unref (player);
}

END_TEST;

static void
test_restart_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (!steps && change == STATE_CHANGE_URI_LOADED) {
    fail_unless (g_str_has_suffix (new_state->uri_loaded, "sintel.mkv"));
    new_state->test_data = GINT_TO_POINTER (steps + 1);
  } else if (change == STATE_CHANGE_STATE_CHANGED
      && new_state->state == GST_PLAY_STATE_BUFFERING) {
    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;
  }
}

static void
test_restart_cb2 (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  gint steps = GPOINTER_TO_INT (new_state->test_data);

  if (!steps && change == STATE_CHANGE_URI_LOADED) {
    fail_unless (g_str_has_suffix (new_state->uri_loaded, "audio-short.ogg"));
    new_state->test_data = GINT_TO_POINTER (steps + 1);
  } else if (change == STATE_CHANGE_STATE_CHANGED
      && new_state->state == GST_PLAY_STATE_BUFFERING) {
    new_state->test_data = GINT_TO_POINTER (steps + 1);
    new_state->done = TRUE;
  }
}


START_TEST (test_restart)
{
  GstPlay *player;
  TestPlayerState state;
  gchar *uri;

  memset (&state, 0, sizeof (state));
  state.test_callback = test_restart_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);

  fail_unless (player != NULL);

  uri = gst_filename_to_uri (TEST_PATH "/sintel.mkv", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);
  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);
  stop_player (player, &state);

  /* Try again with another URI */
  state.test_data = GINT_TO_POINTER (0);
  state.test_callback = test_restart_cb2;

  uri = gst_filename_to_uri (TEST_PATH "/audio-short.ogg", NULL);
  fail_unless (uri != NULL);
  gst_play_set_uri (player, uri);
  g_free (uri);

  gst_play_play (player);
  process_play_messages (player, &state);
  fail_unless_equals_int (GPOINTER_TO_INT (state.test_data), 2);
  stop_player (player, &state);

  g_object_unref (player);
}

END_TEST;

static void
do_get (SoupServerMessage * msg, const char *path)
{
  char *uri;
  SoupStatus status = SOUP_STATUS_OK;

  uri = g_uri_to_string (soup_server_message_get_uri (msg));
  GST_DEBUG ("request: \"%s\"", uri);

  if (status != (SoupStatus) SOUP_STATUS_OK)
    goto beach;

  if (soup_server_message_get_method (msg) == SOUP_METHOD_GET) {
    SoupMessageBody *response_body;
    char *full_path = g_strconcat (TEST_PATH, path, NULL);
    char *buf;
    gsize buflen;

    if (!g_file_get_contents (full_path, &buf, &buflen, NULL)) {
      status = SOUP_STATUS_NOT_FOUND;
      g_free (full_path);
      goto beach;
    }

    g_free (full_path);
    response_body = soup_server_message_get_response_body (msg);
    soup_message_body_append (response_body, SOUP_MEMORY_TAKE, buf, buflen);
  }

beach:
  soup_server_message_set_status (msg, status, NULL);
  g_free (uri);
}

static void
server_callback (SoupServer * server, SoupServerMessage * msg,
    const char *path, GHashTable * query, gpointer data)
{
  SoupMessageBody *request_body;

  GST_DEBUG ("%s %s HTTP/1.%d", soup_server_message_get_method (msg),
      path, soup_server_message_get_http_version (msg));
  if ((request_body = soup_server_message_get_request_body (msg))
      && request_body->length > 0) {
    GST_DEBUG ("%s", request_body->data);
  }

  if (soup_server_message_get_method (msg) == SOUP_METHOD_GET)
    do_get (msg, path);
  else
    soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL);

  GST_DEBUG ("  -> %d %s", soup_server_message_get_status (msg),
      soup_server_message_get_reason_phrase (msg));
}

static guint
get_port_from_server (SoupServer * server)
{
  GSList *uris;
  guint port;

  uris = soup_server_get_uris (server);
  fail_unless_equals_int (g_slist_length (uris), 1);
  port = g_uri_get_port (uris->data);
  g_slist_free_full (uris, (GDestroyNotify) g_uri_unref);

  return port;
}

typedef struct
{
  GMainLoop *loop;
  GMainContext *ctx;
  GThread *thread;
  SoupServer *server;
  GMutex lock;
  GCond cond;
} ServerContext;

static gboolean
main_loop_running_cb (gpointer data)
{
  ServerContext *context = (ServerContext *) data;

  g_mutex_lock (&context->lock);
  g_cond_signal (&context->cond);
  g_mutex_unlock (&context->lock);

  return G_SOURCE_REMOVE;
}

static gpointer
http_main (gpointer data)
{
  ServerContext *context = (ServerContext *) data;
  GSource *source;

  context->server = soup_server_new (NULL, NULL);
  soup_server_add_handler (context->server, NULL, server_callback, NULL, NULL);

  g_main_context_push_thread_default (context->ctx);

  {
    GSocketAddress *address;
    GError *err = NULL;
    SoupServerListenOptions listen_flags = 0;

    address = g_inet_socket_address_new_from_string ("0.0.0.0", 0);
    soup_server_listen (context->server, address, listen_flags, &err);
    g_object_unref (address);

    if (err) {
      GST_ERROR ("Failed to start HTTP server: %s", err->message);
      g_object_unref (context->server);
      g_error_free (err);
    }
  }

  source = g_idle_source_new ();
  g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, context,
      NULL);
  g_source_attach (source, context->ctx);
  g_source_unref (source);

  g_main_loop_run (context->loop);
  g_main_context_pop_thread_default (context->ctx);
  g_object_unref (context->server);
  return NULL;
}

#define TEST_USER_AGENT "test user agent"

static GstElement *test_user_agent_source = NULL;

static void
test_user_agent_source_setup_cp (GstElement * element, GstElement * source,
    gpointer user_data)
{
  gst_object_replace ((GstObject **) & test_user_agent_source,
      (GstObject *) source);
}

static void
test_user_agent_cb (GstPlay * player,
    TestPlayerStateChange change, TestPlayerState * old_state,
    TestPlayerState * new_state)
{
  if (change == STATE_CHANGE_STATE_CHANGED
      && new_state->state == GST_PLAY_STATE_PAUSED) {
    gchar *user_agent;

    fail_unless (test_user_agent_source != NULL);
    g_object_get (test_user_agent_source, "user-agent", &user_agent, NULL);
    fail_unless_equals_string (user_agent, TEST_USER_AGENT);
    g_free (user_agent);
    new_state->done = TRUE;
  }
}

START_TEST (test_user_agent)
{
  GstPlay *player;
  GstElement *pipeline;
  GstStructure *config;
  gchar *user_agent;
  TestPlayerState state;
  guint port;
  gchar *url;
  ServerContext *context = g_new (ServerContext, 1);

  test_user_agent_source = NULL;

  g_mutex_init (&context->lock);
  g_cond_init (&context->cond);
  context->ctx = g_main_context_new ();
  context->loop = g_main_loop_new (context->ctx, FALSE);
  context->server = NULL;

  g_mutex_lock (&context->lock);
  context->thread = g_thread_new ("HTTP Server", http_main, context);
  while (!g_main_loop_is_running (context->loop))
    g_cond_wait (&context->cond, &context->lock);
  g_mutex_unlock (&context->lock);

  if (context->server == NULL) {
    g_print ("Failed to start up HTTP server");
    /* skip this test */
    goto beach;
  }

  memset (&state, 0, sizeof (state));
  state.test_callback = test_user_agent_cb;
  state.test_data = GINT_TO_POINTER (0);

  player = test_play_new (&state);
  fail_unless (player != NULL);

  pipeline = gst_play_get_pipeline (player);
  g_signal_connect (pipeline, "source-setup",
      G_CALLBACK (test_user_agent_source_setup_cp), NULL);
  gst_object_unref (pipeline);

  port = get_port_from_server (context->server);
  url = g_strdup_printf ("http://127.0.0.1:%u/audio.ogg", port);
  fail_unless (url != NULL);

  gst_play_set_uri (player, url);
  g_free (url);

  config = gst_play_get_config (player);
  gst_play_config_set_user_agent (config, TEST_USER_AGENT);

  user_agent = gst_play_config_get_user_agent (config);
  fail_unless_equals_string (user_agent, TEST_USER_AGENT);
  g_free (user_agent);

  gst_play_set_config (player, config);

  gst_play_pause (player);
  process_play_messages (player, &state);

  stop_player (player, &state);
  gst_clear_object (&test_user_agent_source);
  g_object_unref (player);

beach:
  g_main_loop_quit (context->loop);
  g_thread_join (context->thread);
  g_main_loop_unref (context->loop);
  context->loop = NULL;
  g_main_context_unref (context->ctx);
  context->ctx = NULL;
  g_free (context);
}

END_TEST;

static Suite *
play_suite (void)
{
  Suite *s = suite_create ("GstPlay");

  TCase *tc_general = tcase_create ("general");

  /* Use a longer timeout */
#ifdef HAVE_VALGRIND
  if (RUNNING_ON_VALGRIND) {
    tcase_set_timeout (tc_general, 5 * 60);
  } else
#endif
  {
    tcase_set_timeout (tc_general, 2 * 60);
  }

  tcase_add_test (tc_general, test_create_and_free);
  tcase_add_test (tc_general, test_set_and_get_uri);
  tcase_add_test (tc_general, test_set_and_get_position_update_interval);

#ifdef HAVE_VALGRIND
  if (RUNNING_ON_VALGRIND) {
  } else
#endif
  {
    tcase_add_test (tc_general, test_play_position_update_interval);
  }
  tcase_add_test (tc_general, test_play_audio_eos);
  tcase_add_test (tc_general, test_play_audio_video_eos);
  tcase_add_test (tc_general, test_play_error_invalid_uri);
  tcase_add_test (tc_general, test_play_error_invalid_uri_and_play);
  tcase_add_test (tc_general, test_play_media_info);
  tcase_add_test (tc_general, test_play_stream_disable);
  tcase_add_test (tc_general, test_play_stream_switch_audio);
  tcase_add_test (tc_general, test_play_stream_switch_subtitle);
  tcase_add_test (tc_general, test_play_error_invalid_external_suburi);
  tcase_add_test (tc_general, test_play_external_suburi);
  tcase_add_test (tc_general, test_play_forward_rate);
  tcase_add_test (tc_general, test_play_backward_rate);
  tcase_add_test (tc_general, test_play_audio_video_seek_done);
  tcase_add_test (tc_general, test_restart);
  tcase_add_test (tc_general, test_user_agent);

  suite_add_tcase (s, tc_general);

  return s;
}

GST_CHECK_MAIN (play)
