/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 okhttp3.internal.platform;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests for {@link OptionalMethod}.
 */
public class OptionalMethodTest {
  @SuppressWarnings("unused")
  private static class BaseClass {
    public String stringMethod() {
      return "string";
    }

    public void voidMethod() {
    }
  }

  @SuppressWarnings("unused")
  private static class SubClass1 extends BaseClass {
    public String subclassMethod() {
      return "subclassMethod1";
    }

    public String methodWithArgs(String arg) {
      return arg;
    }
  }

  @SuppressWarnings("unused")
  private static class SubClass2 extends BaseClass {
    public int subclassMethod() {
      return 1234;
    }

    public String methodWithArgs(String arg) {
      return arg;
    }

    public void throwsException() throws IOException {
      throw new IOException();
    }

    public void throwsRuntimeException() throws Exception {
      throw new NumberFormatException();
    }

    protected void nonPublic() {
    }
  }

  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_ANY =
      new OptionalMethod<>(null, "stringMethod");
  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_STRING =
      new OptionalMethod<>(String.class, "stringMethod");
  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_INT =
      new OptionalMethod<>(Integer.TYPE, "stringMethod");
  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_ANY =
      new OptionalMethod<>(null, "voidMethod");
  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_VOID =
      new OptionalMethod<>(Void.TYPE, "voidMethod");
  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_ANY =
      new OptionalMethod<>(null, "subclassMethod");
  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_STRING =
      new OptionalMethod<>(String.class, "subclassMethod");
  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_INT =
      new OptionalMethod<>(Integer.TYPE, "subclassMethod");
  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_WRONG_PARAMS =
      new OptionalMethod<>(null, "methodWithArgs", Integer.class);
  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_CORRECT_PARAMS =
      new OptionalMethod<>(null, "methodWithArgs", String.class);

  private final static OptionalMethod<BaseClass> THROWS_EXCEPTION =
      new OptionalMethod<>(null, "throwsException");
  private final static OptionalMethod<BaseClass> THROWS_RUNTIME_EXCEPTION =
      new OptionalMethod<>(null, "throwsRuntimeException");
  private final static OptionalMethod<BaseClass> NON_PUBLIC =
      new OptionalMethod<>(null, "nonPublic");

  @Test
  public void isSupported() throws Exception {
    {
      BaseClass base = new BaseClass();
      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(base));
      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(base));
      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(base));
      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(base));
      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(base));
      assertFalse(SUBCLASS_METHOD_RETURNS_ANY.isSupported(base));
      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(base));
      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(base));
      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(base));
      assertFalse(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(base));
    }
    {
      SubClass1 subClass1 = new SubClass1();
      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass1));
      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass1));
      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass1));
      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass1));
      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass1));
      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass1));
      assertTrue(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass1));
      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass1));
      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass1));
      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass1));
    }
    {
      SubClass2 subClass2 = new SubClass2();
      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass2));
      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass2));
      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass2));
      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass2));
      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass2));
      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass2));
      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass2));
      assertTrue(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass2));
      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass2));
      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass2));
    }
  }

  @Test
  public void invoke() throws Exception {
    {
      BaseClass base = new BaseClass();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(base));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(base));
      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, base);
      assertNull(VOID_METHOD_RETURNS_ANY.invoke(base));
      assertNull(VOID_METHOD_RETURNS_VOID.invoke(base));
      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_ANY, base);
      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, base);
      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, base);
      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, base);
      assertErrorOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, base);
    }
    {
      SubClass1 subClass1 = new SubClass1();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass1));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass1));
      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass1);
      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass1));
      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass1));
      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass1));
      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invoke(subClass1));
      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, subClass1);
      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass1);
      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass1, "arg"));
    }

    {
      SubClass2 subClass2 = new SubClass2();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass2));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass2));
      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass2);
      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass2));
      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass2));
      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass2));
      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, subClass2);
      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invoke(subClass2));
      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass2);
      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass2, "arg"));
    }
  }

  @Test
  public void invokeBadArgs() throws Exception {
    SubClass1 subClass1 = new SubClass1();
    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1,
        new Object());
    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one",
        "two");
  }

  @Test
  public void invokeWithException() throws Exception {
    SubClass2 subClass2 = new SubClass2();
    try {
      THROWS_EXCEPTION.invoke(subClass2);
    } catch (InvocationTargetException expected) {
      assertTrue(expected.getTargetException() instanceof IOException);
    }

    try {
      THROWS_RUNTIME_EXCEPTION.invoke(subClass2);
    } catch (InvocationTargetException expected) {
      assertTrue(expected.getTargetException() instanceof NumberFormatException);
    }
  }

  @Test
  public void invokeNonPublic() throws Exception {
    SubClass2 subClass2 = new SubClass2();
    assertFalse(NON_PUBLIC.isSupported(subClass2));
    assertErrorOnInvoke(NON_PUBLIC, subClass2);
  }

  @Test
  public void invokeOptional() throws Exception {
    {
      BaseClass base = new BaseClass();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(base));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(base));
      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(base));
      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(base));
      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(base));
      assertNull(SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(base));
      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(base));
      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(base));
      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(base));
      assertNull(METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(base));
    }
    {
      SubClass1 subClass1 = new SubClass1();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass1));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass1));
      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass1));
      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass1));
      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass1));
      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass1));
      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass1));
      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass1));
      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass1));
      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass1, "arg"));
    }

    {
      SubClass2 subClass2 = new SubClass2();
      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass2));
      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass2));
      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass2));
      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass2));
      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass2));
      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass2));
      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass2));
      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass2));
      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass2));
      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass2, "arg"));
    }
  }

  @Test
  public void invokeOptionalBadArgs() throws Exception {
    SubClass1 subClass1 = new SubClass1();
    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS,
        subClass1); // no args
    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1,
        true);
    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1,
        new Object());
    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1,
        "one", "two");
  }

  @Test
  public void invokeOptionalWithException() throws Exception {
    SubClass2 subClass2 = new SubClass2();
    try {
      THROWS_EXCEPTION.invokeOptional(subClass2);
    } catch (InvocationTargetException expected) {
      assertTrue(expected.getTargetException() instanceof IOException);
    }

    try {
      THROWS_RUNTIME_EXCEPTION.invokeOptional(subClass2);
    } catch (InvocationTargetException expected) {
      assertTrue(expected.getTargetException() instanceof NumberFormatException);
    }
  }

  @Test
  @Ignore("Despite returning false for isSupported, invocation actually succeeds.")
  public void invokeOptionalNonPublic() throws Exception {
    SubClass2 subClass2 = new SubClass2();
    assertFalse(NON_PUBLIC.isSupported(subClass2));
    assertErrorOnInvokeOptional(NON_PUBLIC, subClass2);
  }

  private static <T> void assertErrorOnInvoke(
      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
    try {
      optionalMethod.invoke(base, args);
    } catch (Error expected) {
      return;
    }
    fail();
  }

  private static <T> void assertIllegalArgumentExceptionOnInvoke(
      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
    try {
      optionalMethod.invoke(base, args);
      fail();
    } catch (IllegalArgumentException expected) {
    }
  }

  private static <T> void assertErrorOnInvokeOptional(
      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
    try {
      optionalMethod.invokeOptional(base, args);
    } catch (Error expected) {
      return;
    }
    fail();
  }

  private static <T> void assertIllegalArgumentExceptionOnInvokeOptional(
      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
    try {
      optionalMethod.invokeOptional(base, args);
      fail();
    } catch (IllegalArgumentException expected) {
    }
  }
}
