/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.dispatch;

import org.apache.pekko.testkit.PekkoJUnitActorSystemResource;
import org.apache.pekko.actor.ActorSystem;

import org.apache.pekko.japi.*;
import org.junit.ClassRule;
import org.scalatestplus.junit.JUnitSuite;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.Promise;
import scala.concurrent.duration.Duration;
import org.junit.Test;
import static org.junit.Assert.*;

import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.LinkedList;
import java.lang.Iterable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.apache.pekko.japi.Util.classTag;

import org.apache.pekko.testkit.PekkoSpec;

public class JavaFutureTests extends JUnitSuite {

  @ClassRule
  public static PekkoJUnitActorSystemResource actorSystemResource =
    new PekkoJUnitActorSystemResource("JavaFutureTests", PekkoSpec.testConf());

  private final ActorSystem system = actorSystemResource.getSystem();
  private final Duration timeout = Duration.create(5, TimeUnit.SECONDS);

  @Test
  public void mustBeAbleToMapAFuture() throws Exception {

    Future<String> f1 = Futures.future(new Callable<String>() {
      public String call() {
        return "Hello";
      }
    }, system.dispatcher());

    Future<String> f2 = f1.map(new Mapper<String, String>() {
      public String apply(String s) {
        return s + " World";
      }
    }, system.dispatcher());

    assertEquals("Hello World", Await.result(f2, timeout));
  }

  @Test
  public void mustBeAbleToExecuteAnOnResultCallback() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    Future<String> f = cf.future();
    f.onComplete(new OnComplete<String>() {
      public void onComplete(Throwable t, String r) {
        if ("foo".equals(r))
          latch.countDown();
      }
    }, system.dispatcher());

    cf.success("foo");
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertEquals("foo", Await.result(f, timeout));
  }

  @Test
  public void mustBeAbleToExecuteAnOnExceptionCallback() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    Future<String> f = cf.future();
    f.onComplete(new OnComplete<String>() {
      public void onComplete(Throwable t, String r) {
        // 'null instanceof ...' is always false
        if (t instanceof NullPointerException)
          latch.countDown();
      }
    }, system.dispatcher());

    Throwable exception = new NullPointerException();
    cf.failure(exception);
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertEquals(exception, f.value().get().failed().get());
  }

  @Test
  public void mustBeAbleToExecuteAnOnCompleteCallback() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    Future<String> f = cf.future();
    f.onComplete(new OnComplete<String>() {
      public void onComplete(Throwable t, String r) {
        latch.countDown();
      }
    }, system.dispatcher());

    cf.success("foo");
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertEquals("foo", Await.result(f, timeout));
  }

  @Test
  public void mustBeAbleToForeachAFuture() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    Future<String> f = cf.future();
    f.foreach(new Foreach<String>() {
      public void each(String future) {
        latch.countDown();
      }
    },system.dispatcher());

    cf.success("foo");
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertEquals("foo", Await.result(f, timeout));
  }

  @Test
  public void mustBeAbleToFlatMapAFuture() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    cf.success("1000");
    Future<String> f = cf.future();
    Future<Integer> r = f.flatMap(new Mapper<String, Future<Integer>>() {
      public Future<Integer> checkedApply(String r) throws Throwable {
        if (false) throw new IOException("Just here to make sure this compiles.");
        latch.countDown();
        Promise<Integer> cf = Futures.promise();
        cf.success(Integer.parseInt(r));
        return cf.future();
      }
    }, system.dispatcher());

    assertEquals("1000", Await.result(f, timeout));
    assertEquals(1000, Await.result(r, timeout).intValue());
    assertTrue(latch.await(5, TimeUnit.SECONDS));
  }

  @Test
  public void mustBeAbleToFilterAFuture() throws Throwable {
    final CountDownLatch latch = new CountDownLatch(1);
    Promise<String> cf = Futures.promise();
    Future<String> f = cf.future();
    Future<String> r = f.filter(Filter.filterOf(new Function<String, Boolean>() {
      public Boolean apply(String r) {
        latch.countDown();
        return r.equals("foo");
      }
    }), system.dispatcher());

    cf.success("foo");
    assertTrue(latch.await(5, TimeUnit.SECONDS));
    assertEquals("foo", Await.result(f, timeout));
    assertEquals("foo", Await.result(r, timeout));
  }

  // TODO: Improve this test, perhaps with an Actor
  @Test
  public void mustSequenceAFutureList() throws Exception{
    LinkedList<Future<String>> listFutures = new LinkedList<>();
    LinkedList<String> listExpected = new LinkedList<>();

    for (int i = 0; i < 10; i++) {
      listExpected.add("test");
      listFutures.add(Futures.future(new Callable<String>() {
        public String call() {
          return "test";
        }
      }, system.dispatcher()));
    }

    Future<Iterable<String>> futureList = Futures.sequence(listFutures, system.dispatcher());

    assertEquals(listExpected, Await.result(futureList, timeout));
  }

  // TODO: Improve this test, perhaps with an Actor
  @Test
  public void foldForJavaApiMustWork() throws Exception{
    LinkedList<Future<String>> listFutures = new LinkedList<>();
    StringBuilder expected = new StringBuilder();

    for (int i = 0; i < 10; i++) {
      expected.append("test");
      listFutures.add(Futures.future(new Callable<String>() {
        public String call() {
          return "test";
        }
      }, system.dispatcher()));
    }

    Future<String> result = Futures.fold("", listFutures, new Function2<String, String, String>() {
      public String apply(String r, String t) {
        return r + t;
      }
    }, system.dispatcher());

    assertEquals(expected.toString(), Await.result(result, timeout));
  }

  @Test
  public void reduceForJavaApiMustWork() throws Exception{
    LinkedList<Future<String>> listFutures = new LinkedList<>();
    StringBuilder expected = new StringBuilder();

    for (int i = 0; i < 10; i++) {
      expected.append("test");
      listFutures.add(Futures.future(new Callable<String>() {
        public String call() {
          return "test";
        }
      }, system.dispatcher()));
    }

    Future<String> result = Futures.reduce(listFutures, new Function2<String, String, String>() {
      public String apply(String r, String t) {
        return r + t;
      }
    }, system.dispatcher());

    assertEquals(expected.toString(), Await.result(result, timeout));
  }

  @Test
  public void traverseForJavaApiMustWork() throws Exception{
    LinkedList<String> listStrings = new LinkedList<>();
    LinkedList<String> expectedStrings = new LinkedList<>();

    for (int i = 0; i < 10; i++) {
      expectedStrings.add("TEST");
      listStrings.add("test");
    }

    Future<Iterable<String>> result = Futures.traverse(listStrings, new Function<String, Future<String>>() {
      public Future<String> apply(final String r) {
        return Futures.future(new Callable<String>() {
          public String call() {
            return r.toUpperCase();
          }
        }, system.dispatcher());
      }
    }, system.dispatcher());

    assertEquals(expectedStrings, Await.result(result, timeout));
  }

  @Test
  public void findForJavaApiMustWork() throws Exception{
    LinkedList<Future<Integer>> listFutures = new LinkedList<>();
    for (int i = 0; i < 10; i++) {
      final Integer fi = i;
      listFutures.add(Futures.future(new Callable<Integer>() {
        public Integer call() {
          return fi;
        }
      }, system.dispatcher()));
    }
    final Integer expect = 5;
    Future<Option<Integer>> f = Futures.find(listFutures, new Function<Integer, Boolean>() {
      public Boolean apply(Integer i) {
        return i == 5;
      }
    }, system.dispatcher());

    assertEquals(expect, Await.result(f, timeout).get());
  }

  @Test
  public void blockMustBeCallable() throws Exception {
    Promise<String> p = Futures.promise();
    Duration d = Duration.create(1, TimeUnit.SECONDS);
    p.success("foo");
    Await.ready(p.future(), d);
    assertEquals("foo", Await.result(p.future(), d));
  }

  @Test
  public void mapToMustBeCallable() throws Exception {
    Promise<Object> p = Futures.promise();
    Future<String> f = p.future().mapTo(classTag(String.class));
    Duration d = Duration.create(1, TimeUnit.SECONDS);
    p.success("foo");
    Await.ready(p.future(), d);
    assertEquals("foo", Await.result(p.future(), d));
  }

  @Test
  public void recoverToMustBeCallable() throws Exception {
    final IllegalStateException fail = new IllegalStateException("OHNOES");
    Promise<Object> p = Futures.promise();
    Future<Object> f = p.future().recover(new Recover<Object>() {
      public Object recover(Throwable t) throws Throwable {
        if (t == fail)
          return "foo";
        else
          throw t;
      }
    }, system.dispatcher());
    Duration d = Duration.create(1, TimeUnit.SECONDS);
    p.failure(fail);
    assertEquals("foo", Await.result(f, d));
  }

  @Test
  public void recoverWithToMustBeCallable() throws Exception{
    final IllegalStateException fail = new IllegalStateException("OHNOES");
    Promise<Object> p = Futures.promise();
    Future<Object> f = p.future().recoverWith(new Recover<Future<Object>>() {
      public Future<Object> recover(Throwable t) throws Throwable {
        if (t == fail)
          return Futures.successful("foo");
        else
          throw t;
      }
    }, system.dispatcher());
    Duration d = Duration.create(1, TimeUnit.SECONDS);
    p.failure(fail);
    assertEquals("foo", Await.result(f, d));
  }
}
