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

package jdocs.org.apache.pekko.actor.testkit.typed.javadsl;

// #imports
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.typesafe.config.Config;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import org.apache.pekko.actor.testkit.typed.CapturedLogEvent;
import org.apache.pekko.actor.testkit.typed.Effect;
import org.apache.pekko.actor.testkit.typed.javadsl.BehaviorTestKit;
import org.apache.pekko.actor.testkit.typed.javadsl.TestInbox;
import org.apache.pekko.actor.typed.*;
import org.apache.pekko.actor.typed.javadsl.*;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

import com.typesafe.config.Config;
import org.slf4j.event.Level;
// #imports
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import org.scalatestplus.junit.JUnitSuite;

public class SyncTestingExampleTest extends JUnitSuite {

  // #child
  public static class Child {
    public static Behavior<String> create() {
      return Behaviors.receive((context, message) -> Behaviors.same());
    }
  }
  // #child

  // #under-test
  public static class Hello extends AbstractBehavior<Hello.Command> {

    public interface Command {}

    public static class CreateAChild implements Command {
      public final String childName;

      public CreateAChild(String childName) {
        this.childName = childName;
      }
    }

    public enum CreateAnAnonymousChild implements Command {
      INSTANCE
    }

    public static class SayHelloToChild implements Command {
      public final String childName;

      public SayHelloToChild(String childName) {
        this.childName = childName;
      }
    }

    public enum SayHelloToAnonymousChild implements Command {
      INSTANCE
    }

    public static class SayHello implements Command {
      public final ActorRef<String> who;

      public SayHello(ActorRef<String> who) {
        this.who = who;
      }
    }

    public static class LogAndSayHello implements Command {
      public final ActorRef<String> who;

      public LogAndSayHello(ActorRef<String> who) {
        this.who = who;
      }
    }

    public static class AskAQuestion implements Command {
      public final ActorRef<Question> who;

      public AskAQuestion(ActorRef<Question> who) {
        this.who = who;
      }
    }

    public static class GotAnAnswer implements Command {
      public final String answer;
      public final ActorRef<Question> from;

      public GotAnAnswer(String answer, ActorRef<Question> from) {
        this.answer = answer;
        this.from = from;
      }
    }

    public static class NoAnswerFrom implements Command {
      public final ActorRef<Question> whom;

      public NoAnswerFrom(ActorRef<Question> whom) {
        this.whom = whom;
      }
    }

    public static class Question {
      public final String q;
      public final ActorRef<Answer> replyTo;

      public Question(String q, ActorRef<Answer> replyTo) {
        this.q = q;
        this.replyTo = replyTo;
      }
    }

    public static class Answer {
      public final String a;

      public Answer(String a) {
        this.a = a;
      }
    }

    public static Behavior<Command> create() {
      return Behaviors.setup(Hello::new);
    }

    private Hello(ActorContext<Command> context) {
      super(context);
    }

    @Override
    public Receive<Command> createReceive() {
      return newReceiveBuilder()
          .onMessage(CreateAChild.class, this::onCreateAChild)
          .onMessage(CreateAnAnonymousChild.class, this::onCreateAnonymousChild)
          .onMessage(SayHelloToChild.class, this::onSayHelloToChild)
          .onMessage(SayHelloToAnonymousChild.class, this::onSayHelloToAnonymousChild)
          .onMessage(SayHello.class, this::onSayHello)
          .onMessage(LogAndSayHello.class, this::onLogAndSayHello)
          .onMessage(AskAQuestion.class, this::onAskAQuestion)
          .onMessage(GotAnAnswer.class, this::onGotAnAnswer)
          .onMessage(NoAnswerFrom.class, this::onNoAnswerFrom)
          .build();
    }

    private Behavior<Command> onCreateAChild(CreateAChild message) {
      getContext().spawn(Child.create(), message.childName);
      return Behaviors.same();
    }

    private Behavior<Command> onCreateAnonymousChild(CreateAnAnonymousChild message) {
      getContext().spawnAnonymous(Child.create());
      return Behaviors.same();
    }

    private Behavior<Command> onSayHelloToChild(SayHelloToChild message) {
      ActorRef<String> child = getContext().spawn(Child.create(), message.childName);
      child.tell("hello");
      return Behaviors.same();
    }

    private Behavior<Command> onSayHelloToAnonymousChild(SayHelloToAnonymousChild message) {
      ActorRef<String> child = getContext().spawnAnonymous(Child.create());
      child.tell("hello stranger");
      return Behaviors.same();
    }

    private Behavior<Command> onSayHello(SayHello message) {
      message.who.tell("hello");
      return Behaviors.same();
    }

    private Behavior<Command> onLogAndSayHello(LogAndSayHello message) {
      getContext().getLog().info("Saying hello to {}", message.who.path().name());
      message.who.tell("hello");
      return Behaviors.same();
    }

    private Behavior<Command> onAskAQuestion(AskAQuestion message) {
      getContext()
          .ask(
              Answer.class,
              message.who,
              Duration.ofSeconds(10),
              (ActorRef<Answer> ref) -> new Question("do you know who I am?", ref),
              (response, throwable) -> {
                if (response != null) {
                  return new GotAnAnswer(response.a, message.who);
                } else {
                  return new NoAnswerFrom(message.who);
                }
              });
      return Behaviors.same();
    }

    private Behavior<Command> onGotAnAnswer(GotAnAnswer message) {
      getContext().getLog().info("Got an answer[{}] from {}", message.answer, message.from);
      return Behaviors.same();
    }

    private Behavior<Command> onNoAnswerFrom(NoAnswerFrom message) {
      getContext().getLog().info("Did not get an answer from {}", message.whom);
      return Behaviors.same();
    }
  }
  // #under-test

  @Test
  public void testSpawning() {
    // #test-child
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
    test.run(new Hello.CreateAChild("child"));
    assertEquals("child", test.expectEffectClass(Effect.Spawned.class).childName());
    // #test-child
  }

  @Test
  public void testSpawningAnonymous() {
    // #test-anonymous-child
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
    test.run(Hello.CreateAnAnonymousChild.INSTANCE);
    test.expectEffectClass(Effect.SpawnedAnonymous.class);
    // #test-anonymous-child
  }

  @Test
  public void testRecodingMessageSend() {
    // #test-message
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
    TestInbox<String> inbox = TestInbox.create();
    test.run(new Hello.SayHello(inbox.getRef()));
    inbox.expectMessage("hello");
    // #test-message
  }

  @Test
  public void testMessageToChild() {
    // #test-child-message
    BehaviorTestKit<Hello.Command> testKit = BehaviorTestKit.create(Hello.create());
    testKit.run(new Hello.SayHelloToChild("child"));
    TestInbox<String> childInbox = testKit.childInbox("child");
    childInbox.expectMessage("hello");
    // #test-child-message
  }

  @Test
  public void testMessageToAnonymousChild() {
    // #test-child-message-anonymous
    BehaviorTestKit<Hello.Command> testKit = BehaviorTestKit.create(Hello.create());
    testKit.run(Hello.SayHelloToAnonymousChild.INSTANCE);
    // Anonymous actors are created as: $a $b etc
    TestInbox<String> childInbox = testKit.childInbox("$a");
    childInbox.expectMessage("hello stranger");
    // #test-child-message-anonymous
  }

  @Test
  public void testCheckLogging() {
    // #test-check-logging
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
    TestInbox<String> inbox = TestInbox.create("Inboxer");
    test.run(new Hello.LogAndSayHello(inbox.getRef()));

    List<CapturedLogEvent> allLogEntries = test.getAllLogEntries();
    assertEquals(1, allLogEntries.size());
    CapturedLogEvent expectedLogEvent =
        new CapturedLogEvent(
            Level.INFO,
            "Saying hello to Inboxer",
            Optional.empty(),
            Optional.empty(),
            new HashMap<>());
    assertEquals(expectedLogEvent, allLogEntries.get(0));
    // #test-check-logging
  }

  @Test
  public void testSupportContextualAsk() {
    // #test-contextual-ask
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create());
    TestInbox<Hello.Question> askee = TestInbox.create();
    test.run(new Hello.AskAQuestion(askee.getRef()));

    Hello.Question question = askee.receiveMessage();
    // Note that the replyTo address in the message is not a priori predictable, so shouldn't be
    // asserted against
    assertEquals(question.q, "do you know who I am?");

    // Retrieve a description of the performed ask
    @SuppressWarnings("unchecked")
    Effect.AskInitiated<Hello.Question, Hello.Answer, Hello.Command> effect =
        test.expectEffectClass(Effect.AskInitiated.class);

    test.clearLog();

    // The effect can be used to complete or time-out the ask at most once
    effect.respondWith(new Hello.Answer("I think I met you somewhere, sometime"));
    // commented out because we've completed the ask
    // effect.timeout();

    // Completing/timing-out the ask is processed synchronously
    List<CapturedLogEvent> allLogEntries = test.getAllLogEntries();
    assertEquals(allLogEntries.size(), 1);

    // The message, including the synthesized "replyTo", can be inspected from the effect
    assertEquals(question, effect.askMessage());

    // The response adaptation can be tested as many times as you want without completing the ask
    Hello.Command response1 = effect.adaptResponse(new Hello.Answer("No.  Who are you?"));
    assertEquals(((Hello.GotAnAnswer) response1).answer, "No.  Who are you?");

    // ... as can the message sent on a timeout
    assertTrue(effect.adaptTimeout() instanceof Hello.NoAnswerFrom);

    // The response timeout is captured
    assertEquals(effect.responseTimeout().toSeconds(), 10L);
    // #test-contextual-ask
  }

  @Test
  public void testWithAppTestCfg() {

    // #test-app-test-config
    Config cfg = BehaviorTestKit.applicationTestConfig();
    BehaviorTestKit<Hello.Command> test = BehaviorTestKit.create(Hello.create(), "hello", cfg);
  }
}
