// Copyright © 2012-2020 VLINGO LABS. All rights reserved.
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain
// one at https://mozilla.org/MPL/2.0/.

package io.vlingo.actors;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.Assert;
import org.junit.Test;

import io.vlingo.actors.WorldTest.Simple;
import io.vlingo.actors.WorldTest.SimpleActor;
import io.vlingo.actors.WorldTest.TestResults;
import io.vlingo.actors.plugin.mailbox.testkit.TestMailbox;
import io.vlingo.actors.testkit.AccessSafely;

public class StageTest extends ActorsTest {
  @Test
  public void testActorForDefinitionAndProtocol() {
    final NoProtocol test = world.stage().actorFor(NoProtocol.class, TestInterfaceActor.class);

    assertNotNull(test);
    assertNotNull(TestInterfaceActor.instance.get());
    assertEquals(world.defaultParent(), TestInterfaceActor.instance.get().lifeCycle.environment.parent);
  }

  @Test
  public void testActorForNoDefinitionAndProtocol() {
    final TestResults testResults = new TestResults(1);
    final Simple simple = world.stage().actorFor(Simple.class, SimpleActor.class, testResults);
    simple.simpleSay();
    assertTrue(testResults.getInvoked());

    // another

    final NoProtocol test = world.stage().actorFor(NoProtocol.class, TestInterfaceActor.class);
    assertNotNull(test);
    assertNotNull(TestInterfaceActor.instance.get());
    assertEquals(world.defaultParent(), TestInterfaceActor.instance.get().lifeCycle.environment.parent);
  }

  @Test
  public void testActorForAll() {
    world.actorFor(NoProtocol.class, ParentInterfaceActor.class);

    final Definition definition =
            Definition.has(
                    TestInterfaceActor.class,
                    Definition.NoParameters,
                    ParentInterfaceActor.parent.get(),
                    TestMailbox.Name,
                    "test-actor");

    final NoProtocol test = world.stage().actorFor(NoProtocol.class, definition);

    assertNotNull(test);
    assertNotNull(TestInterfaceActor.instance.get());
  }

  @Test
  public void testDirectoryScan() {
    final Address address1 = world.addressFactory().uniqueWith("test-actor1");
    final Address address2 = world.addressFactory().uniqueWith("test-actor2");
    final Address address3 = world.addressFactory().uniqueWith("test-actor3");
    final Address address4 = world.addressFactory().uniqueWith("test-actor4");
    final Address address5 = world.addressFactory().uniqueWith("test-actor5");

    final Address address6 = world.addressFactory().uniqueWith("test-actor6");
    final Address address7 = world.addressFactory().uniqueWith("test-actor7");

    world.stage().directory().register(address1, new TestInterfaceActor());
    world.stage().directory().register(address2, new TestInterfaceActor());
    world.stage().directory().register(address3, new TestInterfaceActor());
    world.stage().directory().register(address4, new TestInterfaceActor());
    world.stage().directory().register(address5, new TestInterfaceActor());

    final ScanResult scanResult = new ScanResult(7);

    world.stage().actorOf(NoProtocol.class, address5).andFinallyConsume(actor -> {
      assertNotNull(actor);
      scanResult.found();
    });
    world.stage().actorOf(NoProtocol.class, address4).andFinallyConsume(actor -> {
      assertNotNull(actor);
      scanResult.found();
    });
    world.stage().actorOf(NoProtocol.class, address3).andFinallyConsume(actor -> {
      assertNotNull(actor);
      scanResult.found();
    });
    world.stage().actorOf(NoProtocol.class, address2).andFinallyConsume(actor -> {
      assertNotNull(actor);
      scanResult.found();
    });
    world.stage().actorOf(NoProtocol.class, address1).andFinallyConsume(actor -> {
      assertNotNull(actor);
      scanResult.found();
    });

    world.stage().maybeActorOf(NoProtocol.class, address6)
      .andFinallyConsume((maybe) -> {
        if (maybe.isPresent()) scanResult.found();
        else scanResult.notFound();
      });
    world.stage().maybeActorOf(NoProtocol.class, address7)
      .andFinallyConsume((maybe) -> {
        if (maybe.isPresent()) scanResult.found();
        else scanResult.notFound();
    });

    assertEquals(5, scanResult.getFoundCount());
    assertEquals(2, scanResult.getNotFoundCount());
  }

  @Test
  public void testDirectoryScanMaybeActor() {
    final Address address1 = world.addressFactory().uniqueWith("test-actor1");
    final Address address2 = world.addressFactory().uniqueWith("test-actor2");
    final Address address3 = world.addressFactory().uniqueWith("test-actor3");
    final Address address4 = world.addressFactory().uniqueWith("test-actor4");
    final Address address5 = world.addressFactory().uniqueWith("test-actor5");

    final Address address6 = world.addressFactory().uniqueWith("test-actor6");
    final Address address7 = world.addressFactory().uniqueWith("test-actor7");

    world.stage().directory().register(address1, new TestInterfaceActor());
    world.stage().directory().register(address2, new TestInterfaceActor());
    world.stage().directory().register(address3, new TestInterfaceActor());
    world.stage().directory().register(address4, new TestInterfaceActor());
    world.stage().directory().register(address5, new TestInterfaceActor());

    final ScanResult scanResult = new ScanResult(7);

    world.stage().maybeActorOf(NoProtocol.class, address5).andFinallyConsume(maybe -> {
      assertTrue(maybe.isPresent());
      scanResult.found();
    });
    world.stage().maybeActorOf(NoProtocol.class, address4).andFinallyConsume(maybe -> {
      assertTrue(maybe.isPresent());
      scanResult.found();
    });
    world.stage().maybeActorOf(NoProtocol.class, address3).andFinallyConsume(maybe -> {
      assertTrue(maybe.isPresent());
      scanResult.found();
    });
    world.stage().maybeActorOf(NoProtocol.class, address2).andFinallyConsume(maybe -> {
      assertTrue(maybe.isPresent());
      scanResult.found();
    });
    world.stage().maybeActorOf(NoProtocol.class, address1).andFinallyConsume(maybe -> {
      assertTrue(maybe.isPresent());
      scanResult.found();
    });

    world.stage().maybeActorOf(NoProtocol.class, address6)
      .andFinallyConsume(maybe -> {
        assertFalse(maybe.isPresent());
        scanResult.notFound();
      });
    world.stage().maybeActorOf(NoProtocol.class, address7)
      .andFinallyConsume(maybe -> {
        assertFalse(maybe.isPresent());
        scanResult.notFound();
      });

    assertEquals(5, scanResult.getFoundCount());
    assertEquals(2, scanResult.getNotFoundCount());
  }

  @Test
  public void testThatProtocolIsInterface() {
    world.stage().actorFor(NoProtocol.class, ParentInterfaceActor.class);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testThatProtocolIsNotInterface() {
    world.stage().actorFor(ParentInterfaceActor.class, ParentInterfaceActor.class);
  }

  @Test
  public void testThatProtocolsAreInterfaces() {
    world.stage().actorFor(new Class[] { NoProtocol.class, NoProtocol.class }, ParentInterfaceActor.class);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testThatProtocolsAreNotInterfaces() {
    world.stage().actorFor(new Class[] { NoProtocol.class, ParentInterfaceActor.class }, ParentInterfaceActor.class);
  }

  @Test
  public void testSingleThreadRawLookupOrStartFindsActorPreviouslyStartedWithActorFor() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    world.stage().actorFor(NoProtocol.class, definition, address);
    Actor existing = world.stage().rawLookupOrStart(definition, address);
    assertSame(address, existing.address());
  }

  @Test
  public void testSingleThreadRawLookupOrStartFindsActorPreviouslyStartedWithRawLookupOrStart() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    Actor started = world.stage().rawLookupOrStart(definition, address);
    Actor found = world.stage().rawLookupOrStart(definition, address);
    assertSame(started, found);
  }

  @Test
  public void testSingleThreadActorLookupOrStartFindsActorPreviouslyStartedWithActorFor() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    world.stage().actorFor(NoProtocol.class, definition, address);
    Actor existing = world.stage().actorLookupOrStart(definition, address);
    assertSame(address, existing.address());
  }

  @Test
  public void testSingleThreadActorLookupOrStartFindsActorPreviouslyStartedWithActorLookupOrStart() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    Actor started = world.stage().actorLookupOrStart(definition, address);
    Actor found = world.stage().actorLookupOrStart(definition, address);
    assertSame(started, found);
  }

  @Test
  public void testSingleThreadLookupOrStartFindsActorPreviouslyStartedWithActorFor() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    world.stage().actorFor(NoProtocol.class, definition, address);
    assertNotNull(world.stage().lookupOrStart(NoProtocol.class, definition, address));
  }

  @Test
  public void testSingleThreadLookupOrStartFindsActorPreviouslyStartedWithLookupOrStart() {
    Address address = world.addressFactory().unique();
    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);
    assertNotNull(world.stage().lookupOrStart(NoProtocol.class, definition, address));
    assertNotNull(world.stage().lookupOrStart(NoProtocol.class, definition, address));
  }

  private static final ExecutorService exec = Executors.newFixedThreadPool(32);

  @Test
  public void testMultiThreadRawLookupOrStartFindsActorPreviouslyStartedWIthRawLookupOrStart() {
    final int size = 1000;

    List<Address> addresses = IntStream.range(0, size)
        .mapToObj((ignored) -> world.addressFactory().unique())
        .collect(Collectors.toList());

    CompletionService<Actor> completionService =
        new ExecutorCompletionService<>(exec);

    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);

    multithreadedLookupOrStartTest(index ->
            completionService.submit(() ->
                world.stage()
                    .rawLookupOrStart(definition, addresses.get(index)))
        , size);
  }

  @Test
  public void testMultiThreadActorLookupOrStartFindsActorPreviouslyStartedWIthActorLookupOrStart() {
    final int size = 1000;

    List<Address> addresses = IntStream.range(0, size)
        .mapToObj((ignored) -> world.addressFactory().unique())
        .collect(Collectors.toList());

    CompletionService<Actor> completionService =
        new ExecutorCompletionService<>(exec);

    final Definition definition = Definition.has(ParentInterfaceActor.class,
        ParentInterfaceActor::new);

    multithreadedLookupOrStartTest(index ->
            completionService.submit(() ->
                world.stage()
                    .actorLookupOrStart(definition, addresses.get(index)))
        , size);
  }

  @Test
  public void testThatNonExistingActorCreates() {
    final Address address = world.addressFactory().unique();

    final AtomicInteger valueHolder = new AtomicInteger(0);
    final AccessSafely access = AccessSafely.afterCompleting(1);
    access.writingWith("value", (Integer value) -> valueHolder.set(value));
    access.readingWith("value", () -> valueHolder.get());

    final NoProtocol proxy1 = world.stage().actorOf(NoProtocol.class, address).await();

    Assert.assertNull(proxy1);
    Assert.assertEquals(0, valueHolder.get());

    final NoProtocol proxy2 = world.stage().actorOf(NoProtocol.class, address, NoExistingActor.class, 1, access).await();

    final int value = access.readFrom("value");

    Assert.assertNotNull(proxy2);
    Assert.assertEquals(1, value);
    Assert.assertEquals(value, valueHolder.get());
  }

  @Test
  public void testThatActorOfAddressIsPingedByThreeClients() {
    final Address address = world.addressFactory().unique();

    final AtomicInteger valueHolder = new AtomicInteger(0);

    final AccessSafely access = AccessSafely.afterCompleting(3);

    access.writingWith("value", (Integer value) -> valueHolder.set(value));
    access.readingWith("value", () -> valueHolder.get());

    final RingDing proxy1 = world.stage().actorOf(RingDing.class, address, RingDingActor.class, access).await();
    Assert.assertNotNull(proxy1);
    proxy1.ringDing();

    final RingDing proxy2 = world.stage().actorOf(RingDing.class, address, RingDingActor.class, access).await();
    Assert.assertNotNull(proxy2);
    proxy2.ringDing();

    final RingDing proxy3 = world.stage().actorOf(RingDing.class, address, RingDingActor.class, access).await();
    Assert.assertNotNull(proxy3);
    proxy3.ringDing();

    final int value = access.readFrom("value");

    Assert.assertEquals(3, value);
    Assert.assertEquals(value, valueHolder.get());
  }

  private void multithreadedLookupOrStartTest(final Function<Integer, Future<Actor>> work, final int size) {
    List<Future<Actor>> futures = IntStream.range(0, size)
        .flatMap(i -> IntStream.of(i, i))
        .mapToObj(work::apply)
        .collect(Collectors.toList());

    List<Actor> results = new ArrayList<>(futures.size());
    for (Future<Actor> future : futures) {
      try {
        final Actor actor = future.get();
        if (!results.isEmpty() && results.size() % 2 != 0) {
          final Actor expected = results.get(results.size() - 1);
          assertSame(expected.address(), actor.address());
        }
        results.add(actor);
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }
  }


  public static interface RingDing {
    void ringDing();
  }

  public static class RingDingActor extends Actor implements RingDing {
    private final AccessSafely access;
    private int value;

    public RingDingActor(final AccessSafely access) {
      this.access = access;
      this.value = 0;
    }

    @Override
    public void ringDing() {
      access.writeUsing("value", ++value);
    }
  }

  public static class NoExistingActor extends Actor implements NoProtocol {
    public NoExistingActor(final int value, final AccessSafely access) {
      access.writeUsing("value", value);
    }
  }

  public static class ParentInterfaceActor extends Actor implements NoProtocol {
    public static ThreadLocal<ParentInterfaceActor> parent = new ThreadLocal<>();

    public ParentInterfaceActor() { parent.set(this); }
  }

  public static class TestInterfaceActor extends Actor implements NoProtocol {
    public static ThreadLocal<TestInterfaceActor> instance = new ThreadLocal<>();

    public TestInterfaceActor() {
      instance.set(this);
    }
  }

  private static class ScanResult {
    final AccessSafely scanFound;

    private ScanResult(final int times) {
      final AtomicInteger foundCount = new AtomicInteger(0);
      final AtomicInteger notFoundCount = new AtomicInteger(0);
      this.scanFound = AccessSafely.afterCompleting(times)
              .writingWith("foundCount", (Integer ignored) -> foundCount.incrementAndGet())
              .readingWith("foundCount", foundCount::get)
              .writingWith("notFoundCount", (Integer ignored) -> notFoundCount.incrementAndGet())
              .readingWith("notFoundCount", notFoundCount::get);
    }

    private int getFoundCount(){
      return scanFound.readFrom("foundCount");
    }

    private int getNotFoundCount(){
      return scanFound.readFrom("notFoundCount");
    }

    private void found(){
      this.scanFound.writeUsing("foundCount", 1);
    }

    private void notFound(){
      this.scanFound.writeUsing("notFoundCount", 1);
    }
  }
}