open Lwt.Infix

module B = Basic_backend.Make
module V = Vnetif.Make(B)
module E = Ethernet.Make(V)
module A = Arp.Make(E)

let src = Logs.Src.create "test_arp" ~doc:"Mirage ARP tester"
module Log = (val Logs.src_log src : Logs.LOG)

type arp_stack = {
  backend : B.t;
  netif: V.t;
  ethif: E.t;
  arp: A.t;
}

let first_ip = Ipaddr.V4.of_string_exn "192.168.3.1"
let second_ip = Ipaddr.V4.of_string_exn "192.168.3.10"
let sample_mac = Macaddr.of_string_exn "10:9a:dd:c0:ff:ee"

let packet = (module Arp_packet : Alcotest.TESTABLE with type t = Arp_packet.t)

let ip =
  let module M = struct
    type t = Ipaddr.V4.t
    let pp = Ipaddr.V4.pp
    let equal p q = (Ipaddr.V4.compare p q) = 0
  end in
  (module M : Alcotest.TESTABLE with type t = M.t)

let macaddr =
  let module M = struct
    type t = Macaddr.t
    let pp = Macaddr.pp
    let equal p q = (Macaddr.compare p q) = 0
  end in
  (module M : Alcotest.TESTABLE with type t = M.t)

let header_size = Ethernet.Packet.sizeof_ethernet
let size = Arp_packet.size

let check_header ~message expected actual =
  Alcotest.(check packet) message expected actual

let fail = Alcotest.fail
let failf fmt = Fmt.kstr (fun s -> Alcotest.fail s) fmt

let timeout ~time t =
  let msg = Printf.sprintf "Timed out: didn't complete in %d milliseconds" time in
  Lwt.pick [ t; Mirage_sleep.ns (Duration.of_ms time) >>= fun () -> fail msg; ]

let check_response expected buf =
  match Arp_packet.decode buf with
  | Error s -> Alcotest.fail (Fmt.to_to_string Arp_packet.pp_error s)
  | Ok actual ->
    Alcotest.(check packet) "parsed packet comparison" expected actual

let check_ethif_response expected buf =
  let open Ethernet.Packet in
  match of_cstruct buf with
  | Error s -> Alcotest.fail s
  | Ok ({ethertype; _}, arp) ->
    match ethertype with
    | `ARP -> check_response expected arp
    | _ -> Alcotest.fail "Ethernet packet with non-ARP ethertype"

let garp source_mac source_ip =
  let open Arp_packet in
  {
    operation = Request;
    source_mac;
    target_mac = Macaddr.of_octets_exn "\000\000\000\000\000\000";
    source_ip;
    target_ip = source_ip;
  }

let fail_on_receipt netif buf =
  Alcotest.fail (Format.asprintf "received traffic when none was expected on interface %a: %a"
	  Macaddr.pp (V.mac netif) Cstruct.hexdump_pp buf)

let single_check netif expected =
  V.listen netif ~header_size (fun buf ->
      match Ethernet.Packet.of_cstruct buf with
      | Error _ -> failwith "sad face"
      | Ok (_, payload) ->
        check_response expected payload; V.disconnect netif) >|= fun _ -> ()

(*  { Ethernet_packet.source = arp.source_mac;
      destination = arp.target_mac;
      ethertype = `ARP;
    } *)

let arp_reply ~from_netif ~to_netif ~from_ip ~to_ip arp =
  let open Arp_packet in
  let a =
    { operation = Reply;
      source_mac = V.mac from_netif;
      target_mac = V.mac to_netif;
      source_ip = from_ip;
      target_ip = to_ip}
  in
  encode_into a arp ;
  Arp_packet.size

let arp_request ~from_netif ~to_mac ~from_ip ~to_ip arp =
  let open Arp_packet in
  let a =
    { operation = Request;
      source_mac = V.mac from_netif;
      target_mac = to_mac;
      source_ip = from_ip;
      target_ip = to_ip}
  in
  encode_into a arp ;
  Arp_packet.size

let get_arp ?backend () =
  let backend = match backend with
    | None -> B.create ~use_async_readers:true ~yield:Lwt.pause ()
    | Some b -> b
  in
  V.connect backend >>= fun netif ->
  E.connect netif >>= fun ethif ->
  A.connect ~probe_delay:(Duration.of_ms 2) ethif >>= fun arp ->
  Lwt.return { backend; netif; ethif; arp }

(* we almost always want two stacks on the same backend *)
let two_arp () =
  get_arp () >>= fun first ->
  get_arp ~backend:first.backend () >>= fun second ->
  Lwt.return (first, second)

(* ...but sometimes we want three *)
let three_arp () =
  get_arp () >>= fun first ->
  get_arp ~backend:first.backend () >>= fun second ->
  get_arp ~backend:first.backend () >>= fun third ->
  Lwt.return (first, second, third)

let query_or_die arp ip expected_mac =
  A.query arp ip >>= function
  | Error `Timeout ->
    Log.warn (fun f -> f "Timeout querying %a. Table contents: %a"
                 Ipaddr.V4.pp ip A.pp arp);
    fail "ARP query failed when success was mandatory";
  | Ok mac ->
    Alcotest.(check macaddr) "mismatch for expected query value" expected_mac mac;
    Lwt.return_unit
  | Error e -> failf "ARP query failed with %a" A.pp_error e

let query_and_no_response arp ip =
  A.query arp ip >>= function
  | Error `Timeout ->
    Log.warn (fun f -> f "Timeout querying %a. Table contents: %a" Ipaddr.V4.pp ip A.pp arp);
    Lwt.return_unit
  | Ok _ -> failf "expected nothing, found something in cache"
  | Error e ->
    Log.err (fun m -> m "another err");
    failf "ARP query failed with %a" A.pp_error e

let set_and_check ~listener ~claimant ip =
  A.set_ips claimant.arp [ ip ] >>= fun () ->
  Log.debug (fun f -> f "Set IP for %a to %a" Macaddr.pp (V.mac claimant.netif) Ipaddr.V4.pp ip);
  Logs.debug (fun f -> f "Listener table contents after IP set on claimant: %a" A.pp listener);
  query_or_die listener ip (V.mac claimant.netif)

let start_arp_listener stack () =
  let noop = (fun _ -> Lwt.return_unit) in
  Log.debug (fun f -> f "starting arp listener for %a" Macaddr.pp (V.mac stack.netif));
  let arpv4 frame =
    Log.debug (fun f -> f "frame received for arpv4");
    A.input stack.arp frame
  in
  E.input ~arpv4 ~ipv4:noop ~ipv6:noop stack.ethif

let not_in_cache ~listen probe arp ip =
  Lwt.pick [
    single_check listen probe;
    Mirage_sleep.ns (Duration.of_ms 100) >>= fun () ->
    A.query arp ip >>= function
    | Ok _ -> failf "entry in cache when it shouldn't be %a" Ipaddr.V4.pp ip
    | Error `Timeout -> Lwt.return_unit
    | Error e -> failf "error for %a while reading the cache: %a"
                   Ipaddr.V4.pp ip A.pp_error e
  ]

let set_ip_sends_garp () =
  two_arp () >>= fun (speak, listen) ->
  let emit_garp =
    Mirage_sleep.ns (Duration.of_ms 100) >>= fun () ->
    A.set_ips speak.arp [ first_ip ] >>= fun () ->
    Alcotest.(check (list ip)) "garp emitted when setting ip" [ first_ip ] (A.get_ips speak.arp);
    Lwt.return_unit
  in
  let expected_garp = garp (V.mac speak.netif) first_ip in
  timeout ~time:500 (
  Lwt.join [
    single_check listen.netif expected_garp;
    emit_garp;
  ]) >>= fun () ->
  (* now make sure we have consistency when setting *)
  A.set_ips speak.arp [] >>= fun () ->
  Alcotest.(check (slist ip Ipaddr.V4.compare)) "list of bound IPs on initialization" [] (A.get_ips speak.arp);
  A.set_ips speak.arp [ first_ip; second_ip ] >>= fun () ->
  Alcotest.(check (slist ip Ipaddr.V4.compare)) "list of bound IPs after setting two IPs"
    [ first_ip; second_ip ] (A.get_ips speak.arp);
  Lwt.return_unit

let add_get_remove_ips () =
  get_arp () >>= fun stack ->
  let check str expected =
    Alcotest.(check (list ip)) str expected (A.get_ips stack.arp)
  in
  check "bound ips is an empty list on startup" [];
  A.set_ips stack.arp [ first_ip; first_ip ] >>= fun () ->
  check "set ips with duplicate elements result in deduplication" [first_ip];
  A.remove_ip stack.arp first_ip >>= fun () ->
  check "ip list is empty after removing only ip" [];
  A.remove_ip stack.arp first_ip >>= fun () ->
  check "ip list is empty after removing from empty list" [];
  A.add_ip stack.arp first_ip >>= fun () ->
  check "first ip is the only member of the set of bound ips" [first_ip];
  A.add_ip stack.arp first_ip >>= fun () ->
  check "adding ips is idempotent" [first_ip];
  Lwt.return_unit

let input_single_garp () =
  two_arp () >>= fun (listen, speak) ->
  (* set the IP on speak_arp, which should cause a GARP to be emitted which
     listen_arp will hear and cache. *)
  let one_and_done buf =
    let arpbuf = Cstruct.shift buf 14 in
    A.input listen.arp arpbuf >>= fun () ->
    V.disconnect listen.netif
  in
  timeout ~time:500 (
    Lwt.join [
      (V.listen listen.netif ~header_size one_and_done >|= fun _ -> ());
      Mirage_sleep.ns (Duration.of_ms 100) >>= fun () ->
      Lwt.async (fun () -> A.query listen.arp first_ip >|= ignore) ;
      A.set_ips speak.arp [ first_ip ];
    ])
    >>= fun () ->
  (* try a lookup of the IP set by speak.arp, and fail if this causes listen_arp
     to block or send an ARP query -- listen_arp should answer immediately from
     the cache.  An attempt to resolve via query will result in a timeout, since
     speak.arp has no listener running and therefore won't answer any arp
     who-has requests. *)
    timeout ~time:500 (query_or_die listen.arp first_ip (V.mac speak.netif)) (* >>= fun () ->
                                                                                Time.sleep_ns (Duration.of_sec 5) *)

let input_single_unicast () =
  two_arp () >>= fun (listen, speak) ->
  (* contrive to make a reply packet for the listener to hear *)
  let for_listener =
    arp_reply
      ~from_netif:speak.netif ~to_netif:listen.netif
      ~from_ip:first_ip ~to_ip:second_ip
  in
  let listener = start_arp_listener listen () in
  timeout ~time:500 (
  Lwt.choose [
    (V.listen listen.netif ~header_size listener >|= fun _ -> ());
    Mirage_sleep.ns (Duration.of_ms 2) >>= fun () ->
    E.write speak.ethif (V.mac listen.netif) `ARP ~size for_listener >>= fun _ ->
    query_and_no_response listen.arp first_ip
  ])

let input_resolves_wait () =
  two_arp () >>= fun (listen, speak) ->
  (* contrive to make a reply packet for the listener to hear *)
  let for_listener = arp_reply ~from_netif:speak.netif ~to_netif:listen.netif
                         ~from_ip:first_ip ~to_ip:second_ip in
  (* initiate query when the cache is empty.  On resolution, fail for a timeout
     and test the MAC if resolution was successful, then disconnect the
     listening interface to ensure the test terminates.
     Fail with a timeout message if the whole thing takes more than 5s. *)
  let listener = start_arp_listener listen () in
  let query_then_disconnect =
    query_or_die listen.arp first_ip (V.mac speak.netif) >>= fun () ->
    V.disconnect listen.netif
  in
  timeout ~time:5000 (
    Lwt.join [
      (V.listen listen.netif ~header_size listener >|= fun _ -> ());
      query_then_disconnect;
      Mirage_sleep.ns (Duration.of_ms 1) >>= fun () ->
      E.write speak.ethif (V.mac listen.netif) `ARP ~size for_listener >|= function
      | Ok x -> x
      | Error _ -> failf "ethernet write failed"
    ]
  )

let unreachable_times_out () =
  get_arp () >>= fun speak ->
  A.query speak.arp first_ip >>= function
  | Ok _ -> failf "query claimed success when impossible for %a" Ipaddr.V4.pp first_ip
  | Error `Timeout -> Lwt.return_unit
  | Error e -> failf "error waiting for a timeout: %a" A.pp_error e

let input_replaces_old () =
  three_arp () >>= fun (listen, claimant_1, claimant_2) ->
  (* query for IP to accept responses *)
  Lwt.async (fun () -> A.query listen.arp first_ip >|= ignore) ;
  Lwt.async (fun () ->
      Log.debug (fun f -> f "arp listener started");
      V.listen listen.netif ~header_size (start_arp_listener listen ()) >|= fun _ -> ());
  timeout ~time:2000 (
    set_and_check ~listener:listen.arp ~claimant:claimant_1 first_ip >>= fun () ->
    set_and_check ~listener:listen.arp ~claimant:claimant_2 first_ip >>= fun () ->
    V.disconnect listen.netif
    )

let os_linux_bsd () =
  let cmd = Bos.Cmd.(v "uname" % "-s") in
  match Bos.OS.Cmd.(run_out cmd |> out_string |> success) with
  | Ok s when s = "FreeBSD" -> true
  | Ok s when s = "Linux" -> true
  | Ok _ -> false
  | Error _ -> false

let entries_expire () =
  (* this test fails on windows and macOS for unknown reasons. please, if you
     happen to have your hands on such a machine, investigate the issue. *)
  if not (os_linux_bsd ()) then
    Lwt.return_unit
  else
    two_arp () >>= fun (listen, speak) ->
    A.set_ips listen.arp [ second_ip ] >>= fun () ->
    (* here's what we expect listener to emit once its cache entry has expired *)
    let expected_arp_query =
      Arp_packet.({operation = Request;
                   source_mac = V.mac listen.netif;
                   target_mac = Macaddr.broadcast;
                   source_ip = second_ip; target_ip = first_ip})
    in
    (* query for IP to accept responses *)
    Lwt.async (fun () -> A.query listen.arp first_ip >|= ignore) ;
    Lwt.async (fun () -> V.listen listen.netif ~header_size (start_arp_listener listen ()) >|= fun _ -> ());
    let test =
      Mirage_sleep.ns (Duration.of_ms 10) >>= fun () ->
      set_and_check ~listener:listen.arp ~claimant:speak first_ip >>= fun () ->
      (* sleep for 5s to make sure we hit `tick` often enough *)
      Mirage_sleep.ns (Duration.of_sec 5) >>= fun () ->
      (* asking now should generate a query *)
      not_in_cache ~listen:speak.netif expected_arp_query listen.arp first_ip
    in
    timeout ~time:7000 test

(* RFC isn't strict on how many times to try, so we'll just say any number
   greater than 1 is fine *)
let query_retries () =
  two_arp () >>= fun (listen, speak) ->
  let expected_query = Arp_packet.({source_mac = V.mac speak.netif;
                                    target_mac = Macaddr.broadcast;
                                    source_ip = Ipaddr.V4.any;
                                    target_ip = first_ip;
                                    operation = Request;})
  in
  let how_many = ref 0 in
  let listener buf =
    check_ethif_response expected_query buf;
    if !how_many = 0 then begin
      how_many := !how_many + 1;
      Lwt.return_unit
    end else V.disconnect listen.netif
  in
  let ask () =
    A.query speak.arp first_ip >>= function
    | Error e -> failf "Received error before >1 query: %a" A.pp_error e
    | Ok _ -> failf "got result from query for %a, erroneously" Ipaddr.V4.pp first_ip
  in
  Lwt.pick [
    (V.listen listen.netif ~header_size listener >|= fun _ -> ());
    Mirage_sleep.ns (Duration.of_ms 2) >>= ask;
    Mirage_sleep.ns (Duration.of_sec 6) >>= fun () ->
    fail "query didn't succeed or fail within 6s"
  ]

(* requests for us elicit a reply *)
let requests_are_responded_to () =
  let (answerer_ip, inquirer_ip) = (first_ip, second_ip) in
  two_arp () >>= fun (inquirer, answerer) ->
  (* neither has a listener set up when we set IPs, so no GARPs in the cache *)
  A.add_ip answerer.arp answerer_ip >>= fun () ->
  A.add_ip inquirer.arp inquirer_ip >>= fun () ->
  let request = arp_request ~from_netif:inquirer.netif ~to_mac:Macaddr.broadcast
      ~from_ip:inquirer_ip ~to_ip:answerer_ip
  in
  let expected_reply =
    Arp_packet.({ operation = Reply;
                  source_mac = V.mac answerer.netif;
                  target_mac = V.mac inquirer.netif;
                  source_ip = answerer_ip; target_ip = inquirer_ip})
  in
  let listener close_netif buf =
    check_ethif_response expected_reply buf;
    V.disconnect close_netif
  in
  let arp_listener =
    V.listen answerer.netif ~header_size (start_arp_listener answerer ()) >|= fun _ -> ()
  in
  timeout ~time:1000 (
    Lwt.join [
      (* listen for responses and check them against an expected result *)
      (V.listen inquirer.netif ~header_size (listener inquirer.netif) >|= fun _ -> ());
      (* start the usual ARP listener, which should respond to requests *)
      arp_listener;
      (* send a request for the ARP listener to respond to *)
      Mirage_sleep.ns (Duration.of_ms 100) >>= fun () ->
      E.write inquirer.ethif Macaddr.broadcast `ARP ~size request >>= fun _ ->
      Mirage_sleep.ns (Duration.of_ms 100) >>= fun () ->
      V.disconnect answerer.netif
    ];
  )

let requests_not_us () =
  let (answerer_ip, inquirer_ip) = (first_ip, second_ip) in
  two_arp () >>= fun (answerer, inquirer) ->
  A.add_ip answerer.arp answerer_ip >>= fun () ->
  A.add_ip inquirer.arp inquirer_ip >>= fun () ->
  let ask ip buf =
    let open Arp_packet in
    encode_into
      { operation = Request;
        source_mac = V.mac inquirer.netif; target_mac = Macaddr.broadcast;
        source_ip = inquirer_ip; target_ip = ip }
      buf ;
    size
  in
  let requests = List.map ask [ inquirer_ip; Ipaddr.V4.any;
                                Ipaddr.V4.of_string_exn "255.255.255.255" ] in
  let make_requests =
    Lwt_list.iter_s (fun b -> E.write inquirer.ethif Macaddr.broadcast `ARP ~size b >|= fun _ -> ())
      requests
  in
  let disconnect_listeners () =
    Lwt_list.iter_s (V.disconnect) [answerer.netif; inquirer.netif]
  in
  Lwt.join [
    (V.listen answerer.netif ~header_size (start_arp_listener answerer ()) >|= fun _ -> ());
    (V.listen inquirer.netif ~header_size (fail_on_receipt inquirer.netif) >|= fun _ -> ());
    make_requests >>= fun _ ->
    Mirage_sleep.ns (Duration.of_ms 100) >>=
    disconnect_listeners
  ]

let nonsense_requests () =
  let (answerer_ip, inquirer_ip) = (first_ip, second_ip) in
  three_arp () >>= fun (answerer, inquirer, checker) ->
  A.set_ips answerer.arp [ answerer_ip ] >>= fun () ->
  let request number arp =
    let open Arp_packet in
    encode_into
      { operation = Request;
	source_mac = V.mac inquirer.netif;
	target_mac = Macaddr.broadcast;
	source_ip = inquirer_ip;
	target_ip = answerer_ip } arp ;
    Cstruct.BE.set_uint16 arp 6 number;
    Arp_packet.size
  in
  let requests = List.map request [0; 3; -1; 255; 256; 257; 65536] in
  let make_requests =
    Lwt_list.iter_s (fun l -> E.write inquirer.ethif Macaddr.broadcast `ARP ~size l >|= fun _ -> ()) requests in
  let expected_probe = Arp_packet.{ operation = Request;
                                    source_mac = V.mac answerer.netif;
                                    source_ip = answerer_ip;
                                    target_mac = Macaddr.broadcast;
                                    target_ip = inquirer_ip; }
  in
  Lwt.async (fun () -> V.listen answerer.netif ~header_size (start_arp_listener answerer ()) >|= fun _ -> ());
  timeout ~time:1000 (
    Lwt.join [
      (V.listen inquirer.netif ~header_size (fail_on_receipt inquirer.netif) >|= fun _ -> ());
      make_requests >>= fun () ->
      V.disconnect inquirer.netif >>= fun () ->
      (* not sufficient to just check to see whether we've replied; it's equally
         possible that we erroneously make a cache entry.  Make sure querying
         inquirer_ip results in an outgoing request. *)
      not_in_cache ~listen:checker.netif expected_probe answerer.arp inquirer_ip
    ] )

let packet () =
  let first_mac  = Macaddr.of_string_exn "10:9a:dd:01:23:45" in
  let second_mac = Macaddr.of_string_exn "00:16:3e:ab:cd:ef" in
  let example_request =
    Arp_packet.{ operation = Request;
                 source_mac = first_mac;
                 target_mac = second_mac;
                 source_ip = first_ip;
                 target_ip = second_ip;
               }
  in
  let marshalled = Arp_packet.encode example_request in
  match Arp_packet.decode marshalled with
  | Error _ -> Alcotest.fail "couldn't unmarshal something we made ourselves"
  | Ok unmarshalled ->
    Alcotest.(check packet) "serialize/deserialize" example_request unmarshalled;
    Lwt.return_unit

let suite =
  [
    "conversions neither lose nor gain information", `Quick, packet;
    "nonsense requests are ignored", `Quick, nonsense_requests;
    "requests are responded to", `Quick, requests_are_responded_to;
    "entries expire", `Quick, entries_expire;
    "irrelevant requests are ignored", `Quick, requests_not_us;
    "set_ip sets ip, sends GARP", `Quick, set_ip_sends_garp;
    "add_ip, get_ip and remove_ip as advertised", `Quick, add_get_remove_ips;
    "GARPs are heard and not cached", `Quick, input_single_garp;
    "unsolicited unicast replies are heard and not cached", `Quick, input_single_unicast;
    "solicited unicast replies resolve pending threads", `Quick, input_resolves_wait;
    "entries are replaced with new information", `Quick, input_replaces_old;
    "unreachable IPs time out", `Quick, unreachable_times_out;
    "queries are tried repeatedly before timing out", `Quick, query_retries;
  ]

let run test () =
  Lwt_main.run (test ())

let () =
  (* enable logging to stdout for all modules *)
  Logs.set_reporter (Logs_fmt.reporter ());
  Logs.set_level ~all:true (Some Logs.Debug);
  let suite =
    [ "arp", List.map (fun (d, s, f) -> d, s, run f) suite ]
  in
  Alcotest.run "arp" suite
