// Copyright (C) 2015 The Android Open Source Project
//
// 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.gerrit.httpd;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
import com.google.inject.Key;
import com.google.inject.util.Providers;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;

public class AllRequestFilterFilterProxyTest {
  /**
   * Set of filters for FilterProxy
   *
   * <p>This set is used to as set of filters when fetching an {@link AllRequestFilter.FilterProxy}
   * instance through {@link #getFilterProxy()}.
   */
  private DynamicSet<AllRequestFilter> filters;

  @Before
  public void setUp() throws Exception {
    // Force starting each test with an initially empty set of filters.
    // Filters get added by the tests themselves.
    filters = new DynamicSet<>();
  }

  // The wrapping of {@link #getFilterProxy()} and
  // {@link #addFilter(AllRequestFilter)} into separate methods may seem
  // overengineered at this point. However, if in the future we decide to not
  // test the inner class directly, but rather test from the outside using
  // Guice Injectors, it is now sufficient to change only those two methods,
  // and we need not mess with the individual tests.

  /**
   * Obtain a FilterProxy with a known DynamicSet of filters
   *
   * <p>The returned {@link AllRequestFilter.FilterProxy} can have new filters added dynamically by
   * calling {@link #addFilter(AllRequestFilter)}.
   */
  private AllRequestFilter.FilterProxy getFilterProxy() {
    return new AllRequestFilter.FilterProxy(filters);
  }

  /**
   * Add a filter to created FilterProxy instances
   *
   * <p>This method adds the given filter to all {@link AllRequestFilter.FilterProxy} instances
   * created by {@link #getFilterProxy()}.
   */
  private ReloadableRegistrationHandle<AllRequestFilter> addFilter(AllRequestFilter filter) {
    Key<AllRequestFilter> key = Key.get(AllRequestFilter.class);
    return filters.add("gerrit", key, Providers.of(filter));
  }

  @Test
  public void noFilters() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req = new FakeHttpServletRequest();
    HttpServletResponse res = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();

    filterProxy.init(config);
    filterProxy.doFilter(req, res, chain);
    filterProxy.destroy();

    verify(chain).doFilter(req, res);
  }

  @Test
  public void singleFilterNoBubbling() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req = new FakeHttpServletRequest();
    HttpServletResponse res = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    AllRequestFilter filter = mock(AllRequestFilter.class);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    addFilter(filter);

    filterProxy.init(config);
    filterProxy.doFilter(req, res, chain);
    filterProxy.destroy();

    InOrder inorder = inOrder(filter);
    inorder.verify(filter).init(config);
    inorder.verify(filter).doFilter(eq(req), eq(res), any(FilterChain.class));
    inorder.verify(filter).destroy();
  }

  @Test
  public void singleFilterBubbling() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req = new FakeHttpServletRequest();
    HttpServletResponse res = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    ArgumentCaptor<FilterChain> capturedChain = ArgumentCaptor.forClass(FilterChain.class);

    AllRequestFilter filter = mock(AllRequestFilter.class);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    addFilter(filter);

    InOrder inorder = inOrder(filter, chain);

    filterProxy.init(config);
    filterProxy.doFilter(req, res, chain);

    inorder.verify(filter).init(config);
    inorder.verify(filter).doFilter(eq(req), eq(res), capturedChain.capture());
    capturedChain.getValue().doFilter(req, res);
    inorder.verify(chain).doFilter(req, res);

    filterProxy.destroy();
    inorder.verify(filter).destroy();
  }

  @Test
  public void twoFiltersNoBubbling() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req = new FakeHttpServletRequest();
    HttpServletResponse res = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    AllRequestFilter filterA = mock(AllRequestFilter.class);

    AllRequestFilter filterB = mock(AllRequestFilter.class);
    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    addFilter(filterA);
    addFilter(filterB);

    filterProxy.init(config);
    filterProxy.doFilter(req, res, chain);
    filterProxy.destroy();

    InOrder inorder = inOrder(filterA, filterB);
    inorder.verify(filterA).init(config);
    inorder.verify(filterB).init(config);
    inorder.verify(filterA).doFilter(eq(req), eq(res), any(FilterChain.class));
    inorder.verify(filterA).destroy();
    inorder.verify(filterB).destroy();
  }

  @Test
  public void twoFiltersBubbling() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req = new FakeHttpServletRequest();
    HttpServletResponse res = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    ArgumentCaptor<FilterChain> capturedChainA = ArgumentCaptor.forClass(FilterChain.class);
    ArgumentCaptor<FilterChain> capturedChainB = ArgumentCaptor.forClass(FilterChain.class);

    AllRequestFilter filterA = mock(AllRequestFilter.class);
    AllRequestFilter filterB = mock(AllRequestFilter.class);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    addFilter(filterA);
    addFilter(filterB);

    filterProxy.init(config);
    filterProxy.doFilter(req, res, chain);

    InOrder inorder = inOrder(filterA, filterB, chain);

    inorder.verify(filterA).init(config);
    inorder.verify(filterB).init(config);
    inorder.verify(filterA).doFilter(eq(req), eq(res), capturedChainA.capture());
    capturedChainA.getValue().doFilter(req, res);
    inorder.verify(filterB).doFilter(eq(req), eq(res), capturedChainB.capture());
    capturedChainB.getValue().doFilter(req, res);
    inorder.verify(chain).doFilter(req, res);

    filterProxy.destroy();
    inorder.verify(filterA).destroy();
    inorder.verify(filterB).destroy();
  }

  @Test
  public void postponedLoading() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req1 = new FakeHttpServletRequest();
    HttpServletRequest req2 = new FakeHttpServletRequest();
    HttpServletResponse res1 = new FakeHttpServletResponse();
    HttpServletResponse res2 = new FakeHttpServletResponse();

    FilterChain chain = mock(FilterChain.class);

    ArgumentCaptor<FilterChain> capturedChainA1 = ArgumentCaptor.forClass(FilterChain.class);
    ArgumentCaptor<FilterChain> capturedChainA2 = ArgumentCaptor.forClass(FilterChain.class);
    ArgumentCaptor<FilterChain> capturedChainB = ArgumentCaptor.forClass(FilterChain.class);

    AllRequestFilter filterA = mock(AllRequestFilter.class);
    AllRequestFilter filterB = mock(AllRequestFilter.class);

    InOrder inorder = inOrder(filterA, filterB, chain);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    addFilter(filterA);

    filterProxy.init(config);
    filterProxy.doFilter(req1, res1, chain);
    inorder.verify(filterA).init(config);
    inorder.verify(filterA).doFilter(eq(req1), eq(res1), capturedChainA1.capture());
    capturedChainA1.getValue().doFilter(req1, res1);
    inorder.verify(chain).doFilter(req1, res1);

    addFilter(filterB); // <-- Adds filter after filterProxy's init got called.
    filterProxy.doFilter(req2, res2, chain);
    // after filterProxy's init finished. Nonetheless filterB gets initialized.
    inorder.verify(filterA).doFilter(eq(req2), eq(res2), capturedChainA2.capture());
    capturedChainA2.getValue().doFilter(req2, res2);
    inorder.verify(filterB).init(config); // <-- This is crucial part. filterB got loaded
    inorder.verify(filterB).doFilter(eq(req2), eq(res2), capturedChainB.capture());
    capturedChainB.getValue().doFilter(req2, res2);
    inorder.verify(chain).doFilter(req2, res2);

    filterProxy.destroy();
    inorder.verify(filterA).destroy();
    inorder.verify(filterB).destroy();
  }

  @Test
  public void dynamicUnloading() throws Exception {
    FilterConfig config = mock(FilterConfig.class);
    HttpServletRequest req1 = new FakeHttpServletRequest();
    HttpServletRequest req2 = new FakeHttpServletRequest();
    HttpServletRequest req3 = new FakeHttpServletRequest();
    HttpServletResponse res1 = new FakeHttpServletResponse();
    HttpServletResponse res2 = new FakeHttpServletResponse();
    HttpServletResponse res3 = new FakeHttpServletResponse();

    Plugin plugin = mock(Plugin.class);

    FilterChain chain = mock(FilterChain.class);

    ArgumentCaptor<FilterChain> capturedChainA1 = ArgumentCaptor.forClass(FilterChain.class);
    ArgumentCaptor<FilterChain> capturedChainB1 = ArgumentCaptor.forClass(FilterChain.class);
    ArgumentCaptor<FilterChain> capturedChainB2 = ArgumentCaptor.forClass(FilterChain.class);

    AllRequestFilter filterA = mock(AllRequestFilter.class);
    AllRequestFilter filterB = mock(AllRequestFilter.class);

    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
    ReloadableRegistrationHandle<AllRequestFilter> handleFilterA = addFilter(filterA);
    ReloadableRegistrationHandle<AllRequestFilter> handleFilterB = addFilter(filterB);

    InOrder inorder = inOrder(filterA, filterB, chain);

    filterProxy.init(config);

    inorder.verify(filterA).init(config);
    inorder.verify(filterB).init(config);

    // Request #1 with filterA and filterB
    filterProxy.doFilter(req1, res1, chain);
    inorder.verify(filterA).doFilter(eq(req1), eq(res1), capturedChainA1.capture());
    capturedChainA1.getValue().doFilter(req1, res1);
    inorder.verify(filterB).doFilter(eq(req1), eq(res1), capturedChainB1.capture());
    capturedChainB1.getValue().doFilter(req1, res1);
    inorder.verify(chain).doFilter(req1, res1);

    // Unloading filterA
    handleFilterA.remove();
    filterProxy.onStopPlugin(plugin);

    inorder.verify(filterA).destroy(); // Cleaning up of filterA after it got unloaded

    // Request #2 only with filterB
    filterProxy.doFilter(req2, res2, chain);

    inorder.verify(filterB).doFilter(eq(req2), eq(res2), capturedChainB2.capture());
    inorder.verify(filterA, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));
    capturedChainB2.getValue().doFilter(req2, res2);
    inorder.verify(chain).doFilter(req2, res2);

    // Unloading filterB
    handleFilterB.remove();
    filterProxy.onStopPlugin(plugin);

    inorder.verify(filterB).destroy(); // Cleaning up of filterA after it got unloaded

    // Request #3 with no additional filters
    filterProxy.doFilter(req3, res3, chain);
    inorder.verify(chain).doFilter(req3, res3);
    inorder.verify(filterA, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));
    inorder.verify(filterB, never()).doFilter(eq(req2), eq(res2), any(FilterChain.class));

    filterProxy.destroy();
  }
}
