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

package jdocs.akka.cluster.sharding.typed;

import akka.Done;
import akka.actor.testkit.typed.javadsl.LogCapturing;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.cluster.sharding.typed.javadsl.ClusterSharding;
import akka.cluster.sharding.typed.javadsl.Entity;
import akka.cluster.sharding.typed.javadsl.EntityRef;
import akka.cluster.typed.Cluster;
import akka.cluster.typed.Join;
import akka.pattern.StatusReply;
import akka.persistence.typed.PersistenceId;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;

import java.math.BigDecimal;
import java.time.Duration;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;

import static akka.Done.done;
import static jdocs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity;
import static jdocs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class AccountExampleTest extends JUnitSuite {

  public static final Config config =
      ConfigFactory.parseString(
          "akka.actor.provider = cluster \n"
              + "akka.remote.classic.netty.tcp.port = 0 \n"
              + "akka.remote.artery.canonical.port = 0 \n"
              + "akka.remote.artery.canonical.hostname = 127.0.0.1 \n"
              + "akka.persistence.journal.plugin = \"akka.persistence.journal.inmem\" \n"
              + "akka.persistence.journal.inmem.test-serialization = on \n");

  @ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource(config);

  @Rule public final LogCapturing logCapturing = new LogCapturing();

  private ClusterSharding _sharding = null;

  private ClusterSharding sharding() {
    if (_sharding == null) {
      // initialize first time only
      Cluster cluster = Cluster.get(testKit.system());
      cluster.manager().tell(new Join(cluster.selfMember().address()));

      ClusterSharding sharding = ClusterSharding.get(testKit.system());
      sharding.init(
          Entity.of(
              AccountEntity.ENTITY_TYPE_KEY,
              entityContext ->
                  AccountEntity.create(
                      entityContext.getEntityId(),
                      PersistenceId.of(
                          entityContext.getEntityTypeKey().name(), entityContext.getEntityId()))));
      _sharding = sharding;
    }
    return _sharding;
  }

  @Test
  public void handleDeposit() {
    EntityRef<Command> ref = sharding().entityRefFor(AccountEntity.ENTITY_TYPE_KEY, "1");
    TestProbe<StatusReply<Done>> probe = testKit.createTestProbe();
    ref.tell(new CreateAccount(probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Deposit(BigDecimal.valueOf(100), probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Deposit(BigDecimal.valueOf(10), probe.getRef()));
    probe.expectMessage(StatusReply.ack());
  }

  @Test
  public void handleWithdraw() {
    EntityRef<Command> ref = sharding().entityRefFor(AccountEntity.ENTITY_TYPE_KEY, "2");
    TestProbe<StatusReply<Done>> probe = testKit.createTestProbe();
    ref.tell(new CreateAccount(probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Deposit(BigDecimal.valueOf(100), probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Withdraw(BigDecimal.valueOf(10), probe.getRef()));
    probe.expectMessage(StatusReply.ack());
  }

  @Test
  public void rejectWithdrawOverdraft() {
    EntityRef<Command> ref = sharding().entityRefFor(AccountEntity.ENTITY_TYPE_KEY, "3");
    TestProbe<StatusReply<Done>> probe = testKit.createTestProbe();
    ref.tell(new CreateAccount(probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Deposit(BigDecimal.valueOf(100), probe.getRef()));
    probe.expectMessage(StatusReply.ack());
    ref.tell(new Withdraw(BigDecimal.valueOf(110), probe.getRef()));
    assertTrue(probe.receiveMessage().isError());
  }

  @Test
  public void handleGetBalance() {
    EntityRef<Command> ref = sharding().entityRefFor(AccountEntity.ENTITY_TYPE_KEY, "4");
    TestProbe<StatusReply<Done>> opProbe = testKit.createTestProbe();
    ref.tell(new CreateAccount(opProbe.getRef()));
    opProbe.expectMessage(StatusReply.ack());
    ref.tell(new Deposit(BigDecimal.valueOf(100), opProbe.getRef()));
    opProbe.expectMessage(StatusReply.ack());

    TestProbe<CurrentBalance> getProbe = testKit.createTestProbe(CurrentBalance.class);
    ref.tell(new GetBalance(getProbe.getRef()));
    assertEquals(
        BigDecimal.valueOf(100), getProbe.expectMessageClass(CurrentBalance.class).balance);
  }

  @Test
  public void beUsableWithAsk() throws Exception {
    Duration timeout = Duration.ofSeconds(3);
    EntityRef<Command> ref = sharding().entityRefFor(AccountEntity.ENTITY_TYPE_KEY, "5");
    CompletionStage<Done> createResult = ref.askWithStatus(CreateAccount::new, timeout);
    assertEquals(done(), createResult.toCompletableFuture().get(3, TimeUnit.SECONDS));

    // above works because then the response type is inferred by the lhs type
    // below requires explicit typing

    assertEquals(
        done(),
        ref.<Done>askWithStatus(replyTo -> new Deposit(BigDecimal.valueOf(100), replyTo), timeout)
            .toCompletableFuture()
            .get(3, TimeUnit.SECONDS));

    assertEquals(
        done(),
        ref.<Done>askWithStatus(replyTo -> new Withdraw(BigDecimal.valueOf(10), replyTo), timeout)
            .toCompletableFuture()
            .get(3, TimeUnit.SECONDS));

    BigDecimal balance =
        ref.ask(GetBalance::new, timeout)
            .thenApply(currentBalance -> currentBalance.balance)
            .toCompletableFuture()
            .get(3, TimeUnit.SECONDS);
    assertEquals(BigDecimal.valueOf(90), balance);
  }

  @Test
  public void verifySerialization() {
    TestProbe<StatusReply<Done>> opProbe = testKit.createTestProbe();
    testKit.serializationTestKit().verifySerialization(new CreateAccount(opProbe.getRef()), false);
    Deposit deposit2 =
        testKit
            .serializationTestKit()
            .verifySerialization(new Deposit(BigDecimal.valueOf(100), opProbe.getRef()), false);
    assertEquals(BigDecimal.valueOf(100), deposit2.amount);
    assertEquals(opProbe.getRef(), deposit2.replyTo);
    testKit
        .serializationTestKit()
        .verifySerialization(new Withdraw(BigDecimal.valueOf(90), opProbe.getRef()), false);
    testKit.serializationTestKit().verifySerialization(new CloseAccount(opProbe.getRef()), false);

    TestProbe<CurrentBalance> getProbe = testKit.createTestProbe();
    testKit.serializationTestKit().verifySerialization(new GetBalance(getProbe.getRef()), false);

    testKit
        .serializationTestKit()
        .verifySerialization(new CurrentBalance(BigDecimal.valueOf(100)), false);

    testKit.serializationTestKit().verifySerialization(AccountCreated.INSTANCE, false);
    testKit
        .serializationTestKit()
        .verifySerialization(new Deposited(BigDecimal.valueOf(100)), false);
    testKit
        .serializationTestKit()
        .verifySerialization(new Withdrawn(BigDecimal.valueOf(90)), false);
    testKit.serializationTestKit().verifySerialization(new AccountClosed(), false);

    testKit.serializationTestKit().verifySerialization(new EmptyAccount(), false);
    OpenedAccount openedAccount2 =
        testKit
            .serializationTestKit()
            .verifySerialization(new OpenedAccount(BigDecimal.valueOf(100)), false);
    assertEquals(BigDecimal.valueOf(100), openedAccount2.balance);
    testKit.serializationTestKit().verifySerialization(new ClosedAccount(), false);
  }
}
