/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.common.util.concurrent;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static com.google.common.util.concurrent.Runnables.doNothing;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
import org.jspecify.annotations.NullUnmarked;

/**
 * Test for {@link WrappingExecutorService}
 *
 * @author Chris Nokleberg
 */
@NullUnmarked
public class WrappingExecutorServiceTest extends TestCase {
  private static final String RESULT_VALUE = "ran";

  // Uninteresting delegations
  public void testDelegations() throws InterruptedException {
    MockExecutor mock = new MockExecutor();
    TestExecutor testExecutor = new TestExecutor(mock);
    assertFalse(testExecutor.awaitTermination(10, MILLISECONDS));
    mock.assertLastMethodCalled("awaitTermination");
    assertFalse(testExecutor.isTerminated());
    mock.assertLastMethodCalled("isTerminated");
    assertFalse(testExecutor.isShutdown());
    mock.assertLastMethodCalled("isShutdown");
    testExecutor.shutdown();
    mock.assertLastMethodCalled("shutdown");
    List<Runnable> list = testExecutor.shutdownNow();
    mock.assertLastMethodCalled("shutdownNow");
    assertEquals(ImmutableList.of(), list);
  }

  public void testExecute() {
    MockExecutor mock = new MockExecutor();
    TestExecutor testExecutor = new TestExecutor(mock);
    testExecutor.execute(doNothing());
    mock.assertLastMethodCalled("execute");
  }

  public void testSubmit() throws InterruptedException, ExecutionException {
    {
      MockExecutor mock = new MockExecutor();
      TestExecutor testExecutor = new TestExecutor(mock);
      Future<?> f = testExecutor.submit(doNothing());
      mock.assertLastMethodCalled("submit");
      f.get();
    }
    {
      MockExecutor mock = new MockExecutor();
      TestExecutor testExecutor = new TestExecutor(mock);
      Future<String> f = testExecutor.submit(doNothing(), RESULT_VALUE);
      mock.assertLastMethodCalled("submit");
      assertEquals(RESULT_VALUE, f.get());
    }
    {
      MockExecutor mock = new MockExecutor();
      TestExecutor testExecutor = new TestExecutor(mock);
      Callable<String> task = Callables.returning(RESULT_VALUE);
      Future<String> f = testExecutor.submit(task);
      mock.assertLastMethodCalled("submit");
      assertEquals(RESULT_VALUE, f.get());
    }
  }

  public void testInvokeAll() throws InterruptedException, ExecutionException {
    List<Callable<String>> tasks = createTasks(3);
    {
      MockExecutor mock = new MockExecutor();
      TestExecutor testExecutor = new TestExecutor(mock);
      List<Future<String>> futures = testExecutor.invokeAll(tasks);
      mock.assertLastMethodCalled("invokeAll");
      checkResults(futures);
    }
    {
      MockExecutor mock = new MockExecutor();
      TimeUnit unit = SECONDS;
      long timeout = 5;
      TestExecutor testExecutor = new TestExecutor(mock);
      List<Future<String>> futures = testExecutor.invokeAll(tasks, timeout, unit);
      mock.assertMethodWithTimeout("invokeAll", timeout, unit);
      checkResults(futures);
    }
  }

  public void testInvokeAny() throws InterruptedException, ExecutionException, TimeoutException {
    List<Callable<String>> tasks = createTasks(3);
    {
      MockExecutor mock = new MockExecutor();
      TestExecutor testExecutor = new TestExecutor(mock);
      String s = testExecutor.invokeAny(tasks);
      assertEquals("ran0", s);
      mock.assertLastMethodCalled("invokeAny");
    }
    {
      MockExecutor mock = new MockExecutor();
      TimeUnit unit = SECONDS;
      long timeout = 5;
      TestExecutor testExecutor = new TestExecutor(mock);
      String s = testExecutor.invokeAny(tasks, timeout, unit);
      assertEquals(RESULT_VALUE + "0", s);
      mock.assertMethodWithTimeout("invokeAny", timeout, unit);
    }
  }

  private static void checkResults(List<Future<String>> futures)
      throws InterruptedException, ExecutionException {
    for (int i = 0; i < futures.size(); i++) {
      assertEquals(RESULT_VALUE + i, futures.get(i).get());
    }
  }

  private static List<Callable<String>> createTasks(int n) {
    List<Callable<String>> callables = new ArrayList<>();
    for (int i = 0; i < n; i++) {
      callables.add(Callables.returning(RESULT_VALUE + i));
    }
    return callables;
  }

  private static final class WrappedCallable<T> implements Callable<T> {
    private final Callable<T> delegate;

    WrappedCallable(Callable<T> delegate) {
      this.delegate = delegate;
    }

    @Override
    public T call() throws Exception {
      return delegate.call();
    }
  }

  private static final class WrappedRunnable implements Runnable {
    private final Runnable delegate;

    WrappedRunnable(Runnable delegate) {
      this.delegate = delegate;
    }

    @Override
    public void run() {
      delegate.run();
    }
  }

  private static final class TestExecutor extends WrappingExecutorService {
    TestExecutor(MockExecutor mock) {
      super(mock);
    }

    @Override
    protected <T> Callable<T> wrapTask(Callable<T> callable) {
      return new WrappedCallable<T>(callable);
    }

    @Override
    protected Runnable wrapTask(Runnable command) {
      return new WrappedRunnable(command);
    }
  }

  // TODO: If this test can ever depend on Mockito or the like, use it instead.
  private static final class MockExecutor implements ExecutorService {
    private String lastMethodCalled = "";
    private long lastTimeoutInMillis = -1;
    private final ExecutorService inline = newDirectExecutorService();

    void assertLastMethodCalled(String method) {
      assertEquals(method, lastMethodCalled);
    }

    void assertMethodWithTimeout(String method, long timeout, TimeUnit unit) {
      assertLastMethodCalled(method + "Timeout");
      assertEquals(unit.toMillis(timeout), lastTimeoutInMillis);
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) {
      lastMethodCalled = "awaitTermination";
      return false;
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
      lastMethodCalled = "invokeAll";
      assertTaskWrapped(tasks);
      return inline.invokeAll(tasks);
    }

    @Override
    public <T> List<Future<T>> invokeAll(
        Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException {
      assertTaskWrapped(tasks);
      lastMethodCalled = "invokeAllTimeout";
      lastTimeoutInMillis = unit.toMillis(timeout);
      return inline.invokeAll(tasks, timeout, unit);
    }

    // Define the invokeAny methods to invoke the first task
    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws ExecutionException, InterruptedException {
      assertTaskWrapped(tasks);
      lastMethodCalled = "invokeAny";
      return inline.submit(Iterables.get(tasks, 0)).get();
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws ExecutionException, InterruptedException, TimeoutException {
      assertTaskWrapped(tasks);
      lastMethodCalled = "invokeAnyTimeout";
      lastTimeoutInMillis = unit.toMillis(timeout);
      return inline.submit(Iterables.get(tasks, 0)).get(timeout, unit);
    }

    @Override
    public boolean isShutdown() {
      lastMethodCalled = "isShutdown";
      return false;
    }

    @Override
    public boolean isTerminated() {
      lastMethodCalled = "isTerminated";
      return false;
    }

    @Override
    public void shutdown() {
      lastMethodCalled = "shutdown";
    }

    @Override
    public List<Runnable> shutdownNow() {
      lastMethodCalled = "shutdownNow";
      return ImmutableList.of();
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
      lastMethodCalled = "submit";
      assertThat(task).isInstanceOf(WrappedCallable.class);
      return inline.submit(task);
    }

    @Override
    public Future<?> submit(Runnable task) {
      lastMethodCalled = "submit";
      assertThat(task).isInstanceOf(WrappedRunnable.class);
      return inline.submit(task);
    }

    @Override
    public <T> Future<T> submit(Runnable task, T result) {
      lastMethodCalled = "submit";
      assertThat(task).isInstanceOf(WrappedRunnable.class);
      return inline.submit(task, result);
    }

    @Override
    public void execute(Runnable command) {
      lastMethodCalled = "execute";
      assertThat(command).isInstanceOf(WrappedRunnable.class);
      inline.execute(command);
    }

    private static <T> void assertTaskWrapped(Collection<? extends Callable<T>> tasks) {
      Predicate<Object> p = Predicates.instanceOf(WrappedCallable.class);
      assertTrue(Iterables.all(tasks, p));
    }
  }
}
