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

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

import java.util.concurrent.{ CompletionStage, ThreadFactory }

import scala.annotation.nowarn
import scala.concurrent._
import scala.jdk.FutureConverters._

import org.apache.pekko
import pekko.{ actor => classic }
import pekko.Done
import pekko.actor.{ ActorPath, ActorRefProvider, Address, ReflectiveDynamicAccess }
import pekko.actor.typed.ActorRef
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.Behavior
import pekko.actor.typed.DispatcherSelector
import pekko.actor.typed.Dispatchers
import pekko.actor.typed.Extension
import pekko.actor.typed.ExtensionId
import pekko.actor.typed.Props
import pekko.actor.typed.Scheduler
import pekko.actor.typed.Settings
import pekko.actor.typed.internal.ActorRefImpl
import pekko.actor.typed.internal.InternalRecipientRef
import pekko.actor.typed.receptionist.Receptionist
import pekko.annotation.InternalApi

import org.slf4j.Logger
import org.slf4j.LoggerFactory

import com.typesafe.config.{ Config, ConfigFactory }

/**
 * INTERNAL API
 */
@nowarn
@InternalApi private[pekko] final class ActorSystemStub(
    val name: String,
    config: Config = ActorSystemStub.config.defaultReference)
    extends ActorSystem[Nothing]
    with ActorRef[Nothing]
    with ActorRefImpl[Nothing]
    with InternalRecipientRef[Nothing] {

  private val rootPath: ActorPath = classic.RootActorPath(classic.Address("pekko", name))

  override val path: classic.ActorPath = rootPath / "user"

  override val settings: Settings = {
    val classLoader = getClass.getClassLoader
    val dynamicAccess = new ReflectiveDynamicAccess(classLoader)
    val config_ =
      classic.ActorSystem.Settings.amendSlf4jConfig(config, dynamicAccess)
    val untypedSettings = new classic.ActorSystem.Settings(classLoader, config_, name)
    new Settings(untypedSettings)
  }

  override def tell(message: Nothing): Unit =
    throw new UnsupportedOperationException("must not send message to ActorSystemStub")

  // impl ActorRefImpl
  override def isLocal: Boolean = true
  // impl ActorRefImpl
  override def sendSystem(signal: pekko.actor.typed.internal.SystemMessage): Unit =
    throw new UnsupportedOperationException("must not send SYSTEM message to ActorSystemStub")

  // impl InternalRecipientRef, ask not supported
  override def provider: ActorRefProvider = throw new UnsupportedOperationException("no provider")

  // stream materialization etc. using stub not supported
  override def classicSystem =
    throw new UnsupportedOperationException("no classic actor system available")

  // impl InternalRecipientRef
  def isTerminated: Boolean = whenTerminated.isCompleted

  val deadLettersInbox = new DebugRef[Any](path.parent / "deadLetters", true)
  override def deadLetters[U]: ActorRef[U] = deadLettersInbox

  override def ignoreRef[U]: ActorRef[U] = deadLettersInbox

  val receptionistInbox = new TestInboxImpl[Receptionist.Command](path.parent / "receptionist")

  override def receptionist: ActorRef[Receptionist.Command] = receptionistInbox.ref

  val controlledExecutor = new ControlledExecutor
  implicit override def executionContext: scala.concurrent.ExecutionContextExecutor = controlledExecutor
  override def dispatchers: pekko.actor.typed.Dispatchers = new Dispatchers {
    def lookup(selector: DispatcherSelector): ExecutionContextExecutor = controlledExecutor
    def shutdown(): Unit = ()
  }

  override def dynamicAccess: classic.DynamicAccess = new classic.ReflectiveDynamicAccess(getClass.getClassLoader)

  override def logConfiguration(): Unit = log.info(settings.toString)

  override def scheduler: Scheduler = throw new UnsupportedOperationException("no scheduler")

  private val terminationPromise = Promise[Done]()
  override def terminate(): Unit = terminationPromise.trySuccess(Done)
  override def whenTerminated: Future[Done] = terminationPromise.future
  override def getWhenTerminated: CompletionStage[Done] = whenTerminated.asJava
  override def close(): Unit = {
    terminate()
    Await.result(whenTerminated, scala.concurrent.duration.Duration.Inf)
  }
  override val startTime: Long = System.currentTimeMillis()
  override def uptime: Long = System.currentTimeMillis() - startTime
  override def threadFactory: java.util.concurrent.ThreadFactory = new ThreadFactory {
    override def newThread(r: Runnable): Thread = new Thread(r)
  }

  override def printTree: String = "no tree for ActorSystemStub"

  override def systemActorOf[U](behavior: Behavior[U], name: String, props: Props): ActorRef[U] = {
    throw new UnsupportedOperationException("ActorSystemStub cannot create system actors")
  }

  override def registerExtension[T <: Extension](ext: ExtensionId[T]): T =
    throw new UnsupportedOperationException("ActorSystemStub cannot register extensions")

  override def extension[T <: Extension](ext: ExtensionId[T]): T =
    throw new UnsupportedOperationException("ActorSystemStub cannot register extensions")

  override def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean =
    throw new UnsupportedOperationException("ActorSystemStub cannot register extensions")

  override def log: Logger = LoggerFactory.getLogger(getClass)

  def address: Address = rootPath.address
}

@InternalApi private[pekko] object ActorSystemStub {
  object config {
    // this is backward compatible with the old behavior, hence it uses the loader used to load the test-kit
    // which is not necessarily the one used to load the tests...
    // hence this might not include reference config related to the actually executing test
    // todo: might be better NOT to pass any class loader and let typesafeConfig rely on the contextClassLoader
    // (which is usually the system class loader)
    def defaultReference: Config = ConfigFactory.defaultReference(getClass.getClassLoader)
  }
}
