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

package jdocs.testkit;

import static org.junit.Assert.*;

import org.apache.pekko.pattern.Patterns;
import jdocs.AbstractJavaTest;
import org.junit.Assert;
import org.apache.pekko.japi.JavaPartialFunction;
import org.apache.pekko.testkit.PekkoJUnitActorSystemResource;
import org.apache.pekko.testkit.CallingThreadDispatcher;
import org.apache.pekko.testkit.TestActor;
import org.apache.pekko.testkit.TestActorRef;
import org.apache.pekko.testkit.TestProbe;
import org.apache.pekko.testkit.javadsl.EventFilter;
import org.apache.pekko.testkit.javadsl.TestKit;
import org.junit.ClassRule;
import org.junit.Test;

import com.typesafe.config.ConfigFactory;

import org.apache.pekko.actor.AbstractActorWithTimers;
import org.apache.pekko.actor.ActorKilledException;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.Kill;
import org.apache.pekko.actor.PoisonPill;
import org.apache.pekko.actor.Props;
import org.apache.pekko.actor.Terminated;
import org.apache.pekko.actor.AbstractActor;
import org.apache.pekko.actor.UntypedAbstractActorWithTimers;
import org.apache.pekko.testkit.TestActor.AutoPilot;

import java.util.List;

import java.util.concurrent.CompletableFuture;
import java.time.Duration;

public class TestKitDocTest extends AbstractJavaTest {

  @ClassRule
  public static PekkoJUnitActorSystemResource actorSystemResource =
      new PekkoJUnitActorSystemResource(
          "TestKitDocTest",
          ConfigFactory.parseString(
              "pekko.loggers = [org.apache.pekko.testkit.TestEventListener]"));

  private final ActorSystem system = actorSystemResource.getSystem();

  // #test-actor-ref
  static class MyActor extends AbstractActor {
    @Override
    public Receive createReceive() {
      return receiveBuilder()
          .matchEquals(
              "say42",
              message -> {
                getSender().tell(42, getSelf());
              })
          .match(
              Exception.class,
              (Exception ex) -> {
                throw ex;
              })
          .build();
    }

    public boolean testMe() {
      return true;
    }
  }

  @Test
  public void demonstrateTestActorRef() {
    final Props props = Props.create(MyActor.class);
    final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "testA");
    final MyActor actor = ref.underlyingActor();
    assertTrue(actor.testMe());
  }
  // #test-actor-ref

  // #timer
  static class TestTimerActor extends AbstractActorWithTimers {
    private static final String SCHED_KEY = "SchedKey";

    static final class TriggerScheduling {}

    static final class ScheduledMessage {}

    @Override
    public Receive createReceive() {
      return receiveBuilder().match(TriggerScheduling.class, msg -> triggerScheduling()).build();
    }

    void triggerScheduling() {
      getTimers().startSingleTimer(SCHED_KEY, new ScheduledMessage(), Duration.ofMillis(500));
    }
  }

  static class TestTimerUntypedActor extends UntypedAbstractActorWithTimers {
    private static final String SCHED_KEY = "SchedKey";

    static final class TriggerScheduling {}

    static final class ScheduledMessage {}

    @Override
    public void onReceive(Object message) throws Throwable {
      if (message instanceof TriggerScheduling) {
        triggerScheduling();
      } else {
        unhandled(message);
      }
    }

    void triggerScheduling() {
      getTimers().startSingleTimer(SCHED_KEY, new ScheduledMessage(), Duration.ofMillis(500));
    }
  }
  // #timer

  @Test
  public void demonstrateAsk() throws Exception {
    // #test-behavior
    final Props props = Props.create(MyActor.class);
    final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "testB");
    final CompletableFuture<Object> future =
        Patterns.ask(ref, "say42", Duration.ofMillis(3000)).toCompletableFuture();
    assertTrue(future.isDone());
    assertEquals(42, future.get());
    // #test-behavior
  }

  @Test
  public void demonstrateExceptions() {
    // #test-expecting-exceptions
    final Props props = Props.create(MyActor.class);
    final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "myActor");
    try {
      ref.receive(new Exception("expected"));
      Assert.fail("expected an exception to be thrown");
    } catch (Exception e) {
      assertEquals("expected", e.getMessage());
    }
    // #test-expecting-exceptions
  }

  @Test
  public void demonstrateWithin() {
    // #test-within
    new TestKit(system) {
      {
        getRef().tell(42, ActorRef.noSender());
        within(
            Duration.ZERO,
            Duration.ofSeconds(1),
            () -> {
              assertEquals((Integer) 42, expectMsgClass(Integer.class));
              return null;
            });
      }
    };
    // #test-within
  }

  @Test
  public void demonstrateExpectMsg() {
    // #test-expectmsg
    new TestKit(system) {
      {
        getRef().tell(42, ActorRef.noSender());
        final String out =
            expectMsgPF(
                "match hint",
                in -> {
                  if (in instanceof Integer) {
                    return "match";
                  } else {
                    throw JavaPartialFunction.noMatch();
                  }
                });
        assertEquals("match", out);
      }
    };
    // #test-expectmsg
  }

  @Test
  public void demonstrateReceiveWhile() {
    // #test-receivewhile
    new TestKit(system) {
      {
        getRef().tell(42, ActorRef.noSender());
        getRef().tell(43, ActorRef.noSender());
        getRef().tell("hello", ActorRef.noSender());

        final List<String> out =
            receiveWhile(
                Duration.ofSeconds(1),
                in -> {
                  if (in instanceof Integer) {
                    return in.toString();
                  } else {
                    throw JavaPartialFunction.noMatch();
                  }
                });

        assertArrayEquals(new String[] {"42", "43"}, out.toArray());
        expectMsgEquals("hello");
      }
    };
    // #test-receivewhile
    new TestKit(system) {
      {
        // #test-receivewhile-full
        receiveWhile(
            Duration.ofMillis(100),
            Duration.ofMillis(50),
            12,
            in -> {
              // #match-elided
              throw JavaPartialFunction.noMatch();
              // #match-elided
            });
        // #test-receivewhile-full
      }
    };
  }

  @Test
  public void demonstrateAwaitCond() {
    // #test-awaitCond
    new TestKit(system) {
      {
        getRef().tell(42, ActorRef.noSender());
        awaitCond(Duration.ofSeconds(1), Duration.ofMillis(100), this::msgAvailable);
      }
    };
    // #test-awaitCond
  }

  @Test
  public void demonstrateAwaitAssert() {
    // #test-awaitAssert
    new TestKit(system) {
      {
        getRef().tell(42, ActorRef.noSender());
        awaitAssert(
            Duration.ofSeconds(1),
            Duration.ofMillis(100),
            () -> {
              assertEquals(msgAvailable(), true);
              return null;
            });
      }
    };
    // #test-awaitAssert
  }

  @Test
  @SuppressWarnings({"unchecked", "unused"}) // due to generic varargs
  public void demonstrateExpect() {
    new TestKit(system) {
      {
        getRef().tell("hello", ActorRef.noSender());
        getRef().tell("hello", ActorRef.noSender());
        getRef().tell("hello", ActorRef.noSender());
        getRef().tell("world", ActorRef.noSender());
        getRef().tell(42, ActorRef.noSender());
        getRef().tell(42, ActorRef.noSender());
        // #test-expect
        final String hello = expectMsgEquals("hello");
        final String any = expectMsgAnyOf("hello", "world");
        final List<String> all = expectMsgAllOf("hello", "world");
        final int i = expectMsgClass(Integer.class);
        final Number j = expectMsgAnyClassOf(Integer.class, Long.class);
        expectNoMessage();
        // #test-expect
        getRef().tell("receveN-1", ActorRef.noSender());
        getRef().tell("receveN-2", ActorRef.noSender());
        // #test-expect
        final List<Object> two = receiveN(2);
        // #test-expect
        assertEquals("hello", hello);
        assertEquals("hello", any);
        assertEquals(42, i);
        assertEquals(42, j);
        assertArrayEquals(new String[] {"hello", "world"}, all.toArray());
      }
    };
  }

  @Test
  public void demonstrateIgnoreMsg() {
    // #test-ignoreMsg
    new TestKit(system) {
      {
        // ignore all Strings
        ignoreMsg(msg -> msg instanceof String);
        getRef().tell("hello", ActorRef.noSender());
        getRef().tell(42, ActorRef.noSender());
        expectMsgEquals(42);
        // remove message filter
        ignoreNoMsg();
        getRef().tell("hello", ActorRef.noSender());
        expectMsgEquals("hello");
      }
    };
    // #test-ignoreMsg
  }

  @Test
  public void demonstrateDilated() {
    // #duration-dilation
    new TestKit(system) {
      {
        final Duration original = Duration.ofSeconds(1);
        final Duration stretched = dilated(original);
        assertTrue("dilated", stretched.compareTo(original) >= 0);
      }
    };
    // #duration-dilation
  }

  @Test
  public void demonstrateProbe() {
    // #test-probe
    new TestKit(system) {
      {
        // simple actor which only forwards messages
        class Forwarder extends AbstractActor {
          final ActorRef target;

          @SuppressWarnings("unused")
          public Forwarder(ActorRef target) {
            this.target = target;
          }

          @Override
          public Receive createReceive() {
            return receiveBuilder()
                .matchAny(message -> target.forward(message, getContext()))
                .build();
          }
        }

        // create a test probe
        final TestKit probe = new TestKit(system);

        // create a forwarder, injecting the probe’s testActor
        final Props props = Props.create(Forwarder.class, this, probe.getRef());
        final ActorRef forwarder = system.actorOf(props, "forwarder");

        // verify correct forwarding
        forwarder.tell(42, getRef());
        probe.expectMsgEquals(42);
        assertEquals(getRef(), probe.getLastSender());
      }
    };
    // #test-probe
  }

  @Test
  public void demonstrateTestProbeWithCustomName() {
    // #test-probe-with-custom-name
    new TestKit(system) {
      {
        final TestProbe worker = new TestProbe(system, "worker");
        final TestProbe aggregator = new TestProbe(system, "aggregator");

        assertTrue(worker.ref().path().name().startsWith("worker"));
        assertTrue(aggregator.ref().path().name().startsWith("aggregator"));
      }
    };
    // #test-probe-with-custom-name
  }

  @Test
  public void demonstrateSpecialProbe() {
    // #test-special-probe
    new TestKit(system) {
      {
        class MyProbe extends TestKit {
          public MyProbe() {
            super(system);
          }

          public void assertHello() {
            expectMsgEquals("hello");
          }
        }

        final MyProbe probe = new MyProbe();
        probe.getRef().tell("hello", ActorRef.noSender());
        probe.assertHello();
      }
    };
    // #test-special-probe
  }

  @Test
  public void demonstrateWatch() {
    final ActorRef target = system.actorOf(Props.create(MyActor.class));
    // #test-probe-watch
    new TestKit(system) {
      {
        final TestKit probe = new TestKit(system);
        probe.watch(target);
        target.tell(PoisonPill.getInstance(), ActorRef.noSender());
        final Terminated msg = probe.expectMsgClass(Terminated.class);
        assertEquals(msg.getActor(), target);
      }
    };
    // #test-probe-watch
  }

  @Test
  public void demonstrateReply() {
    // #test-probe-reply
    new TestKit(system) {
      {
        final TestKit probe = new TestKit(system);
        probe.getRef().tell("hello", getRef());
        probe.expectMsgEquals("hello");
        probe.reply("world");
        expectMsgEquals("world");
        assertEquals(probe.getRef(), getLastSender());
      }
    };
    // #test-probe-reply
  }

  @Test
  public void demonstrateForward() {
    // #test-probe-forward
    new TestKit(system) {
      {
        final TestKit probe = new TestKit(system);
        probe.getRef().tell("hello", getRef());
        probe.expectMsgEquals("hello");
        probe.forward(getRef());
        expectMsgEquals("hello");
        assertEquals(getRef(), getLastSender());
      }
    };
    // #test-probe-forward
  }

  @Test
  public void demonstrateUsingInheritenceToTestTimers() {
    // #timer-test
    new TestKit(system) {
      {
        final TestKit probe = new TestKit(system);
        final ActorRef target =
            system.actorOf(
                Props.create(
                    TestTimerActor.class,
                    () ->
                        new TestTimerActor() {
                          @Override
                          void triggerScheduling() {
                            probe.getRef().tell(new ScheduledMessage(), getSelf());
                          }
                        }));
        target.tell(new TestTimerActor.TriggerScheduling(), ActorRef.noSender());
        probe.expectMsgClass(TestTimerActor.ScheduledMessage.class);
      }
    };
    // #timer-test
  }

  @Test
  public void demonstrateWithinProbe() {
    try {
      // #test-within-probe
      new TestKit(system) {
        {
          final TestKit probe = new TestKit(system);
          within(Duration.ofSeconds(1), () -> probe.expectMsgEquals("hello"));
        }
      };
      // #test-within-probe
    } catch (AssertionError e) {
      // expected to fail
    }
  }

  @Test
  public void demonstrateAutoPilot() {
    // #test-auto-pilot
    new TestKit(system) {
      {
        final TestKit probe = new TestKit(system);
        // install auto-pilot
        probe.setAutoPilot(
            new TestActor.AutoPilot() {
              public AutoPilot run(ActorRef sender, Object msg) {
                sender.tell(msg, ActorRef.noSender());
                return noAutoPilot();
              }
            });
        // first one is replied to directly ...
        probe.getRef().tell("hello", getRef());
        expectMsgEquals("hello");
        // ... but then the auto-pilot switched itself off
        probe.getRef().tell("world", getRef());
        expectNoMessage();
      }
    };
    // #test-auto-pilot
  }

  // only compilation
  public void demonstrateCTD() {
    // #calling-thread-dispatcher
    system.actorOf(Props.create(MyActor.class).withDispatcher(CallingThreadDispatcher.Id()));
    // #calling-thread-dispatcher
  }

  @Test
  public void demonstrateEventFilter() {
    // #test-event-filter
    new TestKit(system) {
      {
        assertEquals("TestKitDocTest", system.name());
        final ActorRef victim = system.actorOf(Props.empty(), "victim");

        final int result =
            new EventFilter(ActorKilledException.class, system)
                .from("pekko://TestKitDocTest/user/victim")
                .occurrences(1)
                .intercept(
                    () -> {
                      victim.tell(Kill.getInstance(), ActorRef.noSender());
                      return 42;
                    });
        assertEquals(42, result);
      }
    };
    // #test-event-filter
  }
}
