open! Base

module type Hashtbl_for_testing = sig
  include Hashtbl.Accessors with type 'key key = 'key
  include Invariant.S2 with type ('key, 'data) t := ('key, 'data) t

  (* we don't define [module Poly : Hashtbl.S_poly] because we want to require only
     the minimal number of constructors necessary to implement the tests, and also avoid
     conflicting with any existing names. *)

  val create_poly : ?size:int -> unit -> ('key, 'data) t
  val of_alist_poly_exn : ('key * 'data) list -> ('key, 'data) t
  val of_alist_poly_or_error : ('key * 'data) list -> ('key, 'data) t Or_error.t
end

module Make (Hashtbl : Hashtbl_for_testing) = struct
  open Poly

  let test_data = [ "a", 1; "b", 2; "c", 3 ]

  let test_hash =
    let h = Hashtbl.create_poly () ~size:10 in
    List.iter test_data ~f:(fun (k, v) -> Hashtbl.set h ~key:k ~data:v);
    h
  ;;

  (* This is a very strong notion of equality on hash tables *)
  let equal t t' equal_data =
    let subtable t t' =
      try
        List.for_all (Hashtbl.keys t) ~f:(fun key ->
          equal_data (Hashtbl.find_exn t key) (Hashtbl.find_exn t' key))
      with
      | Invalid_argument _ -> false
    in
    subtable t t' && subtable t' t
  ;;

  let%test "find" =
    let found = Hashtbl.find test_hash "a" in
    let not_found = Hashtbl.find test_hash "A" in
    Hashtbl.invariant ignore ignore test_hash;
    match found, not_found with
    | Some _, None -> true
    | _ -> false
  ;;

  (* In js_of_ocaml, strings can be hashconst-ed. *)
  let%test ("findi_and_call" [@tags "no-js"]) =
    let our_hash = Hashtbl.copy test_hash in
    let test_string = "test string" in
    Hashtbl.add_exn our_hash ~key:test_string ~data:10;
    let test_string' = "test " ^ "string" in
    assert (not (phys_equal test_string test_string'));
    Hashtbl.findi_and_call
      our_hash
      test_string'
      ~if_found:(fun ~key ~data -> phys_equal test_string key && data = 10)
      ~if_not_found:(fun _ -> false)
  ;;

  let%test_unit "add" =
    let our_hash = Hashtbl.copy test_hash in
    let duplicate = Hashtbl.add our_hash ~key:"a" ~data:4 in
    let no_duplicate = Hashtbl.add our_hash ~key:"d" ~data:5 in
    assert (Hashtbl.find our_hash "a" = Some 1);
    assert (Hashtbl.find our_hash "d" = Some 5);
    Hashtbl.invariant ignore ignore our_hash;
    assert (
      match duplicate, no_duplicate with
      | `Duplicate, `Ok -> true
      | _ -> false)
  ;;

  let%test "iter" =
    let predicted =
      List.sort ~compare:Int.descending (List.map test_data ~f:(fun (_, v) -> v))
    in
    let found =
      let found = ref [] in
      Hashtbl.iter test_hash ~f:(fun v -> found := v :: !found);
      !found |> List.sort ~compare:Int.descending
    in
    List.equal Int.equal predicted found
  ;;

  let%test "iter_keys" =
    let predicted =
      List.sort ~compare:String.descending (List.map test_data ~f:(fun (k, _) -> k))
    in
    let found =
      let found = ref [] in
      Hashtbl.iter_keys test_hash ~f:(fun k -> found := k :: !found);
      !found |> List.sort ~compare:String.descending
    in
    List.equal String.equal predicted found
  ;;

  let%test_module "of_alist" =
    (module struct
      let%test "size" =
        let predicted = List.length test_data in
        let found = Hashtbl.length (Hashtbl.of_alist_poly_exn test_data) in
        predicted = found
      ;;

      let%test "right keys" =
        let predicted = List.map test_data ~f:(fun (k, _) -> k) in
        let found = Hashtbl.keys (Hashtbl.of_alist_poly_exn test_data) in
        let sp = List.sort ~compare:Poly.ascending predicted in
        let sf = List.sort ~compare:Poly.ascending found in
        sp = sf
      ;;
    end)
  ;;

  let%test_module "of_alist_or_error" =
    (module struct
      let%test "unique" = Result.is_ok (Hashtbl.of_alist_poly_or_error test_data)

      let%test "duplicate" =
        Result.is_error (Hashtbl.of_alist_poly_or_error (test_data @ test_data))
      ;;
    end)
  ;;

  let%test "size and right keys" =
    let predicted = List.map test_data ~f:(fun (k, _) -> k) in
    let found = Hashtbl.keys test_hash in
    let sp = List.sort ~compare:Poly.ascending predicted in
    let sf = List.sort ~compare:Poly.ascending found in
    sp = sf
  ;;

  let%test "size and right data" =
    let predicted = List.map test_data ~f:(fun (_, v) -> v) in
    let found = Hashtbl.data test_hash in
    let sp = List.sort ~compare:Poly.ascending predicted in
    let sf = List.sort ~compare:Poly.ascending found in
    sp = sf
  ;;

  let%test "map" =
    let add1 x = x + 1 in
    let predicted_data =
      List.sort ~compare:Poly.ascending (List.map test_data ~f:(fun (k, v) -> k, add1 v))
    in
    let found_alist =
      Hashtbl.map test_hash ~f:add1
      |> Hashtbl.to_alist
      |> List.sort ~compare:Poly.ascending
    in
    List.equal Poly.equal predicted_data found_alist
  ;;

  let%test_unit "filter_map" =
    let f x = Some x in
    let result = Hashtbl.filter_map test_hash ~f in
    assert (equal test_hash result Int.( = ));
    let is_even x = x % 2 = 0 in
    let add1_to_even x = if is_even x then Some (x + 1) else None in
    let predicted_data =
      List.filter_map test_data ~f:(fun (k, v) ->
        if is_even v then Some (k, v + 1) else None)
    in
    let found = Hashtbl.filter_map test_hash ~f:add1_to_even in
    let found_alist = List.sort ~compare:Poly.ascending (Hashtbl.to_alist found) in
    assert (List.equal Poly.equal predicted_data found_alist)
  ;;

  let%test "filter_inplace" =
    let f x = x <> 2 in
    let predicted_data =
      List.sort ~compare:Poly.ascending (List.filter test_data ~f:(fun (_, v) -> f v))
    in
    let test_hash = Hashtbl.copy test_hash in
    Hashtbl.filter_inplace test_hash ~f;
    let found_alist = Hashtbl.to_alist test_hash |> List.sort ~compare:Poly.ascending in
    List.equal Poly.equal predicted_data found_alist
  ;;

  let%test "filter_keys_inplace" =
    let f x = x = "c" in
    let predicted_data =
      List.sort ~compare:Poly.ascending (List.filter test_data ~f:(fun (k, _) -> f k))
    in
    let test_hash = Hashtbl.copy test_hash in
    Hashtbl.filter_keys_inplace test_hash ~f;
    let found_alist = Hashtbl.to_alist test_hash |> List.sort ~compare:Poly.ascending in
    List.equal Poly.equal predicted_data found_alist
  ;;

  let%test "filter_map_inplace" =
    let f x = if x = 1 then None else Some (x * 2) in
    let predicted_data =
      List.sort
        ~compare:Poly.ascending
        (List.filter_map test_data ~f:(fun (k, v) -> Option.map (f v) ~f:(fun x -> k, x)))
    in
    let test_hash = Hashtbl.copy test_hash in
    Hashtbl.filter_map_inplace test_hash ~f;
    let found_alist = Hashtbl.to_alist test_hash |> List.sort ~compare:Poly.ascending in
    List.equal Poly.equal predicted_data found_alist
  ;;

  let%test "map_inplace" =
    let f x = x + 3 in
    let predicted_data =
      List.sort ~compare:Poly.ascending (List.map test_data ~f:(fun (k, v) -> k, f v))
    in
    let test_hash = Hashtbl.copy test_hash in
    Hashtbl.map_inplace test_hash ~f;
    let found_alist = Hashtbl.to_alist test_hash |> List.sort ~compare:Poly.ascending in
    List.equal Poly.equal predicted_data found_alist
  ;;

  let%test_unit "insert-find-remove" =
    let t = Hashtbl.create_poly () ~size:1 in
    let inserted = ref [] in
    Random.init 123;
    let verify_inserted t =
      let missing =
        List.fold !inserted ~init:[] ~f:(fun acc (key, data) ->
          match Hashtbl.find t key with
          | None -> `Missing key :: acc
          | Some d -> if data = d then acc else `Wrong_data (key, data) :: acc)
      in
      match missing with
      | [] -> ()
      | _ ->
        raise_s
          [%message
            "some inserts are missing"
              (missing : [ `Missing of int | `Wrong_data of int * int ] list)]
    in
    let equal = Int.equal in
    let rec loop i t =
      if i < 2000
      then (
        let k = Random.int 10_000 in
        inserted := List.Assoc.add (List.Assoc.remove !inserted ~equal k) ~equal k i;
        Hashtbl.set t ~key:k ~data:i;
        Hashtbl.invariant ignore ignore t;
        verify_inserted t;
        loop (i + 1) t)
    in
    loop 0 t;
    List.iter !inserted ~f:(fun (x, _) ->
      Hashtbl.remove t x;
      Hashtbl.invariant ignore ignore t;
      (match Hashtbl.find t x with
       | None -> ()
       | Some _ -> failwith (Printf.sprintf "present after removal: %d" x));
      inserted := List.Assoc.remove !inserted ~equal x;
      verify_inserted t)
  ;;

  let%test_unit "clear" =
    let t = Hashtbl.create_poly () ~size:1 in
    let l = List.range 0 100 in
    let verify_present l = List.for_all l ~f:(Hashtbl.mem t) in
    let verify_not_present l = List.for_all l ~f:(fun i -> not (Hashtbl.mem t i)) in
    List.iter l ~f:(fun i -> Hashtbl.set t ~key:i ~data:(i * i));
    List.iter l ~f:(fun i -> Hashtbl.set t ~key:i ~data:(i * i));
    assert (Hashtbl.length t = 100);
    assert (verify_present l);
    Hashtbl.clear t;
    Hashtbl.invariant ignore ignore t;
    assert (Hashtbl.length t = 0);
    assert (verify_not_present l);
    let l = List.take l 42 in
    List.iter l ~f:(fun i -> Hashtbl.set t ~key:i ~data:(i * i));
    assert (Hashtbl.length t = 42);
    assert (verify_present l);
    Hashtbl.invariant ignore ignore t
  ;;

  let%test_unit "mem" =
    let t = Hashtbl.create_poly () ~size:1 in
    Hashtbl.invariant ignore ignore t;
    assert (not (Hashtbl.mem t "Fred"));
    Hashtbl.invariant ignore ignore t;
    Hashtbl.set t ~key:"Fred" ~data:"Wilma";
    Hashtbl.invariant ignore ignore t;
    assert (Hashtbl.mem t "Fred");
    Hashtbl.invariant ignore ignore t;
    Hashtbl.remove t "Fred";
    Hashtbl.invariant ignore ignore t;
    assert (not (Hashtbl.mem t "Fred"));
    Hashtbl.invariant ignore ignore t
  ;;

  let%test_unit "exists" =
    let t = Hashtbl.create_poly () in
    assert (not (Hashtbl.exists t ~f:(fun _ -> failwith "can't be called")));
    assert (not (Hashtbl.existsi t ~f:(fun ~key:_ ~data:_ -> failwith "can't be called")));
    Hashtbl.set t ~key:7 ~data:3;
    assert (not (Hashtbl.exists t ~f:(Int.equal 4)));
    Hashtbl.set t ~key:8 ~data:4;
    assert (Hashtbl.exists t ~f:(Int.equal 4));
    Hashtbl.set t ~key:9 ~data:5;
    assert (Hashtbl.existsi t ~f:(fun ~key ~data -> key + data = 14))
  ;;

  let%test_unit "for_all" =
    let t = Hashtbl.create_poly () in
    assert (Hashtbl.for_all t ~f:(fun _ -> failwith "can't be called"));
    assert (Hashtbl.for_alli t ~f:(fun ~key:_ ~data:_ -> failwith "can't be called"));
    Hashtbl.set t ~key:7 ~data:3;
    assert (Hashtbl.for_all t ~f:(fun x -> Int.equal x 3));
    Hashtbl.set t ~key:8 ~data:4;
    assert (not (Hashtbl.for_all t ~f:(fun x -> Int.equal x 3)));
    Hashtbl.set t ~key:9 ~data:5;
    assert (Hashtbl.for_alli t ~f:(fun ~key ~data -> key - 4 = data))
  ;;

  let%test_unit "count" =
    let t = Hashtbl.create_poly () in
    assert (Hashtbl.count t ~f:(fun _ -> failwith "can't be called") = 0);
    assert (Hashtbl.counti t ~f:(fun ~key:_ ~data:_ -> failwith "can't be called") = 0);
    Hashtbl.set t ~key:7 ~data:3;
    assert (Hashtbl.count t ~f:(fun x -> Int.equal x 3) = 1);
    Hashtbl.set t ~key:8 ~data:4;
    assert (Hashtbl.count t ~f:(fun x -> Int.equal x 3) = 1);
    Hashtbl.set t ~key:9 ~data:5;
    assert (Hashtbl.counti t ~f:(fun ~key ~data -> key - 4 = data) = 3)
  ;;

  let%test_unit "merge" =
    let make alist = Hashtbl.of_alist_poly_exn alist in
    let t1 = make [ 1, 111; 2, 222; 3, 333 ] in
    let t2 = make [ 1, 123; 2, 222; 4, 444 ] in
    [%test_result: (int * [ `Left of int | `Right of int | `Both of int * int ]) List.t]
      (Hashtbl.merge t1 t2 ~f:(fun ~key:_ -> function
         | `Left x -> Some (`Left x)
         | `Right y -> Some (`Right y)
         | `Both (x, y) -> if x = y then None else Some (`Both (x, y)))
       |> Hashtbl.to_alist
       |> List.sort ~compare:(fun (x, _) (y, _) -> Int.compare x y))
      ~expect:[ 1, `Both (111, 123); 3, `Left 333; 4, `Right 444 ]
  ;;
end

(* typechecking this code is a compile-time test that [Creators] is a specialization of
   [Creators_generic].  *)
module _ : sig end = struct
  module Make_creators_check
      (Type : T.T2)
      (Key : T.T1)
      (Options : T.T3)
      (_ : Hashtbl.Private.Creators_generic
       with type ('a, 'b) t := ('a, 'b) Type.t
       with type 'a key := 'a Key.t
       with type ('a, 'b, 'z) create_options := ('a, 'b, 'z) Options.t) =
  struct end

  module _ (M : Hashtbl.Creators) =
    Make_creators_check
      (struct
        type ('a, 'b) t = ('a, 'b) M.t
      end)
      (struct
        type 'a t = 'a
      end)
      (struct
        type ('a, 'b, 'z) t = ('a, 'b, 'z) Hashtbl.create_options
      end)
      (struct
        include M

        let create ?growth_allowed ?size m () = create ?growth_allowed ?size m
      end)
end
