/**
 * Copyright (C) 2014-2015 Typesafe Inc. <http://www.typesafe.com>
 */
package akka.persistence

import akka.actor._
import akka.persistence.JournalProtocol.ReplayMessages
import akka.testkit._
import com.typesafe.config.Config

import scala.concurrent.duration._

object PersistentViewSpec {

  private class TestPersistentActor(name: String, probe: ActorRef) extends NamedPersistentActor(name) {
    def receiveCommand = {
      case msg ⇒ persist(msg) { m ⇒ probe ! s"${m}-${lastSequenceNr}" }
    }

    override def receiveRecover: Receive = {
      case _ ⇒ // do nothing...
    }
  }

  private class TestPersistentView(name: String, probe: ActorRef, interval: FiniteDuration, var failAt: Option[String]) extends PersistentView {
    def this(name: String, probe: ActorRef, interval: FiniteDuration) =
      this(name, probe, interval, None)

    def this(name: String, probe: ActorRef) =
      this(name, probe, 100.milliseconds)

    override def autoUpdateInterval: FiniteDuration = interval.dilated(context.system)
    override val persistenceId: String = name
    override val viewId: String = name + "-view"

    var last: String = _

    def receive = {
      case "get" ⇒
        probe ! last
      case "boom" ⇒
        throw new TestException("boom")

      case payload if isPersistent && shouldFailOn(payload) ⇒
        throw new TestException("boom")

      case payload if isPersistent ⇒
        last = s"replicated-${payload}-${lastSequenceNr}"
        probe ! last

    }

    override def postRestart(reason: Throwable): Unit = {
      super.postRestart(reason)
      failAt = None
    }

    def shouldFailOn(m: Any): Boolean =
      failAt.foldLeft(false) { (a, f) ⇒ a || (m == f) }
  }

  private class PassiveTestPersistentView(name: String, probe: ActorRef, var failAt: Option[String]) extends PersistentView {
    override val persistenceId: String = name
    override val viewId: String = name + "-view"

    override def autoUpdate: Boolean = false
    override def autoUpdateReplayMax: Long = 0L // no message replay during initial recovery

    var last: String = _

    def receive = {
      case "get" ⇒
        probe ! last
      case payload if isPersistent && shouldFailOn(payload) ⇒
        throw new TestException("boom")
      case payload ⇒
        last = s"replicated-${payload}-${lastSequenceNr}"
    }

    override def postRestart(reason: Throwable): Unit = {
      super.postRestart(reason)
      failAt = None
    }

    def shouldFailOn(m: Any): Boolean =
      failAt.foldLeft(false) { (a, f) ⇒ a || (m == f) }

  }

  private class ActiveTestPersistentView(name: String, probe: ActorRef) extends PersistentView {
    override val persistenceId: String = name
    override val viewId: String = name + "-view"

    override def autoUpdateInterval: FiniteDuration = 50.millis
    override def autoUpdateReplayMax: Long = 2

    def receive = {
      case payload ⇒
        probe ! s"replicated-${payload}-${lastSequenceNr}"
    }
  }

  private class BecomingPersistentView(name: String, probe: ActorRef) extends PersistentView {
    override def persistenceId = name
    override def viewId = name + "-view"

    def receive = Actor.emptyBehavior

    context.become {
      case payload ⇒ probe ! s"replicated-${payload}-${lastSequenceNr}"
    }
  }

  private class StashingPersistentView(name: String, probe: ActorRef) extends PersistentView {
    override def persistenceId = name
    override def viewId = name + "-view"

    def receive = {
      case "other" ⇒ stash()
      case "unstash" ⇒
        unstashAll()
        context.become {
          case msg ⇒ probe ! s"$msg-${lastSequenceNr}"
        }
      case msg ⇒ stash()
    }
  }

  private class PersistentOrNotTestPersistentView(name: String, probe: ActorRef) extends PersistentView {
    override val persistenceId: String = name
    override val viewId: String = name + "-view"

    def receive = {
      case payload if isPersistent ⇒ probe ! s"replicated-${payload}-${lastSequenceNr}"
      case payload                 ⇒ probe ! s"normal-${payload}-${lastSequenceNr}"
    }
  }

  private class SnapshottingPersistentView(name: String, probe: ActorRef) extends PersistentView {
    override val persistenceId: String = name
    override val viewId: String = s"${name}-replicator"

    override def autoUpdateInterval: FiniteDuration = 100.microseconds.dilated(context.system)

    var last: String = _

    def receive = {
      case "get" ⇒
        probe ! last
      case "snap" ⇒
        saveSnapshot(last)
      case "restart" ⇒
        throw new TestException("restart requested")
      case SaveSnapshotSuccess(_) ⇒
        probe ! "snapped"
      case SnapshotOffer(metadata, snapshot: String) ⇒
        last = snapshot
        probe ! last
      case payload ⇒
        last = s"replicated-${payload}-${lastSequenceNr}"
        probe ! last
    }
  }
}

abstract class PersistentViewSpec(config: Config) extends PersistenceSpec(config) with ImplicitSender {
  import akka.persistence.PersistentViewSpec._

  var persistentActor: ActorRef = _
  var view: ActorRef = _

  var persistentActorProbe: TestProbe = _
  var viewProbe: TestProbe = _

  override protected def beforeEach(): Unit = {
    super.beforeEach()

    persistentActorProbe = TestProbe()
    viewProbe = TestProbe()

    persistentActor = system.actorOf(Props(classOf[TestPersistentActor], name, persistentActorProbe.ref))
    persistentActor ! "a"
    persistentActor ! "b"

    persistentActorProbe.expectMsg("a-1")
    persistentActorProbe.expectMsg("b-2")
  }

  override protected def afterEach(): Unit = {
    system.stop(persistentActor)
    system.stop(view)
    super.afterEach()
  }

  def subscribeToReplay(probe: TestProbe): Unit =
    system.eventStream.subscribe(probe.ref, classOf[ReplayMessages])

  "A persistent view" must {
    "receive past updates from a persistent actor" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
    }
    "receive live updates from a persistent actor" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      persistentActor ! "c"
      viewProbe.expectMsg("replicated-c-3")
    }
    "run updates at specified interval" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref, 2.seconds))
      // initial update is done on start
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      // live updates takes 5 seconds to replicate
      persistentActor ! "c"
      viewProbe.expectNoMsg(1.second)
      viewProbe.expectMsg("replicated-c-3")
    }
    "run updates on user request" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref, 5.seconds))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      persistentActor ! "c"
      persistentActorProbe.expectMsg("c-3")
      view ! Update(await = false)
      viewProbe.expectMsg("replicated-c-3")
    }
    "run updates on user request and await update" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref, 5.seconds))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      persistentActor ! "c"
      persistentActorProbe.expectMsg("c-3")
      view ! Update(await = true)
      view ! "get"
      viewProbe.expectMsg("replicated-c-3")
    }
    "run updates again on failure outside an update cycle" in {
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref, 5.seconds))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      view ! "boom"
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
    }
    "run updates again on failure during an update cycle" in {
      persistentActor ! "c"
      persistentActorProbe.expectMsg("c-3")
      view = system.actorOf(Props(classOf[TestPersistentView], name, viewProbe.ref, 5.seconds, Some("b")))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      viewProbe.expectMsg("replicated-c-3")
    }
    "run size-limited updates on user request" in {
      persistentActor ! "c"
      persistentActor ! "d"
      persistentActor ! "e"
      persistentActor ! "f"

      persistentActorProbe.expectMsg("c-3")
      persistentActorProbe.expectMsg("d-4")
      persistentActorProbe.expectMsg("e-5")
      persistentActorProbe.expectMsg("f-6")

      view = system.actorOf(Props(classOf[PassiveTestPersistentView], name, viewProbe.ref, None))

      view ! Update(await = true, replayMax = 2)
      view ! "get"
      viewProbe.expectMsg("replicated-b-2")

      view ! Update(await = true, replayMax = 1)
      view ! "get"
      viewProbe.expectMsg("replicated-c-3")

      view ! Update(await = true, replayMax = 4)
      view ! "get"
      viewProbe.expectMsg("replicated-f-6")
    }
    "run size-limited updates automatically" in {
      val replayProbe = TestProbe()

      persistentActor ! "c"
      persistentActor ! "d"

      persistentActorProbe.expectMsg("c-3")
      persistentActorProbe.expectMsg("d-4")

      subscribeToReplay(replayProbe)

      view = system.actorOf(Props(classOf[ActiveTestPersistentView], name, viewProbe.ref))

      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      viewProbe.expectMsg("replicated-c-3")
      viewProbe.expectMsg("replicated-d-4")

      replayProbe.expectMsgPF() { case ReplayMessages(1L, _, 2L, _, _) ⇒ }
      replayProbe.expectMsgPF() { case ReplayMessages(3L, _, 2L, _, _) ⇒ }
      replayProbe.expectMsgPF() { case ReplayMessages(5L, _, 2L, _, _) ⇒ }
    }
    "support context.become" in {
      view = system.actorOf(Props(classOf[BecomingPersistentView], name, viewProbe.ref))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
    }
    "check if an incoming message is persistent" in {
      persistentActor ! "c"

      persistentActorProbe.expectMsg("c-3")

      view = system.actorOf(Props(classOf[PersistentOrNotTestPersistentView], name, viewProbe.ref))

      view ! "d"
      view ! "e"

      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      viewProbe.expectMsg("replicated-c-3")
      viewProbe.expectMsg("normal-d-3")
      viewProbe.expectMsg("normal-e-3")

      persistentActor ! "f"
      viewProbe.expectMsg("replicated-f-4")
    }
    "take snapshots" in {
      view = system.actorOf(Props(classOf[SnapshottingPersistentView], name, viewProbe.ref))
      viewProbe.expectMsg("replicated-a-1")
      viewProbe.expectMsg("replicated-b-2")
      view ! "snap"
      viewProbe.expectMsg("snapped")
      view ! "restart"
      persistentActor ! "c"
      viewProbe.expectMsg("replicated-b-2")
      viewProbe.expectMsg("replicated-c-3")
    }
    "support stash" in {
      view = system.actorOf(Props(classOf[StashingPersistentView], name, viewProbe.ref))
      view ! "other"
      view ! "unstash"
      viewProbe.expectMsg("a-2") // note that the lastSequenceNumber is 2, since we have replayed b-2
      viewProbe.expectMsg("b-2")
      viewProbe.expectMsg("other-2")
    }
  }
}

class LeveldbPersistentViewSpec extends PersistentViewSpec(PersistenceSpec.config("leveldb", "LeveldbPersistentViewSpec"))
class InmemPersistentViewSpec extends PersistentViewSpec(PersistenceSpec.config("inmem", "InmemPersistentViewSpec"))

