/*
 * 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) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.actor.testkit.typed.javadsl

import java.time.Duration

import scala.jdk.DurationConverters._

import org.apache.pekko
import pekko.actor.DeadLetter
import pekko.actor.Dropped
import pekko.actor.UnhandledMessage
import pekko.actor.testkit.typed.TestKitSettings
import pekko.actor.testkit.typed.internal.TestKitUtils
import pekko.actor.testkit.typed.scaladsl
import pekko.actor.typed.ActorRef
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.Behavior
import pekko.actor.typed.Props
import pekko.actor.typed.Scheduler
import pekko.util.Timeout

import com.typesafe.config.Config

object ActorTestKit {

  /**
   * Create a testkit named from the ActorTestKit class.
   *
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   *
   * Config loaded from `application-test.conf` if that exists, otherwise
   * using default configuration from the reference.conf resources that ship with the Akka libraries.
   * The application.conf of your project is not used in this case.
   */
  @noinline def create(): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(TestKitUtils.testNameFromCallStack(classOf[ActorTestKit])))

  /**
   * Create a testkit from the provided actor system.
   *
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   *
   * Config loaded from the provided actor if that exists, otherwise
   * using default configuration from the reference.conf resources that ship with the Akka libraries.
   */
  def create(system: ActorSystem[_]): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(system))

  /**
   * Create a testkit using the provided name.
   *
   * It will create a [[pekko.actor.typed.ActorSystem]] with this name,
   * e.g. threads will include the name.
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   *
   * Config loaded from `application-test.conf` if that exists, otherwise
   * using default configuration from the reference.conf resources that ship with the Akka libraries.
   * The application.conf of your project is not used in this case.
   */
  def create(name: String): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(name))

  /**
   * Create a testkit named from the ActorTestKit class,
   * and use a custom config for the actor system.
   *
   * It will also used the provided customConfig provided to create the `ActorSystem`
   *
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   */
  @noinline def create(customConfig: Config): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(TestKitUtils.testNameFromCallStack(classOf[ActorTestKit]), customConfig))

  /**
   * Create a test kit named based on the provided name,
   * and uses the provided custom config for the actor system.
   *
   * It will create a [[pekko.actor.typed.ActorSystem]] with this name,
   * e.g. threads will include the name.
   *
   * It will also used the provided customConfig provided to create the `ActorSystem`
   *
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   */
  def create(name: String, customConfig: Config): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(name, customConfig))

  /**
   * Create a [[pekko.actor.typed.ActorSystem]] named based on the provided name,
   * use the provided custom config for the actor system, and the testkit will use the provided setting.
   *
   * It will create a [[pekko.actor.typed.ActorSystem]] with this name,
   * e.g. threads will include the name.
   *
   * It will also used the provided customConfig provided to create the `ActorSystem`, and provided setting.
   *
   * When the test has completed you should terminate the `ActorSystem` and
   * the testkit with [[ActorTestKit#shutdownTestKit]].
   */
  def create(name: String, customConfig: Config, settings: TestKitSettings): ActorTestKit =
    new ActorTestKit(scaladsl.ActorTestKit(name, customConfig, settings))

  /**
   * Shutdown the given actor system and wait up to `duration` for shutdown to complete.
   * @param throwIfShutdownTimesOut Fail the test if the system fails to shut down, if false
   *                             an error is printed to stdout when the system did not shutdown but
   *                             no exception is thrown.
   */
  def shutdown(system: ActorSystem[_], duration: Duration, throwIfShutdownTimesOut: Boolean): Unit = {
    TestKitUtils.shutdown(system, duration.toScala, throwIfShutdownTimesOut)
  }

  /**
   * Shutdown the given [[pekko.actor.typed.ActorSystem]] and block until it shuts down,
   * if more time than `system-shutdown-default` passes an exception is thrown
   * (can be configured with `throw-on-shutdown-timeout`).
   */
  def shutdown(system: ActorSystem[_], duration: Duration): Unit = {
    val settings = TestKitSettings.create(system)
    shutdown(system, duration, settings.ThrowOnShutdownTimeout)
  }

  /**
   * Shutdown the given [[pekko.actor.typed.ActorSystem]] and block until it shuts down,
   * if more time than `system-shutdown-default` passes an exception is thrown
   * (can be configured with `throw-on-shutdown-timeout`).
   */
  def shutdown(system: ActorSystem[_]): Unit = {
    val settings = TestKitSettings.create(system)
    shutdown(system, settings.DefaultActorSystemShutdownTimeout.toJava, settings.ThrowOnShutdownTimeout)
  }

  /**
   * Config loaded from `application-test.conf`, which is used if no specific config is given.
   */
  def applicationTestConfig: Config = scaladsl.ActorTestKit.ApplicationTestConfig

}

/**
 * Java API: Test kit for asynchronous testing of typed actors.
 * Provides a typed actor system started on creation, that can be used for multiple test cases and is
 * shut down when `shutdown` is called.
 *
 * The actor system has a custom guardian that allows for spawning arbitrary actors using the `spawn` methods.
 *
 * Designed to work with any test framework, but framework glue code that calls `shutdownTestKit` after all tests has
 * run needs to be provided by the user or with [[TestKitJunitResource]].
 *
 * Use `TestKit.create` factories to construct manually or [[TestKitJunitResource]] to use together with JUnit tests
 *
 * For synchronous testing of a `Behavior` see [[BehaviorTestKit]]
 */
final class ActorTestKit private[pekko] (delegate: pekko.actor.testkit.typed.scaladsl.ActorTestKit) {

  /**
   * The default timeout as specified with the config/[[pekko.actor.testkit.typed.TestKitSettings]]
   */
  def timeout: Timeout = delegate.timeout

  /**
   * The actor system running for this testkit. Interaction with the user guardian is done through methods on the testkit
   * which is why it is typed to `Void`.
   */
  def system: ActorSystem[Void] = delegate.system.asInstanceOf[ActorSystem[Void]]

  def testKitSettings: TestKitSettings = delegate.testKitSettings

  /**
   * The scheduler of the testkit actor system
   */
  def scheduler: Scheduler = delegate.scheduler

  /**
   * Spawn a new auto-named actor under the testkit user guardian and return the ActorRef for the spawned actor
   */
  def spawn[T](behavior: Behavior[T]): ActorRef[T] = delegate.spawn(behavior)

  /**
   * Spawn a new named actor under the testkit user guardian and return the ActorRef for the spawned actor,
   * note that spawning actors with the same name in multiple test cases will cause failures.
   */
  def spawn[T](behavior: Behavior[T], name: String): ActorRef[T] = delegate.spawn(behavior, name)

  /**
   * Spawn a new auto-named actor under the testkit user guardian with the given props
   * and return the ActorRef for the spawned actor
   */
  def spawn[T](behavior: Behavior[T], props: Props): ActorRef[T] = delegate.spawn(behavior, props)

  /**
   * Spawn a new named actor under the testkit user guardian with the given props and return the ActorRef
   * for the spawned actor, note that spawning actors with the same name in multiple test cases will cause failures.
   */
  def spawn[T](behavior: Behavior[T], name: String, props: Props): ActorRef[T] = delegate.spawn(behavior, name, props)

  /**
   * Stop the actor under test and wait until it terminates.
   * It can only be used for actors that were spawned by this `ActorTestKit`.
   * Other actors will not be stopped by this method.
   */
  def stop[T](ref: ActorRef[T]): Unit = delegate.stop(ref)

  /**
   * Stop the actor under test and wait `max` until it terminates.
   * It can only be used for actors that were spawned by this `ActorTestKit`.
   * Other actors will not be stopped by this method.
   */
  def stop[T](ref: ActorRef[T], max: Duration): Unit = delegate.stop(ref, max.toScala)

  /**
   * Shortcut for creating a new test probe for the testkit actor system
   * @tparam M the type of messages the probe should accept
   */
  def createTestProbe[M](): TestProbe[M] = TestProbe.create(system)

  /**
   * Shortcut for creating a new test probe for the testkit actor system
   * @tparam M the type of messages the probe should accept
   */
  def createTestProbe[M](clazz: Class[M]): TestProbe[M] = TestProbe.create(clazz, system)

  /**
   * Shortcut for creating a new named test probe for the testkit actor system
   * @tparam M the type of messages the probe should accept
   */
  def createTestProbe[M](name: String): TestProbe[M] = TestProbe.create(name, system)

  /**
   * Shortcut for creating a new named test probe for the testkit actor system
   * @tparam M the type of messages the probe should accept
   */
  def createTestProbe[M](name: String, clazz: Class[M]): TestProbe[M] = TestProbe.create(name, clazz, system)

  /**
   * @return A test probe that is subscribed to dropped letters from the system event bus. Subscription
   *         will be completed and verified so any dropped letter after it will be caught by the probe.
   */
  def createDroppedMessageProbe(): TestProbe[Dropped] =
    delegate.createDroppedMessageProbe().asJava

  /**
   * @return A test probe that is subscribed to dead letters from the system event bus. Subscription
   *         will be completed and verified so any dead letter after it will be caught by the probe.
   */
  def createDeadLetterProbe(): TestProbe[DeadLetter] = delegate.createDeadLetterProbe().asJava

  /**
   * @return A test probe that is subscribed to unhandled messages from the system event bus. Subscription
   *         will be completed and verified so any unhandled message after it will be caught by the probe.
   */
  def createUnhandledMessageProbe(): TestProbe[UnhandledMessage] = delegate.createUnhandledMessageProbe().asJava

  // Note that if more methods are added here they should also be added to TestKitJunitResource

  /**
   * Terminate the actor system and the testkit
   */
  def shutdownTestKit(): Unit = delegate.shutdownTestKit()

  /**
   * Additional testing utilities for serialization.
   */
  val serializationTestKit: SerializationTestKit = new SerializationTestKit(delegate.internalSystem)

}
