(* Copyright (C) 2023--2025  Petter A. Urkedal <paurkedal@gmail.com>
 *
 * 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 3 of the License, or (at your
 * option) any later version, with the LGPL-3.0 Linking Exception.
 *
 * 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
 * and the LGPL-3.0 Linking Exception along with this library.  If not, see
 * <http://www.gnu.org/licenses/> and <https://spdx.org>, respectively.
 *)

module Pool = Caqti_miou_unix.System.Pool

module Resource = struct
  type t = {
    id: int;
    use_count: int Atomic.t;
  }

  let alive = Hashtbl.create 17
  let alive_mutex = Mutex.create ()

  let with_alive_locked f =
    Mutex.lock alive_mutex;
    Fun.protect ~finally:(fun () -> Mutex.unlock alive_mutex) f

  let latest_id = Atomic.make 0

  let create () =
    let id = Atomic.fetch_and_add latest_id 1 in
    with_alive_locked (fun () -> Hashtbl.add alive id ());
    Ok {id; use_count = Atomic.make 0}

  let create_or_fail () =
    if Random.int 4 = 0 then Error () else
    create ()

  let free resource =
    with_alive_locked begin fun () ->
      assert (Hashtbl.mem alive resource.id);
      Hashtbl.remove alive resource.id
    end
end

exception Timeout

let with_timeout ts fn =
  let prm0 = Miou.async fn in
  let prm1 = Miou.async @@ fun () -> Miou_unix.sleep ts; raise Timeout in
  match Miou.await_first [ prm0; prm1 ] with
  | Ok value -> value
  | Error Timeout -> raise Timeout
  | Error exn ->
      Logs.err (fun m -> m "Unexpected exception: %S" (Printexc.to_string exn));
      raise exn

let test_n n =
  Caqti_miou.Switch.run @@ fun sw ->
  let max_idle_size = Random.int 11 in
  let max_size = max 1 (max_idle_size + Random.int 5) in
  let max_use_count =
    (match Random.bool () with
     | false -> None
     | true -> Some (1 + Random.int 8))
  in
  let pool =
    let config =
      Caqti_pool_config.create ~max_idle_size ~max_size ~max_use_count ()
    in
    Pool.create ~config ~sw ~stdenv:()
      Resource.create_or_fail Resource.free
  in
  let wakers = Array.make n None in
  let wait_count = ref 0 in
  let wait_count_cond = Miou.Condition.create () in
  let wait_count_mutex = Miou.Mutex.create () in
  let wake j c =
    ignore (Miou.Computation.try_return c ());
    wakers.(j) <- None in
  for _ = 0 to 3 * n - 1 do
    let j = Random.int n in
    (* NOTE(dinosaure): This assertion is not necessarily true when another
       domain may be in charge of the asynchronous functions and these are not
       actually launched immediately. *)
    (* assert (Pool.size pool = Hashtbl.length Resource.alive); *)
    (match wakers.(j) with
     | None ->
        let c = Miou.Computation.create () in
        Miou.Mutex.protect wait_count_mutex (fun () -> incr wait_count);
        let task (resource : Resource.t) =
          begin match max_use_count with
          | None -> ()
          | Some n -> assert (Atomic.get resource.use_count < n) end;
          Atomic.incr resource.use_count;
          ignore (Miou.Computation.await c);
          Miou.Mutex.protect wait_count_mutex begin fun () ->
            decr wait_count;
            Miou.Condition.signal wait_count_cond
          end;
          Ok ()
        in
        Caqti_miou.System_core.async ~sw begin fun () ->
          begin match Pool.use task pool with
          | Ok () -> ()
          | Error () ->
             ignore (Miou.Computation.await c);
             Miou.Mutex.protect wait_count_mutex begin fun () ->
               decr wait_count;
               Miou.Condition.signal wait_count_cond
             end;
          end
        end;
        wakers.(j) <- Some c
     | Some c -> wake j c)
  done;
  for j = 0 to n - 1 do
    (match wakers.(j) with
     | None -> Miou.yield ()
     | Some c -> wake j c)
  done;
  let rec wait_for_all () =
    Miou.Mutex.lock wait_count_mutex;
    while !wait_count > 0 do
      Miou.Condition.wait wait_count_cond wait_count_mutex;
    done;
    let n = !wait_count in
    Miou.Mutex.unlock wait_count_mutex;
    if n > 0 then wait_for_all ()
  in
  (* NOTE(dinosaure): see the note below, we need to give a better chance to
     wait all tasks. *)
  with_timeout 5.0 wait_for_all;
  if not (Pool.size pool <= max_idle_size) then begin
    (* TODO: Remove condition and CI config after fixing #126. *)
    if Sys.getenv_opt "CAQTI_DEBUGGING_ISSUE_126" = Some "true" then
      Alcotest.failf "%d resources left in pool, expected at most %d"
        (Pool.size pool) max_idle_size
  end;
  Alcotest.(check int) "still waiting" 0 !wait_count;
  Pool.drain pool;
  Alcotest.(check int) "pool size after drain" 0 (Pool.size pool);
  Alcotest.(check int) "alive after drain" 0 (Hashtbl.length Resource.alive)

let test () =
  Miou_unix.run @@ fun () ->
  test_n 0;
  test_n 1;
  (* NOTE(dinosaure): For a simple core, the miou pattern is not the most
     optimised: it's basically a scheduler within a scheduler. So the [Switch]
     ends up in the role of a small scheduler which will execute tasks with
     [async], whereas Miou is a scheduler. The aim is for the [Switch] to
     execute tasks in another domain. In this case, the test execution time is
     reasonable, but for a simple core, this repetition of task management slows
     down the process. We therefore limit the number of tasks to be performed
     for a single core.

     We could offer another [Switch] implementation depending on the number of
     domains available. However, this choice can only be made dynamically. It is
     more reasonable to consider that with miou, we have at least 2 cores
     available. *)
  let max =
    if Miou.Domain.available () > 0
    then 12 else 6 in
  let rec loop n_it =
    if n_it > 0 then begin
      test_n (Random.int (1 lsl max));
      loop (n_it - 1)
    end
  in
  loop 500

let create_gathering n =
  let count = Atomic.make n in
  let c = Miou.Computation.create () in
  fun () ->
    let v = Atomic.fetch_and_add count (-1) in
    Logs.debug (fun m -> m "count:%d" (v - 1));
    if v - 1 > 0 then Miou.Computation.await_exn c
    else begin
      assert (Miou.Computation.try_return c ());
      Logs.debug (fun m -> m "signal others")
    end

let test_age _ =
  Miou_unix.run @@ fun () ->
  Caqti_miou.Switch.run @@ fun sw ->
  let max_size = 8 in
  let max_idle_size = 4 in
  let max_idle_age = Some Mtime.Span.(100 * ms) in
  let pool =
    let config =
      Caqti_pool_config.create ~max_size ~max_idle_size ~max_idle_age ()
    in
    Pool.create ~config ~sw ~stdenv:() Resource.create Resource.free
  in
  let user_count = 8 in
  let join_gathering = create_gathering user_count in
  let f _i _resource =
    Ok (join_gathering ()) in
  let jobs = List.init user_count f in
  let launch job =
    Caqti_miou_unix.System.async ~sw @@ fun () ->
    ignore (Pool.use job pool) in
  List.iter launch jobs;
  (* NOTE(dinosaure): miou does not effectively launch jobs. So, [Pool.use]
     is not yet effectively executed. At this stage, [Pool.size] should be
     equal to [0] but, due to the fact the a domain is probably used to launch
     these jobs, it can be higher than [0]. *)
  (* Alcotest.(check int) "pool size before sleep" 4 (Pool.size pool); *)
  let rec wait_while_draining timeout =
    if Pool.size pool > 0 then begin
      Miou_unix.sleep 0.1; Miou.yield ();
      wait_while_draining (timeout -. 0.1)
    end
  in
  wait_while_draining 5.0;
  Alcotest.(check int) "pool size after sleep" 0 (Pool.size pool)

let test_cases = [
  Alcotest.test_case "basic usage" `Quick test;
  Alcotest.test_case "timed cleanup" `Quick test_age;
]
