(* This file is part of Lwt, released under the MIT license. See LICENSE.md for
   details, or visit https://github.com/ocsigen/lwt/blob/master/LICENSE.md. *)


type test = {
  test_name : string;
  skip_if_this_is_false : unit -> bool;
  sequential : bool;
  run : unit -> bool Lwt.t;
}

type outcome =
  | Passed
  | Failed
  | Exception of exn
  | Skipped

exception Skip
exception Duplicate_Test_Names of string

let test_direct test_name ?(only_if = fun () -> true) run =
  let run =
    fun () ->
      Lwt.return (run ())
  in
  {test_name; skip_if_this_is_false = only_if; sequential = false; run}

let test test_name ?(only_if = fun () -> true) ?(sequential = false) run =
  {test_name; skip_if_this_is_false = only_if; sequential; run}

module Log =
struct
  let log_file =
    let pid = Unix.getpid () in
    let ms = Unix.gettimeofday () |> modf |> fst in
    let filename = Printf.sprintf "test.%i.%03.0f.log" pid (ms *. 1e3) in
    open_out filename
  let () =
    at_exit (fun () -> close_out_noerr log_file)

  let start_time = ref None
  let elapsed () =
    let now = Unix.gettimeofday () in
    match !start_time with
    | None ->
      start_time := Some now;
      0.
    | Some start_time ->
      now -. start_time

  let write identifier message =
    Printf.ksprintf (output_string log_file) "%s [%07.3f]: %s\n"
      identifier (mod_float (elapsed ()) 1000.) message;
    flush log_file
  let log k =
    k (fun identifier ->
      Printf.ksprintf (write identifier))
end

let log = Log.log

let run_test : test -> outcome Lwt.t = fun test ->
  if test.skip_if_this_is_false () = false then begin
    log @@ (fun k -> k test.test_name "skipping");
    Lwt.return Skipped
  end

  else begin
    let start_time = Unix.gettimeofday () in
    log @@ (fun k -> k test.test_name "starting");

    (* Lwt.async_exception_hook handling inspired by
         https://github.com/mirage/alcotest/issues/45 *)
    let async_exception_promise, async_exception_occurred = Lwt.task () in
    let old_async_exception_hook = !Lwt.async_exception_hook in
    Lwt.async_exception_hook := (fun exn ->
      Lwt.wakeup_later async_exception_occurred (Exception exn));

    Lwt.finalize
      (fun () ->
        let test_completion_promise =
          Lwt.try_bind
            (fun () ->
              test.run ())

            (fun test_did_pass ->
              if test_did_pass then
                Lwt.return Passed
              else
                Lwt.return Failed)

            (function
            | Skip ->
              Lwt.return Skipped

            | exn_raised_by_test ->
              Lwt.return (Exception exn_raised_by_test))
        in

        Lwt.pick [test_completion_promise; async_exception_promise])

      (fun () ->
        Lwt.async_exception_hook := old_async_exception_hook;
        let elapsed = Unix.gettimeofday () -. start_time in
        log @@ (fun k -> k test.test_name "finished in %.3f s" elapsed);
        Lwt.return_unit)
  end

let outcome_to_character : outcome -> string = function
  | Passed -> "."
  | Failed -> "F"
  | Exception _ -> "E"
  | Skipped -> "S"



type suite = {
  suite_name : string;
  suite_tests : test list;
  skip_suite_if_this_is_false : unit -> bool;
}

let contains_dup_tests suite tests =
  let names =
    List.map (fun t -> "suite:" ^ suite ^ " test:" ^ t.test_name) tests in
  let sorted_unique_names = List.sort_uniq String.compare names in
  let counts =
    List.map (fun x ->
      let tests = List.find_all (fun y -> y = x) names in
      (x, List.length tests)) sorted_unique_names in
  let dups = List.filter (fun (_, count) -> count > 1) counts |>
                          List.map (fun (name, _) -> name) in
  if List.length dups > 0 then
    Some dups
  else
    None

let suite name ?(only_if = fun () -> true) tests =
  match contains_dup_tests name tests with
    | Some names -> raise (Duplicate_Test_Names (String.concat ", " names))
    | None -> ();
  {suite_name = name;
   suite_tests = tests;
   skip_suite_if_this_is_false = only_if}

let run_test_suite : suite -> ((string * outcome) list) Lwt.t = fun suite ->
  if suite.skip_suite_if_this_is_false () = false then
    let outcomes =
      suite.suite_tests
      |> List.map (fun {test_name; _} -> (test_name, Skipped))
    in
    (outcome_to_character Skipped).[0]
    |> String.make (List.length outcomes)
    |> print_string;
    flush stdout;

    Lwt.return outcomes

  else
    suite.suite_tests |> Lwt_list.map_s begin fun test ->
      Lwt.bind (run_test test) (fun outcome ->
      outcome |> outcome_to_character |> print_string;
      flush stdout;
      Lwt.return (test.test_name, outcome))
    end

let outcomes_all_ok : (_ * outcome) list -> bool = fun outcomes ->
  outcomes
  |> List.for_all (fun (_test_name, outcome) ->
    match outcome with
    | Passed | Skipped -> true
    | Failed | Exception _ -> false)

let show_failures : (string * outcome) list -> unit =
  List.iter (fun (test_name, outcome) ->
    match outcome with
    | Passed
    | Skipped ->
      ()

    | Failed ->
      Printf.eprintf
        "Test '%s' produced 'false'\n" test_name

    | Exception exn ->
      Printf.eprintf
        "Test '%s' raised '%s'\n" test_name (Printexc.to_string exn))



type ('a, 'b) aggregated_outcomes = ('a * (('b * outcome) list)) list

let fold_over_outcomes :
    ('a -> outcome -> 'a) ->
    'a ->
    (_, _) aggregated_outcomes ->
      'a =
    fun f init outcomes ->

  List.fold_left (fun accumulator (_suite_name, test_outcomes) ->
    List.fold_left (fun accumulator (_test_name, test_outcome) ->
      f accumulator test_outcome)
      accumulator
      test_outcomes)
    init
    outcomes

let count_ran : (_, _) aggregated_outcomes -> int = fun outcomes ->
  outcomes
  |> fold_over_outcomes
    (fun count -> function
      | Skipped ->
        count
      | _ ->
        count + 1)
    0

let count_skipped : (_, _) aggregated_outcomes -> int = fun outcomes ->
  outcomes
  |> fold_over_outcomes
    (fun count -> function
      | Skipped ->
        count + 1
      | _ ->
        count)
    0

(* Runs a series of test suites. If one of the test suites fails, does not run
   subsequent suites. *)
let run library_name suites =
  Printexc.record_backtrace true;

  Printexc.register_printer (function
    | Failure message -> Some (Printf.sprintf "Failure(%S)" message)
    | _ -> None);

  Printf.printf "Testing library '%s'...\n" library_name;

  let start_time = Unix.gettimeofday () in

  let rec loop_over_suites aggregated_outcomes suites =
    match suites with
    | [] ->
      let end_time = Unix.gettimeofday () in
      Printf.printf
        "\nOk. %i tests ran, %i tests skipped in %.2f seconds\n"
        (count_ran aggregated_outcomes)
        (count_skipped aggregated_outcomes)
        (end_time -. start_time);
      Lwt.return_unit

    | suite::rest ->
      Lwt.bind (run_test_suite suite) begin fun outcomes ->
        if not (outcomes_all_ok outcomes) then begin
          print_newline ();
          flush stdout;
          Printf.eprintf "Failures in test suite '%s':\n" suite.suite_name;
          show_failures outcomes;
          exit 1
        end
        else
          loop_over_suites
            ((suite.suite_name, outcomes)::aggregated_outcomes) rest
      end
  in

  loop_over_suites [] suites
  |> Lwt_main.run

let concurrent library_name suites =
  Printexc.register_printer (function
    | Failure message -> Some (Printf.sprintf "Failure(%S)" message)
    | _ -> None);

  Printf.printf "Testing library '%s'...\n" library_name;

  let open Lwt.Infix in

  let run_test (suite, test) =
    begin
      if suite.skip_suite_if_this_is_false () = false then
        Lwt.return Skipped
      else
        run_test test
    end
    >|= fun outcome ->
    print_string (outcome_to_character outcome);
    flush stdout;
    ((suite, test), outcome)
  in

  let start_time = Unix.gettimeofday () in

  (* List all the tests. *)
  suites
  |> List.map (fun suite ->
    suite.suite_tests
    |> List.map (fun test ->
      (suite, test)))
  |> List.flatten

  (* Separate the tests that must be run sequentially, and run them. *)
  |> List.partition (fun (_suite, test) -> test.sequential)
  |> fun (sequential, concurrent) ->
    Lwt_list.map_s run_test sequential
  >>= fun sequential_outcomes ->

  (* Run the tests that can be run concurrently. *)
  concurrent
  |> Lwt_list.map_p run_test

  (* Summarize the results. *)
  >>= fun concurrent_outcomes ->
  let outcomes = sequential_outcomes @ concurrent_outcomes in
  if outcomes_all_ok outcomes then
    let end_time = Unix.gettimeofday () in
    let aggregated_outcomes = [(), outcomes] in
    Printf.printf
      "\nOk. %i tests ran, %i tests skipped in %.2f seconds\n"
      (count_ran aggregated_outcomes)
      (count_skipped aggregated_outcomes)
      (end_time -. start_time);
    Lwt.return_unit
  else begin
    print_newline ();
    flush stdout;
    outcomes |> List.iter (function
      | (suite, test), Failed ->
        Printf.eprintf "Test '%s' in suite '%s' produced 'false'\n"
          test.test_name suite.suite_name
      | (suite, test), Exception exn ->
        Printf.eprintf "Test '%s' in suite '%s' raised '%s'\n"
          test.test_name suite.suite_name (Printexc.to_string exn)
      | _ ->
        ());
    exit 1
  end

let concurrent library_name suites =
  Lwt_main.run (concurrent library_name suites)

let with_async_exception_hook hook f =
  let old_hook = !Lwt.async_exception_hook in
  Lwt.async_exception_hook := hook;
  Lwt.finalize f (fun () ->
    Lwt.async_exception_hook := old_hook;
    Lwt.return_unit)

let instrument = function
  | true -> Printf.ksprintf (fun _s -> true)
  | false -> Printf.ksprintf (fun s -> prerr_endline ("\n" ^ s); false)
