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

package org.apache.pekko.persistence.r2dbc.state

import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.{ LogCapturing, ScalaTestWithActorTestKit }
import pekko.actor.typed.ActorSystem
import pekko.persistence.r2dbc.{ TestConfig, TestData, TestDbLifecycle }
import pekko.persistence.r2dbc.state.scaladsl.R2dbcDurableStateStore
import pekko.persistence.state.DurableStateStoreRegistry
import pekko.persistence.state.scaladsl.GetObjectResult
import pekko.persistence.typed.PersistenceId
import org.scalatest.wordspec.AnyWordSpecLike

import scala.concurrent.Await
import scala.concurrent.duration.DurationInt

class DurableStateStoreSpec
    extends ScalaTestWithActorTestKit(TestConfig.config)
    with AnyWordSpecLike
    with TestDbLifecycle
    with TestData
    with LogCapturing {

  override def typedSystem: ActorSystem[_] = system

  private val store = DurableStateStoreRegistry(testKit.system)
    .durableStateStoreFor[R2dbcDurableStateStore[String]](R2dbcDurableStateStore.Identifier)

  private val unusedTag = "n/a"
  private val emptyTag = ""

  "The R2DBC durable state store" should {
    "save and retrieve a value" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "my-persistenceId").id
      val value = "Genuinely Collaborative"

      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
    }

    "save and retrieve a value with empty tag" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "my-persistenceId-empty-tag").id
      val value = "Genuinely Collaborative"

      store.upsertObject(persistenceId, 1L, value, emptyTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
    }

    "produce None when fetching a non-existing key" in {
      val entityType = nextEntityType()
      val key = PersistenceId(entityType, "nonexistent-id").id
      store.getObject(key).futureValue should be(GetObjectResult(None, 0L))
    }

    "update a value" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "id-to-be-updated").id
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))

      val updatedValue = "Open to Feedback"
      store.upsertObject(persistenceId, 2L, updatedValue, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(updatedValue), 2L))
    }

    "detect and reject concurrent inserts" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "id-to-be-inserted-concurrently")
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId.id, revision = 1L, value, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(value), 1L))

      val updatedValue = "Open to Feedback"
      val failure =
        store.upsertObject(persistenceId.id, revision = 1L, updatedValue, entityType).failed.futureValue
      failure.getMessage should include(
        s"Insert failed: durable state for persistence id [${persistenceId.id}] already exists")
    }

    "detect and reject concurrent updates" in {
      if (!stateSettings.durableStateAssertSingleWriter)
        pending

      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "id-to-be-updated-concurrently")
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId.id, revision = 1L, value, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(value), 1L))

      val updatedValue = "Open to Feedback"
      store.upsertObject(persistenceId.id, revision = 2L, updatedValue, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(updatedValue), 2L))

      // simulate an update by a different node that didn't see the first one:
      val updatedValue2 = "Genuine and Sincere in all Communications"
      val failure =
        store.upsertObject(persistenceId.id, revision = 2L, updatedValue2, entityType).failed.futureValue
      failure.getMessage should include(
        s"Update failed: durable state for persistence id [${persistenceId.id}] could not be updated to revision [2]")
    }

    "support deletions" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-added-and-removed").id
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
      store.deleteObject(persistenceId).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 0L))
    }

    "hard delete when revision=0" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-added-and-removed").id
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
      store.deleteObject(persistenceId, revision = 0).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 0L))
    }

    "delete payload but keep revision" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-added-and-removed").id
      val value1 = "value1"
      store.upsertObject(persistenceId, 1L, value1, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value1), 1L))
      store.deleteObject(persistenceId, revision = 2L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 2L))

      val value2 = "value2"
      store.upsertObject(persistenceId, 3L, value2, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value2), 3L))
    }

    "update revision when deleting" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-removed").id

      store.deleteObject(persistenceId, revision = 1L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 1L))
      store.deleteObject(persistenceId, revision = 2L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 2L))

      val value1 = "value1"
      store.upsertObject(persistenceId, 3L, value1, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value1), 3L))

      store.deleteObject(persistenceId, revision = 4L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 4L))
      store.deleteObject(persistenceId, revision = 5L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 5L))
    }

    "upsert with correct revision after delete succeeds" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-deleted-then-re-inserted").id
      val value1 = "initial value"
      store.upsertObject(persistenceId, 1L, value1, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value1), 1L))
      store.deleteObject(persistenceId, revision = 2L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 2L))

      val value2 = "new value after delete"
      store.upsertObject(persistenceId, 3L, value2, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value2), 3L))
    }

    "reject upsert with same revision as delete revision" in {
      if (!stateSettings.durableStateAssertSingleWriter)
        pending

      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-deleted-then-bad-update-same-rev").id
      val value = "initial value"
      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
      store.deleteObject(persistenceId, revision = 2L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 2L))

      // revision=2 is already used by the delete; next valid revision is 3
      val failure =
        store.upsertObject(persistenceId, revision = 2L, "wrong value", unusedTag).failed.futureValue
      failure.getMessage should include(
        s"Update failed: durable state for persistence id [$persistenceId] could not be updated to revision [2]")
    }

    "reject upsert with skipped revision after delete" in {
      if (!stateSettings.durableStateAssertSingleWriter)
        pending

      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "to-be-deleted-then-bad-update-skipped-rev").id
      val value = "initial value"
      store.upsertObject(persistenceId, 1L, value, unusedTag).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(Some(value), 1L))
      store.deleteObject(persistenceId, revision = 2L).futureValue
      store.getObject(persistenceId).futureValue should be(GetObjectResult(None, 2L))

      // revision=4 skips revision 3; the correct next revision after delete at 2 is 3
      val failure =
        store.upsertObject(persistenceId, revision = 4L, "wrong value", unusedTag).failed.futureValue
      failure.getMessage should include(
        s"Update failed: durable state for persistence id [$persistenceId] could not be updated to revision [4]")
    }

    "detect and reject concurrent delete of revision 1" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "id-to-be-deleted-concurrently")
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId.id, revision = 1L, value, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(value), 1L))

      val failure =
        store.deleteObject(persistenceId.id, revision = 1L).failed.futureValue
      failure.getMessage should include(
        s"Insert delete marker with revision 1 failed: durable state for persistence id [${persistenceId.id}] already exists")
    }

    "detect and reject concurrent deletes" in {
      val entityType = nextEntityType()
      val persistenceId = PersistenceId(entityType, "id-to-be-updated-concurrently")
      val value = "Genuinely Collaborative"
      store.upsertObject(persistenceId.id, revision = 1L, value, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(value), 1L))

      val updatedValue = "Open to Feedback"
      store.upsertObject(persistenceId.id, revision = 2L, updatedValue, entityType).futureValue
      store.getObject(persistenceId.id).futureValue should be(GetObjectResult(Some(updatedValue), 2L))

      // simulate a delete by a different node that didn't see the first one:
      val updatedValue2 = "Genuine and Sincere in all Communications"
      val failure =
        store.deleteObject(persistenceId.id, revision = 2L).failed.futureValue
      failure.getMessage should include(
        s"Delete failed: durable state for persistence id [${persistenceId.id}] could not be updated to revision [2]")
    }

  }

}
