/*
 * Copyright (C) 2009 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.directExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
import org.jspecify.annotations.NullUnmarked;

/**
 * Tests for {@link AbstractIdleService}.
 *
 * @author Chris Nokleberg
 * @author Ben Yu
 */
@NullUnmarked
public class AbstractIdleServiceTest extends TestCase {
  public void testStart() {
    TestService service = new TestService();
    assertEquals(0, service.startUpCalled);
    service.startAsync().awaitRunning();
    assertEquals(1, service.startUpCalled);
    assertEquals(Service.State.RUNNING, service.state());
    assertThat(service.transitionStates).containsExactly(Service.State.STARTING);
  }

  public void testStart_failed() {
    Exception exception = new Exception("deliberate");
    TestService service =
        new TestService() {
          @Override
          protected void startUp() throws Exception {
            super.startUp();
            throw exception;
          }
        };
    assertEquals(0, service.startUpCalled);
    RuntimeException e =
        assertThrows(RuntimeException.class, () -> service.startAsync().awaitRunning());
    assertThat(e).hasCauseThat().isSameInstanceAs(exception);
    assertEquals(1, service.startUpCalled);
    assertEquals(Service.State.FAILED, service.state());
    assertThat(service.transitionStates).containsExactly(Service.State.STARTING);
  }

  public void testStop_withoutStart() {
    TestService service = new TestService();
    service.stopAsync().awaitTerminated();
    assertEquals(0, service.startUpCalled);
    assertEquals(0, service.shutDownCalled);
    assertEquals(Service.State.TERMINATED, service.state());
    assertThat(service.transitionStates).isEmpty();
  }

  public void testStop_afterStart() {
    TestService service = new TestService();
    service.startAsync().awaitRunning();
    assertEquals(1, service.startUpCalled);
    assertEquals(0, service.shutDownCalled);
    service.stopAsync().awaitTerminated();
    assertEquals(1, service.startUpCalled);
    assertEquals(1, service.shutDownCalled);
    assertEquals(Service.State.TERMINATED, service.state());
    assertThat(service.transitionStates)
        .containsExactly(Service.State.STARTING, Service.State.STOPPING)
        .inOrder();
  }

  public void testStop_failed() {
    Exception exception = new Exception("deliberate");
    TestService service =
        new TestService() {
          @Override
          protected void shutDown() throws Exception {
            super.shutDown();
            throw exception;
          }
        };
    service.startAsync().awaitRunning();
    assertEquals(1, service.startUpCalled);
    assertEquals(0, service.shutDownCalled);
    RuntimeException e =
        assertThrows(RuntimeException.class, () -> service.stopAsync().awaitTerminated());
    assertThat(e).hasCauseThat().isSameInstanceAs(exception);
    assertEquals(1, service.startUpCalled);
    assertEquals(1, service.shutDownCalled);
    assertEquals(Service.State.FAILED, service.state());
    assertThat(service.transitionStates)
        .containsExactly(Service.State.STARTING, Service.State.STOPPING)
        .inOrder();
  }

  public void testServiceToString() {
    AbstractIdleService service = new TestService();
    assertEquals("TestService [NEW]", service.toString());
    service.startAsync().awaitRunning();
    assertEquals("TestService [RUNNING]", service.toString());
    service.stopAsync().awaitTerminated();
    assertEquals("TestService [TERMINATED]", service.toString());
  }

  public void testTimeout() throws Exception {
    // Create a service whose executor will never run its commands
    Service service =
        new TestService() {
          @Override
          protected Executor executor() {
            return new Executor() {
              @Override
              public void execute(Runnable command) {}
            };
          }

          @Override
          protected String serviceName() {
            return "Foo";
          }
        };
    TimeoutException e =
        assertThrows(
            TimeoutException.class, () -> service.startAsync().awaitRunning(1, MILLISECONDS));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo("Timed out waiting for Foo [STARTING] to reach the RUNNING state.");
  }

  private static class TestService extends AbstractIdleService {
    int startUpCalled = 0;
    int shutDownCalled = 0;
    final List<State> transitionStates = new ArrayList<>();

    @Override
    protected void startUp() throws Exception {
      assertEquals(0, startUpCalled);
      assertEquals(0, shutDownCalled);
      startUpCalled++;
      assertEquals(State.STARTING, state());
    }

    @Override
    protected void shutDown() throws Exception {
      assertEquals(1, startUpCalled);
      assertEquals(0, shutDownCalled);
      shutDownCalled++;
      assertEquals(State.STOPPING, state());
    }

    @Override
    protected Executor executor() {
      transitionStates.add(state());
      return directExecutor();
    }
  }

  // Functional tests using real thread. We only verify publicly visible state.
  // Interaction assertions are done by the single-threaded unit tests.

  private static class DefaultService extends AbstractIdleService {
    @Override
    protected void startUp() throws Exception {}

    @Override
    protected void shutDown() throws Exception {}
  }

  public void testFunctionalServiceStartStop() {
    AbstractIdleService service = new DefaultService();
    service.startAsync().awaitRunning();
    assertEquals(Service.State.RUNNING, service.state());
    service.stopAsync().awaitTerminated();
    assertEquals(Service.State.TERMINATED, service.state());
  }

  public void testFunctionalStart_failed() {
    Exception exception = new Exception("deliberate");
    AbstractIdleService service =
        new DefaultService() {
          @Override
          protected void startUp() throws Exception {
            throw exception;
          }
        };
    RuntimeException e =
        assertThrows(RuntimeException.class, () -> service.startAsync().awaitRunning());
    assertThat(e).hasCauseThat().isSameInstanceAs(exception);
    assertEquals(Service.State.FAILED, service.state());
  }

  public void testFunctionalStop_failed() {
    Exception exception = new Exception("deliberate");
    AbstractIdleService service =
        new DefaultService() {
          @Override
          protected void shutDown() throws Exception {
            throw exception;
          }
        };
    service.startAsync().awaitRunning();
    RuntimeException e =
        assertThrows(RuntimeException.class, () -> service.stopAsync().awaitTerminated());
    assertThat(e).hasCauseThat().isSameInstanceAs(exception);
    assertEquals(Service.State.FAILED, service.state());
  }
}
