package test.reflect;

import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.TestRunner;
import org.testng.annotations.DataProvider;
import org.testng.annotations.NoInjection;
import org.testng.annotations.Test;
import org.testng.internal.reflect.InjectableParameter;
import org.testng.internal.reflect.Parameter;
import org.testng.internal.reflect.ReflectionRecipes;
import org.testng.xml.XmlTest;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author <a href="mailto:nitin.matrix@gmail.com">Nitin Verma</a>
 */
public class ReflectionRecipesTest {

  private static final Object[] A0 = new Object[]{343, true};
  private static final Parameter[] S0 = getMethodParameters(T.class, "s0");
  private static final Parameter[] S1 = getMethodParameters(T.class, "s1");
  private static final Parameter[] S2 = getMethodParameters(T.class, "s2");
  private static final Parameter[] S3 = getMethodParameters(T.class, "s3");

  private static Parameter[] getMethodParameters(final Class<?> clazz, final String methodName) {
    Method method = null;
    Parameter[] parameters = null;
    for (final Method m : clazz.getMethods()) {
      if (m.getName().equals(methodName)) {
        method = m;
      }
    }
    if (method != null) {
      parameters = ReflectionRecipes.getMethodParameters(method);
    }
    return parameters;
  }

  @DataProvider
  public Object[][] methodInputParameters() {
    return new Object[][]{S0, S1, S2, S3};
  }

  @DataProvider
  public Object[][] methodInputParamArgsPair() {
    return new Object[][]{
      new Object[]{S0, A0},
      new Object[]{S1, A0},
      new Object[]{S2, A0},
      new Object[]{S3, A0},
    };
  }

  @DataProvider
  public Object[][] exactMatchDP() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    final Method[] methods = ExactMatchTest.class.getDeclaredMethods();
    final List<Object[]> objects = new ArrayList<>();
    System.out.println("exactMatchDP:");
    for (int i = 0; i < methods.length; i++) {
      final Method method = methods[i];
      final ExactMatchTest.Expectation annotation =
        method.getAnnotation(ExactMatchTest.Expectation.class);
      if (annotation != null) {
        final String provider = annotation.expectationProvider();
        final int flag = annotation.flag();
        Assert.assertNotNull(provider);
        final Method providerMethod = ExactMatchTest.class.getMethod(provider, int.class);
        final Object out = providerMethod.invoke(ExactMatchTest.class, flag);
        Assert.assertTrue(out instanceof Object[][]);
        System.out.println(method.getName() + ", " + out);
        objects.add(new Object[]{out, method});
      }
    }
    return objects.toArray(new Object[objects.size()][]);
  }

  @DataProvider
  public Object[][] matchArrayEndingDP() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    final Method[] methods = MatchArrayEndingTest.class.getDeclaredMethods();
    final List<Object[]> objects = new ArrayList<>();
    System.out.println("matchArrayEndingDP:");
    for (int i = 0; i < methods.length; i++) {
      final Method method = methods[i];
      final MatchArrayEndingTest.Expectation annotation =
        method.getAnnotation(MatchArrayEndingTest.Expectation.class);
      if (annotation != null) {
        final String provider = annotation.expectationProvider();
        final int flag = annotation.flag();
        Assert.assertNotNull(provider);
        final Method providerMethod = MatchArrayEndingTest.class.getMethod(provider, int.class);
        final Object out = providerMethod.invoke(MatchArrayEndingTest.class, flag);
        Assert.assertTrue(out instanceof Object[][]);
        System.out.println(method.getName() + ", " + out);
        objects.add(new Object[]{out, method});
      }
    }
    return objects.toArray(new Object[objects.size()][]);
  }

  @DataProvider
  public Object[][] testContexts() {
    return new Object[][]{
      {TestRunner.class},
      {ITestContext.class},
      {TestContextJustForTesting.class}
    };
  }

  @DataProvider
  public Object[][] notTestContexts() {
    return new Object[][]{
      {Object.class},
      {Class.class},
      {Connection.class}
    };
  }

  @Test(dataProvider = "matchArrayEndingDP")
  public void matchArrayEndingTest(final Object[][] expected, @NoInjection final Method method) {
    if (expected != null) {
      final Parameter[] methodParameters = ReflectionRecipes.getMethodParameters(method);
      Assert.assertNotNull(methodParameters);
      final Parameter[] filteredParameters =
        ReflectionRecipes.filter(methodParameters, InjectableParameter.Assistant.ALL_INJECTS);
      Assert.assertNotNull(filteredParameters);

      for (final Object[] expect : expected) {
        Assert.assertNotNull(expect);
        Assert.assertEquals(expect.length, 2);
        Assert.assertNotNull(expect[0]);
        Assert.assertTrue(expect[0] instanceof Object[]);
        Assert.assertNotNull(expect[1]);
        Assert.assertTrue(expect[1] instanceof Boolean);
        Assert.assertEquals(
          ReflectionRecipes.matchArrayEnding(filteredParameters, (Object[]) expect[0]),
          expect[1]
        );
      }
    }
  }

  @Test(dataProvider = "exactMatchDP")
  public void exactMatchTest(final Object[][] expected, @NoInjection final Method method) {
    if (expected != null) {
      final Parameter[] methodParameters = ReflectionRecipes.getMethodParameters(method);
      Assert.assertNotNull(methodParameters);
      final Parameter[] filteredParameters =
        ReflectionRecipes.filter(methodParameters, InjectableParameter.Assistant.ALL_INJECTS);
      Assert.assertNotNull(filteredParameters);

      for (final Object[] expect : expected) {
        Assert.assertNotNull(expect);
        Assert.assertEquals(expect.length, 2);
        Assert.assertNotNull(expect[0]);
        Assert.assertTrue(expect[0] instanceof Object[]);
        Assert.assertNotNull(expect[1]);
        Assert.assertTrue(expect[1] instanceof Boolean);
        Assert.assertEquals(
          ReflectionRecipes.exactMatch(filteredParameters, (Object[]) expect[0]),
          expect[1]
        );
      }
    }
  }

  @Test(dataProvider = "methodInputParameters")
  public void testFilters(final Parameter[] parameters) {
    System.out.println("In: " + Arrays.asList(parameters));
    final Parameter[] parameters1 = ReflectionRecipes.filter(parameters, InjectableParameter.Assistant.ALL_INJECTS);
    System.out.println("Out: " + Arrays.asList(parameters1));
    Assert.assertEquals(parameters1.length, 2);
    Assert.assertEquals(parameters1[0].getType(), int.class);
    Assert.assertEquals(parameters1[1].getType(), Boolean.class);
  }

  @Test(dataProvider = "methodInputParamArgsPair")
  public void testInject(final Parameter[] parameters, final Object[] args) {
    System.out.println("In: " + Arrays.asList(parameters));
    System.out.println("args: " + Arrays.asList(args));
    final Object[] injectedArgs = ReflectionRecipes.inject(
      parameters, InjectableParameter.Assistant.ALL_INJECTS, args,
      null, null, null
    );
    System.out.println("injectedArgs: " + Arrays.asList(injectedArgs));
    Assert.assertEquals(injectedArgs.length, parameters.length);
  }

  @Test(dataProvider = "testContexts")
  public void testIsOrImplementsInterface(final Class<?> clazz) {
    Assert.assertTrue(ReflectionRecipes.isOrImplementsInterface(ITestContext.class, clazz));
  }

  @Test(dataProvider = "notTestContexts")
  public void testNegativeCaseIsOrImplementsInterface(final Class<?> clazz) {
    Assert.assertFalse(ReflectionRecipes.isOrImplementsInterface(ITestContext.class, clazz));
  }

  private static interface T {
    public void s0(TestContextJustForTesting testContext, int i, Boolean b);

    public void s1(int i, ITestContext iTestContext, Boolean b);

    public void s2(int i, Boolean b, ITestContext iTestContext);

    public void s3(ITestContext iTestContext1, int i, Boolean b, ITestContext iTestContext2);
  }

  public static abstract class ExactMatchTest {
    public static Object[][] exactMatchData(final int flag) {
      switch (flag) {
        case 0:
          return new Object[][]{
            new Object[]{new Object[]{}, true},
            new Object[]{new Object[]{1}, false},
            new Object[]{new Object[]{""}, false},
            new Object[]{new Object[]{1, ""}, false},
          };
        case 1:
          return new Object[][]{
            new Object[]{new Object[]{1}, true},
            new Object[]{new Object[]{}, false},
            new Object[]{new Object[]{""}, false},
            new Object[]{new Object[]{1, ""}, false},
            new Object[]{new Object[]{"", 1}, false},
          };
        default:
          return null;
      }
    }

    @Expectation(expectationProvider = "exactMatchData", flag = 0)
    public abstract void s0();

    @Expectation(expectationProvider = "exactMatchData", flag = 0)
    public abstract void s0(final ITestContext a0);

    @Expectation(expectationProvider = "exactMatchData", flag = 0)
    public abstract void s0(final ITestContext a0, final ITestResult a1);

    @Expectation(expectationProvider = "exactMatchData", flag = 0)
    public abstract void s0(final ITestContext a0, final ITestResult a1, final XmlTest a2);

    @Expectation(expectationProvider = "exactMatchData", flag = 0)
    public abstract void s0(final ITestContext a0, final ITestResult a1, final XmlTest a2, final Method a3);

    @Expectation(expectationProvider = "exactMatchData", flag = 1)
    public abstract void s1(final int a0);

    @Expectation(expectationProvider = "exactMatchData", flag = 1)
    public abstract void s1(final ITestContext a0, final int a1);

    @Expectation(expectationProvider = "exactMatchData", flag = 1)
    public abstract void s1(final ITestContext a0, final Integer a1, final ITestResult a2);

    @Expectation(expectationProvider = "exactMatchData", flag = 1)
    public abstract void s1(final int a0, final ITestContext a1, final ITestResult a2, final XmlTest a3);

    @Expectation(expectationProvider = "exactMatchData", flag = 1)
    public abstract void s1(final ITestContext a0, final ITestResult a1, final int a2,
                            final XmlTest a3, final Method a4);

    @Retention(RUNTIME)
    @Target({METHOD})
    public static @interface Expectation {
      public String expectationProvider();

      public int flag();
    }
  }


  public static abstract class MatchArrayEndingTest {
    public static Object[][] matchArrayEndingData(final int flag) {
      switch (flag) {
        case 0:
          return new Object[][]{
            new Object[]{new Object[]{10f, 2.1f}, true},
            new Object[]{new Object[]{10}, false},
            new Object[]{new Object[]{10d, ""}, false},
            new Object[]{new Object[]{1, ""}, false},
          };
        case 1:
          return new Object[][]{
            new Object[]{new Object[]{1, 10f, 2.1f}, true},
            new Object[]{new Object[]{}, false},
            new Object[]{new Object[]{""}, false},
            new Object[]{new Object[]{10f, "", 2.1f}, false},
            new Object[]{new Object[]{"", 10f, 2.1f}, false},
          };
        default:
          return null;
      }
    }

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 0)
    public abstract void s0(final float... f);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 0)
    public abstract void s0(final float[] f, final ITestContext a0);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 0)
    public abstract void s0(final ITestContext a0, final float[] f, final ITestResult a1);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 0)
    public abstract void s0(final ITestContext a0, final ITestResult a1, final XmlTest a2, final float... f);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 0)
    public abstract void s0(final ITestContext a0, final ITestResult a1,
                            final XmlTest a2, final float[] f,
                            final Method a3);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 1)
    public abstract void s1(final int a0, final float... f);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 1)
    public abstract void s1(final ITestContext a0, final int a1, final float... f);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 1)
    public abstract void s1(final ITestContext a0, final Integer a1,
                            final ITestResult a2, final float... f);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 1)
    public abstract void s1(final int a0, final ITestContext a1, final ITestResult a2,
                            final float[] f, final XmlTest a3);

    @Expectation(expectationProvider = "matchArrayEndingData", flag = 1)
    public abstract void s1(final ITestContext a0, final ITestResult a1, final int a2,
                            final XmlTest a3, final float[] f, final Method a4);

    @Retention(RUNTIME)
    @Target({METHOD})
    public static @interface Expectation {
      public String expectationProvider();

      public int flag();
    }
  }

}
