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

package akka.remote

import java.io.NotSerializableException

import akka.actor.{ Actor, ActorSystem, ExtendedActorSystem, Props, RootActorPath }
import akka.serialization.SerializerWithStringManifest
import akka.testkit.{ AkkaSpec, TestActors, TestKit }
import com.typesafe.config.{ Config, ConfigFactory }

object TransientSerializationErrorSpec {
  object ManifestNotSerializable
  object ManifestIllegal
  object ToBinaryNotSerializable
  object ToBinaryIllegal
  object NotDeserializable
  object IllegalOnDeserialize

  class TestSerializer(system: ExtendedActorSystem) extends SerializerWithStringManifest {
    def identifier: Int = 666
    def manifest(o: AnyRef): String = o match {
      case ManifestNotSerializable ⇒ throw new NotSerializableException()
      case ManifestIllegal         ⇒ throw new IllegalArgumentException()
      case ToBinaryNotSerializable ⇒ "TBNS"
      case ToBinaryIllegal         ⇒ "TI"
      case NotDeserializable       ⇒ "ND"
      case IllegalOnDeserialize    ⇒ "IOD"
    }
    def toBinary(o: AnyRef): Array[Byte] = o match {
      case ToBinaryNotSerializable ⇒ throw new NotSerializableException()
      case ToBinaryIllegal         ⇒ throw new IllegalArgumentException()
      case _                       ⇒ Array.emptyByteArray
    }
    def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = {
      manifest match {
        case "ND"  ⇒ throw new NotSerializableException() // Not sure this applies here
        case "IOD" ⇒ throw new IllegalArgumentException()
      }
    }
  }
}

abstract class AbstractTransientSerializationErrorSpec(config: Config) extends AkkaSpec(
  config.withFallback(ConfigFactory.parseString(
    """
    akka {
      loglevel = info
      actor {
        provider = remote
        warn-about-java-serializer-usage = off
        serialize-creators = off
        serializers {
          test = "akka.remote.TransientSerializationErrorSpec$TestSerializer"
        }
        serialization-bindings {
          "akka.remote.TransientSerializationErrorSpec$ManifestNotSerializable$" = test
          "akka.remote.TransientSerializationErrorSpec$ManifestIllegal$" = test
          "akka.remote.TransientSerializationErrorSpec$ToBinaryNotSerializable$" = test
          "akka.remote.TransientSerializationErrorSpec$ToBinaryIllegal$" = test
          "akka.remote.TransientSerializationErrorSpec$NotDeserializable$" = test
          "akka.remote.TransientSerializationErrorSpec$IllegalOnDeserialize$" = test
        }
      }
    }
  """))) {

  import TransientSerializationErrorSpec._

  val port = system.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress.port.get
  val sysName = system.name
  val protocol =
    if (RARP(system).provider.remoteSettings.Artery.Enabled) "akka"
    else "akka.tcp"

  val system2 = ActorSystem(system.name, system.settings.config)
  val system2Address = RARP(system2).provider.getDefaultAddress

  "The transport" must {

    "stay alive after a transient exception from the serializer" in {
      system2.actorOf(TestActors.echoActorProps, "echo")

      val selection = system.actorSelection(RootActorPath(system2Address) / "user" / "echo")

      selection.tell("ping", this.testActor)
      expectMsg("ping")

      // none of these should tear down the connection
      List(
        ManifestIllegal,
        ManifestNotSerializable,
        ToBinaryIllegal,
        ToBinaryNotSerializable,
        NotDeserializable,
        IllegalOnDeserialize
      ).foreach(msg ⇒
        selection.tell(msg, this.testActor)
      )

      // make sure we still have a connection
      selection.tell("ping", this.testActor)
      expectMsg("ping")

    }
  }

  override def afterTermination(): Unit = {
    TestKit.shutdownActorSystem(system2)
  }
}

class TransientSerializationErrorSpec extends AbstractTransientSerializationErrorSpec(ConfigFactory.parseString("""
  akka.remote.netty.tcp {
    hostname = localhost
    port = 0
 }
"""))
