/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright 2011 Red Hat, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library 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 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "gio.h"

/* hack */
#define GIO_COMPILATION
#include "gnetworkmonitorbase.h"

#include <string.h>

/* Test data; the GInetAddresses and GInetAddressMasks get filled in
 * in main(). Each address in a TestAddress matches the mask in its
 * corresponding TestMask, and none of them match any of the other
 * masks. The addresses in unmatched don't match any of the masks.
 */

typedef struct {
  const char *string;
  GInetAddress *address;
} TestAddress;

typedef struct {
  const char *mask_string;
  GInetAddressMask *mask;
  TestAddress *addresses;
} TestMask;

TestAddress net127addrs[] = {
  { "127.0.0.1", NULL },
  { "127.0.0.2", NULL },
  { "127.0.0.255", NULL },
  { "127.0.1.0", NULL },
  { "127.0.255.0", NULL },
  { "127.0.255.0", NULL },
  { "127.255.255.255", NULL },
  { NULL, NULL }
};
TestMask net127 = { "127.0.0.0/8", NULL, net127addrs };

TestAddress net10addrs[] = {
  { "10.0.0.1", NULL },
  { "10.0.0.2", NULL },
  { "10.0.0.255", NULL },
  { NULL, NULL }
};
TestMask net10 = { "10.0.0.0/24", NULL, net10addrs };

TestAddress net192addrs[] = {
  { "192.168.0.1", NULL },
  { "192.168.0.2", NULL },
  { "192.168.0.255", NULL },
  { "192.168.1.0", NULL },
  { "192.168.15.0", NULL },
  { NULL, NULL }
};
TestMask net192 = { "192.168.0.0/20", NULL, net192addrs };

TestAddress netlocal6addrs[] = {
  { "::1", NULL },
  { NULL, NULL }
};
TestMask netlocal6 = { "::1/128", NULL, netlocal6addrs };

TestAddress netfe80addrs[] = {
  { "fe80::", NULL },
  { "fe80::1", NULL },
  { "fe80::21b:77ff:fea2:972a", NULL },
  { NULL, NULL }
};
TestMask netfe80 = { "fe80::/64", NULL, netfe80addrs };

TestAddress unmatched[] = {
  { "10.0.1.0", NULL },
  { "10.0.255.0", NULL },
  { "10.255.255.255", NULL },
  { "192.168.16.0", NULL },
  { "192.168.255.0", NULL },
  { "192.169.0.0", NULL },
  { "192.255.255.255", NULL },
  { "::2", NULL },
  { "1::1", NULL },
  { "fe80::1:0:0:0:0", NULL },
  { "fe80:8000::0:0:0:0", NULL },
  { NULL, NULL }
};

GInetAddressMask *ip4_default, *ip6_default;

static void
notify_handler (GObject    *object,
                GParamSpec *pspec,
                gpointer    user_data)
{
  gboolean *emitted = user_data;

  *emitted = TRUE;
}

static void
network_changed_handler (GNetworkMonitor *monitor,
                         gboolean         available,
                         gpointer         user_data)
{
  gboolean *emitted = user_data;

  *emitted = TRUE;
}

static void
assert_signals (GNetworkMonitor *monitor,
                gboolean         should_emit_notify,
                gboolean         should_emit_network_changed,
                gboolean         expected_network_available)
{
  gboolean emitted_notify = FALSE, emitted_network_changed = FALSE;
  guint h1, h2;

  h1 = g_signal_connect (monitor, "notify::network-available",
                         G_CALLBACK (notify_handler),
                         &emitted_notify);
  h2 = g_signal_connect (monitor, "network-changed",
                         G_CALLBACK (network_changed_handler),
                         &emitted_network_changed);

  g_main_context_iteration (NULL, FALSE);

  g_signal_handler_disconnect (monitor, h1);
  g_signal_handler_disconnect (monitor, h2);

  g_assert (emitted_notify == should_emit_notify);
  g_assert (emitted_network_changed == should_emit_network_changed);

  g_assert (g_network_monitor_get_network_available (monitor) == expected_network_available);
}

typedef struct {
  GNetworkMonitor *monitor;
  GMainLoop       *loop;
  GSocketAddress  *sockaddr;
  gboolean         should_be_reachable;
} CanReachData;

static void
reach_cb (GObject      *source,
          GAsyncResult *res,
          gpointer      user_data)
{
  GError *error = NULL;
  gboolean reachable;
  CanReachData *data = user_data;

  reachable = g_network_monitor_can_reach_finish (data->monitor, res, &error);

  if (data->should_be_reachable)
    g_assert_no_error (error);
  else
    {
      g_assert (error != NULL);
      g_clear_error (&error);
    }
  g_assert (reachable == data->should_be_reachable);

  g_main_loop_quit (data->loop);
}

static gboolean
test_reach_async (gpointer user_data)
{
  CanReachData *data = user_data;

  g_network_monitor_can_reach_async (data->monitor,
                                     G_SOCKET_CONNECTABLE (data->sockaddr),
                                     NULL,
                                     reach_cb,
                                     data);
  return G_SOURCE_REMOVE;
}

static void
run_tests (GNetworkMonitor *monitor,
           TestAddress     *addresses,
           gboolean         should_be_reachable)
{
  GError *error = NULL;
  int i;
  gboolean reachable;
  GSocketAddress *sockaddr;
  CanReachData data;

  data.monitor = monitor;
  data.loop = g_main_loop_new (NULL, FALSE);

  for (i = 0; addresses[i].address; i++)
    {
      sockaddr = g_inet_socket_address_new (addresses[i].address, 0);
      reachable = g_network_monitor_can_reach (monitor,
                                               G_SOCKET_CONNECTABLE (sockaddr),
                                               NULL, &error);
      data.sockaddr = sockaddr;
      data.should_be_reachable = should_be_reachable;

      g_idle_add (test_reach_async, &data);
      g_main_loop_run (data.loop);

      g_object_unref (sockaddr);
      g_assert_cmpint (reachable, ==, should_be_reachable);
      if (should_be_reachable)
        g_assert_no_error (error);
      else
        {
          g_assert (error != NULL);
          g_clear_error (&error);
        }
    }

  g_main_loop_unref (data.loop);
}

static void
test_default (void)
{
  GNetworkMonitor *monitor, *m;
  GError *error = NULL;

  m = g_network_monitor_get_default ();
  g_assert (G_IS_NETWORK_MONITOR (m));

  monitor = g_object_new (G_TYPE_NETWORK_MONITOR_BASE, NULL);
  g_initable_init (G_INITABLE (monitor), NULL, &error);
  g_assert_no_error (error);

  /* In the default configuration, all addresses are reachable */
  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, TRUE);

  assert_signals (monitor, FALSE, FALSE, TRUE);

  g_object_unref (monitor);
}

static void
test_remove_default (void)
{
  GNetworkMonitor *monitor;
  GError *error = NULL;

  monitor = g_initable_new (G_TYPE_NETWORK_MONITOR_BASE, NULL, &error, NULL);
  g_assert_no_error (error);
  assert_signals (monitor, FALSE, FALSE, TRUE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip4_default);
  assert_signals (monitor, FALSE, TRUE, TRUE);
  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip6_default);
  assert_signals (monitor, TRUE, TRUE, FALSE);

  /* Now nothing should be reachable */
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_object_unref (monitor);
}

static void
test_add_networks (void)
{
  GNetworkMonitor *monitor;
  GError *error = NULL;

  monitor = g_initable_new (G_TYPE_NETWORK_MONITOR_BASE, NULL, &error, NULL);
  g_assert_no_error (error);
  assert_signals (monitor, FALSE, FALSE, TRUE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip4_default);
  assert_signals (monitor, FALSE, TRUE, TRUE);
  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip6_default);
  assert_signals (monitor, TRUE, TRUE, FALSE);

  /* Now add the masks one by one */

  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net127.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);

  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net10.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net192.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      netlocal6.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      netfe80.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  g_object_unref (monitor);
}

static void
test_remove_networks (void)
{
  GNetworkMonitor *monitor;
  GError *error = NULL;

  monitor = g_initable_new (G_TYPE_NETWORK_MONITOR_BASE, NULL, &error, NULL);
  g_assert_no_error (error);
  assert_signals (monitor, FALSE, FALSE, TRUE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip4_default);
  assert_signals (monitor, FALSE, TRUE, TRUE);
  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         ip6_default);
  assert_signals (monitor, TRUE, TRUE, FALSE);

  /* First add them */
  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net127.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net10.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      net192.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      netlocal6.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  g_network_monitor_base_add_network (G_NETWORK_MONITOR_BASE (monitor),
                                      netfe80.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);

  run_tests (monitor, net127.addresses, TRUE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  /* Now remove them one by one */
  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         net127.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, TRUE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         net10.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, TRUE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         net192.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, TRUE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         netlocal6.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, TRUE);
  run_tests (monitor, unmatched, FALSE);

  g_network_monitor_base_remove_network (G_NETWORK_MONITOR_BASE (monitor),
                                         netfe80.mask);
  assert_signals (monitor, FALSE, TRUE, FALSE);
  run_tests (monitor, net127.addresses, FALSE);
  run_tests (monitor, net10.addresses, FALSE);
  run_tests (monitor, net192.addresses, FALSE);
  run_tests (monitor, netlocal6.addresses, FALSE);
  run_tests (monitor, netfe80.addresses, FALSE);
  run_tests (monitor, unmatched, FALSE);

  g_object_unref (monitor);
}


static void
init_test (TestMask *test)
{
  GError *error = NULL;
  int i;

  test->mask = g_inet_address_mask_new_from_string (test->mask_string, &error);
  g_assert_no_error (error);

  for (i = 0; test->addresses[i].string; i++)
    {
      test->addresses[i].address = g_inet_address_new_from_string (test->addresses[i].string);
      if (strchr (test->addresses[i].string, ':'))
        g_assert_cmpint (g_inet_address_get_family (test->addresses[i].address), ==, G_SOCKET_FAMILY_IPV6);
      else
        g_assert_cmpint (g_inet_address_get_family (test->addresses[i].address), ==, G_SOCKET_FAMILY_IPV4);
    }
}

static void
cleanup_test (TestMask *test)
{
  int i;

  g_object_unref (test->mask);
  for (i = 0; test->addresses[i].string; i++)
    g_object_unref (test->addresses[i].address);
}

static void
watch_network_changed (GNetworkMonitor *monitor,
                       gboolean         available,
                       gpointer         user_data)
{
  g_print ("Network is %s\n", available ? "up" : "down");
}

static void
watch_connectivity_changed (GNetworkMonitor *monitor,
			    GParamSpec      *pspec,
			    gpointer         user_data)
{
  g_print ("Connectivity is %d\n", g_network_monitor_get_connectivity (monitor));
}

static void
watch_metered_changed (GNetworkMonitor *monitor,
                       GParamSpec      *pspec,
                       gpointer         user_data)
{
  g_print ("Metered is %d\n", g_network_monitor_get_network_metered (monitor));
}

static void
do_watch_network (void)
{
  GNetworkMonitor *monitor = g_network_monitor_get_default ();
  GMainLoop *loop;

  g_print ("Monitoring via %s\n", g_type_name_from_instance ((GTypeInstance *) monitor));

  g_signal_connect (monitor, "network-changed",
                    G_CALLBACK (watch_network_changed), NULL);
  g_signal_connect (monitor, "notify::connectivity",
                    G_CALLBACK (watch_connectivity_changed), NULL);
  g_signal_connect (monitor, "notify::network-metered",
                    G_CALLBACK (watch_metered_changed), NULL);
  watch_network_changed (monitor, g_network_monitor_get_network_available (monitor), NULL);
  watch_connectivity_changed (monitor, NULL, NULL);
  watch_metered_changed (monitor, NULL, NULL);

  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);
}

int
main (int argc, char **argv)
{
  int ret;

  if (argc == 2 && !strcmp (argv[1], "--watch"))
    {
      do_watch_network ();
      return 0;
    }

  g_test_init (&argc, &argv, NULL);

  /* GNetworkMonitor will resolve addresses through a proxy if one is set and a
   * GIO module is available to handle it. In these tests we deliberately
   * change the idea of a reachable network to exclude the proxy, which will
   * lead to negative results. We're not trying to test the proxy-resolving
   * functionality (that would be for e.g. glib-networking's testsuite), so
   * let's just use the dummy proxy resolver, which always pretends the
   * passed-in URL is directly resolvable.
   */
  g_setenv ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);

  init_test (&net127);
  init_test (&net10);
  init_test (&net192);
  init_test (&netlocal6);
  init_test (&netfe80);
  ip4_default = g_inet_address_mask_new_from_string ("0.0.0.0/0", NULL);
  ip6_default = g_inet_address_mask_new_from_string ("::/0", NULL);

  g_test_add_func ("/network-monitor/default", test_default);
  g_test_add_func ("/network-monitor/remove_default", test_remove_default);
  g_test_add_func ("/network-monitor/add_networks", test_add_networks);
  g_test_add_func ("/network-monitor/remove_networks", test_remove_networks);

  ret = g_test_run ();

  cleanup_test (&net127);
  cleanup_test (&net10);
  cleanup_test (&net192);
  cleanup_test (&netlocal6);
  cleanup_test (&netfe80);
  g_object_unref (ip4_default);
  g_object_unref (ip6_default);

  return ret;
}
