package org.jooby.internal;

import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;

import org.jooby.Env;
import org.jooby.test.MockUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.google.common.io.Resources;
import com.typesafe.config.Config;

@RunWith(PowerMockRunner.class)
@PrepareForTest({RouteMetadata.class, Resources.class, URL.class, ClassReader.class })
public class RouteMetadataTest {

  public static class Mvc {

    public Mvc() {
    }

    public Mvc(final String s) {
    }

    public void noarg() {

    }

    public void arg(final double v) {

    }

    public void arg(final String x) {

    }

    public void arg(final double v, final int u) {

    }

    public static void staticMethod() {

    }
  }

  @Test
  public void noargconst() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Constructor<?> constructor = Mvc.class.getDeclaredConstructor();
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[0], ci.names(constructor));
          assertEquals(35, ci.startAt(constructor));
        });
  }

  @Test
  public void consArgS() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Constructor<?> constructor = Mvc.class.getDeclaredConstructor(String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[]{"s" }, ci.names(constructor));
          assertEquals(38, ci.startAt(constructor));
        });
  }

  @Test
  public void noargmethod() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("noarg");
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[0], ci.names(m));
          assertEquals(43, ci.startAt(m));
        });
  }

  @Test
  public void argI() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("arg", double.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[]{"v" }, ci.names(m));
          assertEquals(47, ci.startAt(m));
        });
  }

  @Test
  public void argS() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("arg", String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[]{"x" }, ci.names(m));
          assertEquals(51, ci.startAt(m));
        });
  }

  @Test
  public void argVU() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("arg", double.class, int.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          assertArrayEquals(new String[]{"v", "u" }, ci.names(m));
          assertEquals(55, ci.startAt(m));
        });
  }

  @Test
  public void nocache() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("arg", String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          String[] params1 = ci.names(m);
          String[] params2 = ci.names(m);
          assertNotSame(params1, params2);
        });
  }

  @Test
  public void nocacheMavenBuild() throws Exception {
    InputStream stream = getClass().getResourceAsStream("RouteMetadataTest$Mvc.bc");
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .expect(unit -> {
          URL resource = unit.mock(URL.class);
          expect(resource.openStream()).andReturn(stream);
          unit.mockStatic(Resources.class);
          expect(Resources.getResource(Mvc.class, "RouteMetadataTest$Mvc.class"))
              .andReturn(resource);
        })
        .run(unit -> {
          Method method = Mvc.class.getDeclaredMethod("arg", String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          String[] params = ci.names(method);
          assertEquals("x", params[0]);
        });
  }

  @Test(expected = IllegalStateException.class)
  public void cannotReadByteCode() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("dev");
        })
        .expect(unit -> {
          InputStream stream = unit.mock(InputStream.class);
          stream.close();
          URL resource = unit.mock(URL.class);
          expect(resource.openStream()).andReturn(stream);

          ClassReader reader = unit
              .mockConstructor(ClassReader.class, new Class[]{InputStream.class }, stream);
          reader.accept(isA(ClassVisitor.class), eq(0));
          expectLastCall().andThrow(new NullPointerException("intentional err"));

          unit.mockStatic(Resources.class);
          expect(Resources.getResource(Mvc.class, "RouteMetadataTest$Mvc.class"))
              .andReturn(resource);
        })
        .run(unit -> {
          Method method = Mvc.class.getDeclaredMethod("arg", String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          String[] params = ci.names(method);
          assertEquals("x", params[0]);
        });
  }

  @Test
  public void withcache() throws Exception {
    new MockUnit(Config.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.hasPath("application.env")).andReturn(true);
          expect(config.getString("application.env")).andReturn("prod");
        })
        .run(unit -> {
          Method m = Mvc.class.getDeclaredMethod("arg", String.class);
          RouteMetadata ci = new RouteMetadata(Env.DEFAULT.build(unit.get(Config.class)));
          String[] params1 = ci.names(m);
          String[] params2 = ci.names(m);
          assertSame(params1, params2);
        });
  }

}
