/*
 * 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 org.apache.knox.gateway.provider.federation;

import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import javax.security.auth.Subject;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.util.UUID;

import static org.easymock.EasyMock.anyObject;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class CommonJWTFilterTest {

  private AbstractJWTFilter handler;
  private static final String SERVICE_URL = "https://localhost:8888/gateway/sandbox";
  private static final String JWKS_PATH = "/knoxtoken/api/v1/jwks.json";

  @Before
  public void setUp() {
    handler = new TestHandler();
  }

  @After
  public void tearDown() {
    handler = null;
  }

  @Test
  public void testServerManagedTokenStateEnabledByProviderOverride() throws Exception {
    // Provider param should override gateway config
    assertTrue(doTestServerManagedTokenState(false, "true"));
  }

  @Test
  public void testServerManagedTokenStateDisabledByProviderOverride() throws Exception {
    // Provider param should override gateway config
    assertFalse(doTestServerManagedTokenState(true, "false"));
  }

  @Test
  public void testServerManagedTokenStateDisabledWithoutProviderOverride() throws Exception {
    // Missing provider param override should apply gateway config
    assertFalse(doTestServerManagedTokenState(false, null));
  }

  @Test
  public void testServerManagedTokenStateEnabledWithoutProviderOverride() throws Exception {
    // Missing provider param override should apply gateway config
    assertTrue(doTestServerManagedTokenState(true, null));
  }

  @Test
  public void testServerManagedTokenStateDisabledWitEmptyProviderOverride() throws Exception {
    // Empty provider param override should apply gateway config
    assertFalse(doTestServerManagedTokenState(false, ""));
  }

  @Test
  public void testServerManagedTokenStateEnabledWithEmptyProviderOverride() throws Exception {
    // Empty provider param override should apply gateway config
    assertTrue(doTestServerManagedTokenState(true, ""));
  }

  private boolean doTestServerManagedTokenState(final Boolean isEnabledAtGateway, final String providerParamValue)
      throws Exception {

    GatewayConfig gwConf = EasyMock.createNiceMock(GatewayConfig.class);
    EasyMock.expect(gwConf.isServerManagedTokenStateEnabled()).andReturn(isEnabledAtGateway).anyTimes();
    EasyMock.replay(gwConf);

    ServletContext sc = EasyMock.createNiceMock(ServletContext.class);
    EasyMock.expect(sc.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gwConf).anyTimes();
    EasyMock.replay(sc);

    FilterConfig fc = EasyMock.createNiceMock(FilterConfig.class);
    EasyMock.expect(fc.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED)).andReturn(providerParamValue).anyTimes();
    EasyMock.expect(fc.getServletContext()).andReturn(sc).anyTimes();
    EasyMock.replay(fc);

    return TokenUtils.isServerManagedTokenStateEnabled(fc);
  }

  @Test
  public void testIsStillValid() throws Exception {
    assertTrue("Expected the token to be valid because it has not yet expired.",
               doTestIsStillValid(System.currentTimeMillis() + 300000)); // 5 minutes later
  }

  @Test
  public void testIsStillValidExpired() throws Exception {
    assertFalse("Expected the token to be invalid because it has already expired.",
                doTestIsStillValid(System.currentTimeMillis() - 300000)); // 5 minutes ago
  }

  @Test(expected = UnknownTokenException.class)
  public void testIsStillValidUnknownToken() throws Exception {
    TokenStateService tss = EasyMock.createNiceMock(TokenStateService.class);
    final String tokenId = UUID.randomUUID().toString();
    EasyMock.expect(tss.getTokenExpiration(anyObject(JWT.class)))
            .andThrow(new UnknownTokenException(tokenId))
            .anyTimes();
    EasyMock.expect(tss.getTokenExpiration(anyObject(String.class)))
            .andThrow(new UnknownTokenException(tokenId))
            .anyTimes();
    EasyMock.replay(tss);

    doTestIsStillValid(tss);
  }

  @Test
  public void testUnauthenticatedList() throws Exception {
    HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
    FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class);

    EasyMock.expect(request.getPathInfo()).andReturn(JWKS_PATH).anyTimes();
    EasyMock.expect(request.getQueryString()).andReturn(null);
    HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
    EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL);
    EasyMock.expect(response.getOutputStream()).andAnswer(
        AbstractJWTFilterTest.DummyServletOutputStream::new).anyTimes();
    EasyMock.replay(request, response, filterConfig);


    JWTFederationFilter jwtFilter = new JWTFederationFilter();
    DummyFilterChain chain = new DummyFilterChain();

    jwtFilter.init(filterConfig);
    jwtFilter.doFilter(request, response, chain);
    Assert.assertTrue("doFilterCalled should be true.", chain.doFilterCalled );
    /* make sure the principal is anonymous */
    Assert.assertEquals("anonymous", chain.subject.getPrincipals().stream().findFirst().get().getName());
  }

  public static class DummyFilterChain implements FilterChain {
    boolean doFilterCalled;
    Subject subject;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException {
      doFilterCalled = true;
      subject = Subject.getSubject( AccessController.getContext() );
    }
  }

  private boolean doTestIsStillValid(final Long expiration) throws Exception {
    TokenStateService tss = EasyMock.createNiceMock(TokenStateService.class);
    EasyMock.expect(tss.getTokenExpiration(anyObject(JWT.class)))
            .andReturn(expiration)
            .anyTimes();
    EasyMock.expect(tss.getTokenExpiration(anyObject(String.class)))
            .andReturn(expiration)
            .anyTimes();
    EasyMock.replay(tss);
    return doTestIsStillValid(tss);
  }

  private boolean doTestIsStillValid(final TokenStateService tss) throws Exception {
    GatewayConfig gwConf = EasyMock.createNiceMock(GatewayConfig.class);
    EasyMock.expect(gwConf.isServerManagedTokenStateEnabled()).andReturn(true).anyTimes();
    EasyMock.replay(gwConf);

    ServletContext sc = EasyMock.createNiceMock(ServletContext.class);
    EasyMock.expect(sc.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gwConf).anyTimes();
    EasyMock.replay(sc);

    JWT jwt = EasyMock.createNiceMock(JWT.class);
    EasyMock.replay(jwt);

    Field tokenStateServiceField = AbstractJWTFilter.class.getDeclaredField("tokenStateService");
    tokenStateServiceField.setAccessible(true);
    tokenStateServiceField.set(handler, tss);

    Method m = AbstractJWTFilter.class.getDeclaredMethod("tokenIsStillValid", JWT.class);
    m.setAccessible(true);
    try {
      return (Boolean) m.invoke(handler, jwt);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof Exception) {
        throw (Exception) cause;
      } else {
        throw e;
      }
    }
  }

  static final class TestHandler extends AbstractJWTFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    }

    @Override
    protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status, String error) throws IOException {

    }

    @Override
    public void destroy() {

    }
  }

}
