content: package ro.isdc.wro.model.resource.support.change;
  
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertNotNull;
  import static org.junit.Assert.assertTrue;
  import static org.mockito.Mockito.never;
  import static org.mockito.Mockito.verify;
  import static org.mockito.Mockito.when;
  import static org.mockito.MockitoAnnotations.initMocks;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.concurrent.Callable;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.concurrent.atomic.AtomicReference;
  
  import javax.servlet.FilterConfig;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  
  import org.junit.After;
  import org.junit.AfterClass;
  import org.junit.Before;
  import org.junit.BeforeClass;
  import org.junit.Test;
  import org.mockito.ArgumentCaptor;
  import org.mockito.Mock;
  import org.mockito.Mockito;
  import org.mockito.invocation.InvocationOnMock;
  import org.mockito.stubbing.Answer;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import ro.isdc.wro.cache.CacheKey;
  import ro.isdc.wro.cache.CacheStrategy;
  import ro.isdc.wro.cache.CacheValue;
  import ro.isdc.wro.config.Context;
  import ro.isdc.wro.config.support.ContextPropagatingCallable;
  import ro.isdc.wro.http.WroFilter;
  import ro.isdc.wro.manager.callback.LifecycleCallback;
  import ro.isdc.wro.manager.callback.LifecycleCallbackRegistry;
  import ro.isdc.wro.manager.callback.LifecycleCallbackSupport;
  import ro.isdc.wro.manager.factory.BaseWroManagerFactory;
  import ro.isdc.wro.manager.factory.WroManagerFactory;
  import ro.isdc.wro.model.WroModel;
  import ro.isdc.wro.model.factory.WroModelFactory;
  import ro.isdc.wro.model.group.Group;
  import ro.isdc.wro.model.group.Inject;
  import ro.isdc.wro.model.group.processor.Injector;
  import ro.isdc.wro.model.group.processor.InjectorBuilder;
  import ro.isdc.wro.model.resource.Resource;
  import ro.isdc.wro.model.resource.ResourceType;
  import ro.isdc.wro.model.resource.locator.UriLocator;
  import ro.isdc.wro.model.resource.locator.factory.AbstractUriLocatorFactory;
  import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
  import ro.isdc.wro.model.resource.support.change.ResourceWatcher.Callback;
  import ro.isdc.wro.util.Function;
  import ro.isdc.wro.util.ObjectFactory;
  import ro.isdc.wro.util.WroTestUtils;
  
  
  /**
   * @author Alex Objelean
   */
  public class TestResourceWatcher {
    private static final Logger LOG = LoggerFactory.getLogger(TestResourceWatcher.class);
    /**
     * The uri to the first resource in a group.
     */
    private static final String RESOURCE_JS_URI = "/path/1.js";
    private static final String RESOURCE_CSS_URI = "/test.css";
    private static final String GROUP_NAME = "g1";
    private static final String MIXED_GROUP_NAME = "mixedGroup";
    /**
     * Group containing two js resources.
     */
    private static final String GROUP_2 = "g2";
    private final CacheKey cacheKey = new CacheKey(GROUP_NAME, ResourceType.CSS, true);
    private final CacheKey cacheKey2 = new CacheKey(GROUP_2, ResourceType.JS, true);
    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private FilterConfig filterConfig;
    @Mock
    private UriLocator mockLocator;
    @Mock
    private Callback resourceWatcherCallback;
    @Mock
    private CacheStrategy<CacheKey, CacheValue> cacheStrategy;
  
    private ResourceWatcher victim;
  
    @BeforeClass
    public static void onBeforeClass() {
      assertEquals(0, Context.countActive());
    }
  
    @AfterClass
    public static void onAfterClass() {
      assertEquals(0, Context.countActive());
    }
  
    @Before
    public void setUp() {
      initMocks(this);
      Context.set(Context.webContext(request, response, filterConfig));
      // spy the interface instead of WroTestUtils.createResourceMockingLocator() because of mockito bug which was
      // reported on their mailing list.
      mockLocator = Mockito.spy(new UriLocator() {
        public InputStream locate(final String uri)
            throws IOException {
          return new ByteArrayInputStream(uri.getBytes());
        }
  
        public boolean accept(final String uri) {
          return true;
        }
      });
      // Add explicity the filter which makes the request allowed for async check
      when(request.getAttribute(Mockito.eq(WroFilter.ATTRIBUTE_PASSED_THROUGH_FILTER))).thenReturn(true);
  
      victim = new ResourceWatcher();
      createDefaultInjector().inject(victim);
    }
  
    @After
    public void tearDown()
        throws Exception {
      victim.destroy();
      Context.unset();
    }
  
    public Injector createDefaultInjector() {
      final UriLocatorFactory locatorFactory = new AbstractUriLocatorFactory() {
        public UriLocator getInstance(final String uri) {
          return mockLocator;
        }
      };
  
      final WroModel model = new WroModel().addGroup(new Group(GROUP_NAME).addResource(Resource.create(RESOURCE_CSS_URI)));
      model.addGroup(new Group(GROUP_2).addResource(Resource.create(RESOURCE_JS_URI)).addResource(
          Resource.create("/path/2.js")));
      model.addGroup(new Group(MIXED_GROUP_NAME).addResource(Resource.create(RESOURCE_CSS_URI)).addResource(
          Resource.create(RESOURCE_JS_URI)));
      final WroModelFactory modelFactory = WroTestUtils.simpleModelFactory(model);
      final WroManagerFactory factory = new BaseWroManagerFactory().setModelFactory(modelFactory).setUriLocatorFactory(
          locatorFactory).setCacheStrategy(cacheStrategy);
      final Injector injector = InjectorBuilder.create(factory).build();
      return injector;
    }
  
    @Test(expected = NullPointerException.class)
    public void cannotCheckNullCacheEntry() {
      Context.unset();
      victim.check(null);
    }
  
    @Test
    public void shouldNotDetectChangeAfterFirstRun()
        throws Exception {
      victim.check(cacheKey);
      assertFalse(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
    }
  
    @Test
    public void shouldDetectResourceChange()
        throws Exception {
      // flag used to assert that the expected code was invoked
      createDefaultInjector().inject(victim);
      victim.check(cacheKey, resourceWatcherCallback);
      assertFalse(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
  
      Mockito.when(mockLocator.locate(Mockito.anyString())).thenReturn(new ByteArrayInputStream("different".getBytes()));
      final ArgumentCaptor<CacheKey> argumentCaptor = ArgumentCaptor.forClass(CacheKey.class);
  
      victim.check(cacheKey);
      assertTrue(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
      Mockito.verify(resourceWatcherCallback).onGroupChanged(argumentCaptor.capture());
      assertEquals(GROUP_NAME, argumentCaptor.getValue().getGroupName());
    }
  
    @Test
    public void shouldAssumeResourceNotChangedWhenStreamIsUnavailable()
        throws Exception {
      createDefaultInjector().inject(victim);
      final ResourceChangeDetector mockChangeDetector = Mockito.spy(victim.getResourceChangeDetector());
  
      Mockito.when(mockLocator.locate(Mockito.anyString())).thenThrow(new IOException("Resource is unavailable"));
  
      victim.check(cacheKey, resourceWatcherCallback);
      verify(resourceWatcherCallback, never()).onGroupChanged(Mockito.any(CacheKey.class));
      verify(mockChangeDetector, never()).checkChangeForGroup(Mockito.anyString(), Mockito.anyString());
    }
  
    @Test
    public void shouldDetectChangeOfImportedResource()
        throws Exception {
      final String importResourceUri = "imported.css";
      final CacheKey cacheEntry = new CacheKey(GROUP_NAME, ResourceType.CSS, true);
      victim = new ResourceWatcher();
      createDefaultInjector().inject(victim);
      when(mockLocator.locate(Mockito.anyString())).thenAnswer(answerWithContent("initial"));
      when(mockLocator.locate("/" + Mockito.eq(RESOURCE_CSS_URI))).thenAnswer(
          answerWithContent(String.format("@import url(%s)", importResourceUri)));
  
      victim.check(cacheEntry, resourceWatcherCallback);
  
      when(mockLocator.locate(Mockito.anyString())).thenAnswer(answerWithContent("changed"));
      when(mockLocator.locate("/" + Mockito.eq(RESOURCE_CSS_URI))).thenAnswer(
          answerWithContent(String.format("@import url(%s)", importResourceUri)));
  
      victim.check(cacheEntry);
  
      verify(resourceWatcherCallback).onGroupChanged(Mockito.any(CacheKey.class));
      verify(resourceWatcherCallback).onResourceChanged(Mockito.any(Resource.class));
    }
  
    /**
     * Fix the issue described <a href="https://github.com/alexo/wro4j/issues/72">here</a>.
     */
    @Test
    public void shouldNotDetectErroneouslyChange()
        throws Exception {
      createDefaultInjector().inject(victim);
  
      // first check will always detect changes.
      victim.check(cacheKey2, resourceWatcherCallback);
  
      when(mockLocator.locate(RESOURCE_JS_URI)).thenAnswer(answerWithContent("changed"));
  
      victim.check(cacheKey2, resourceWatcherCallback);
      verify(resourceWatcherCallback, Mockito.atLeastOnce()).onGroupChanged(Mockito.any(CacheKey.class));
      verify(resourceWatcherCallback, Mockito.atLeastOnce()).onResourceChanged(Mockito.any(Resource.class));
  
      Mockito.reset(resourceWatcherCallback);
  
      // next check should find no change
      victim.check(cacheKey2, resourceWatcherCallback);
      verify(resourceWatcherCallback, Mockito.never()).onGroupChanged(Mockito.any(CacheKey.class));
      verify(resourceWatcherCallback, Mockito.never()).onResourceChanged(Mockito.any(Resource.class));
    }
  
    private static class CallbackRegistryHolder {
      @Inject
      private LifecycleCallbackRegistry registry;
    }
  
    @Test
    public void shouldInvokeCallbackWhenChangeIsDetected()
        throws Exception {
      final CallbackRegistryHolder callbackRegistryHolder = new CallbackRegistryHolder();
      final AtomicBoolean flag = new AtomicBoolean();
      final Injector injector = createDefaultInjector();
      injector.inject(victim);
      injector.inject(callbackRegistryHolder);
      callbackRegistryHolder.registry.registerCallback(new ObjectFactory<LifecycleCallback>() {
        public LifecycleCallback create() {
          return new LifecycleCallbackSupport() {
            @Override
            public void onResourceChanged(final Resource resource) {
              flag.set(true);
            }
          };
        }
      });
      victim.check(cacheKey);
      assertTrue(flag.get());
    }
  
    @Test
    public void shouldCheckForChangeAsynchronously()
        throws Exception {
      final int timeout = 100;
      Context.get().getConfig().setConnectionTimeout(timeout);
      final String invalidUrl = "http://localhost:1/";
      when(request.getRequestURL()).thenReturn(new StringBuffer(invalidUrl));
      when(request.getServletPath()).thenReturn("");
      final AtomicReference<Callable<Void>> asyncInvoker = new AtomicReference<Callable<Void>>();
      final AtomicReference<Exception> exceptionHolder = new AtomicReference<Exception>();
      victim = new ResourceWatcher() {
        @Override
        void submit(final Callable<Void> callable) {
          try {
            final Callable<Void> decorated = new ContextPropagatingCallable<Void>(callable) {
              @Override
              public Void call()
                  throws Exception {
                try {
                  callable.call();
                  return null;
                } catch (final Exception e) {
                  exceptionHolder.set(e);
                  throw e;
                } finally {
                  asyncInvoker.set(callable);
                }
              }
            };
            super.submit(decorated);
          } finally {
          }
        }
      };
      createDefaultInjector().inject(victim);
  
      Context.get().getConfig().setResourceWatcherAsync(true);
  
      victim.tryAsyncCheck(cacheKey);
      WroTestUtils.waitUntil(new Function<Void, Boolean>() {
        public Boolean apply(final Void input)
            throws Exception {
          return asyncInvoker.get() != null;
        }
      }, timeout * 2);
      assertNotNull(asyncInvoker.get());
      assertNotNull(exceptionHolder.get());
      // We expect a request to fail, since a request a localhost using some port from where we expect to get no response.
      LOG.debug("Exception: {}", exceptionHolder.get().getClass());
      assertTrue(exceptionHolder.get() instanceof IOException);
    }
  
    @Test
    public void shouldNotCheckAtAllWhenAsyncIsConfiguredButNotAllowed() {
      Context.get().getConfig().setResourceWatcherAsync(true);
      when(request.getAttribute(Mockito.eq(WroFilter.ATTRIBUTE_PASSED_THROUGH_FILTER))).thenReturn(null);
      final ResourceWatcher victimSpy = Mockito.spy(victim);
      victimSpy.tryAsyncCheck(cacheKey);
      verify(victimSpy, Mockito.never()).check(Mockito.eq(cacheKey));
    }
  
    @Test
    public void shouldRemoveKeyFromCacheStrategyWhenChangeDetected() {
      victim.check(cacheKey);
      final CacheValue cacheValue = null;
      verify(cacheStrategy).put(Mockito.eq(cacheKey), Mockito.eq(cacheValue));
    }
  
    private Answer<InputStream> answerWithContent(final String content) {
      return answerWithContent(content, 0);
    }
  
    private Answer<InputStream> answerWithContent(final String content, final long delay) {
      return new Answer<InputStream>() {
        public InputStream answer(final InvocationOnMock invocation)
            throws Throwable {
          if (delay > 0) {
            Thread.sleep(delay);
          }
          return new ByteArrayInputStream(content.getBytes());
        }
      };
    }
  
    @Test
    public void shouldCheckForResourceChangeAsynchronously()
        throws Exception {
      Context.get().getConfig().setResourceWatcherAsync(true);
      final CacheKey cacheKey1 = new CacheKey(MIXED_GROUP_NAME, ResourceType.CSS, true);
      final CacheKey cacheKey2 = new CacheKey(MIXED_GROUP_NAME, ResourceType.JS, true);
      // First check is required to ensure that the subsequent changes do not detect any change
      victim.check(cacheKey1);
      victim.check(cacheKey2);
  
      when(mockLocator.locate(RESOURCE_JS_URI)).thenAnswer(answerWithContent("changed"));
      victim.check(cacheKey2, resourceWatcherCallback);
      verify(resourceWatcherCallback).onGroupChanged(Mockito.any(CacheKey.class));
      verify(resourceWatcherCallback).onResourceChanged(Mockito.any(Resource.class));
    }
  
    private ContextPropagatingCallable<Void> createCheckingCallable(final CacheKey cacheKey, final Callback callback) {
      return new ContextPropagatingCallable<Void>(new Callable<Void>() {
        public Void call()
            throws Exception {
          victim.check(cacheKey, callback);
          return null;
        }
      });
    }
  }
  