/**
 * 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.hadoop.fs.s3a;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.file.AccessDeniedException;

import static org.apache.hadoop.fs.s3a.S3ATestConstants.TEST_FS_S3A_NAME;

/**
 * Tests that credentials can go into the URL. This includes a valid
 * set, and a check that an invalid set do at least get stripped out
 * of the final URI
 */
public class ITestS3ACredentialsInURL extends Assert {
  private S3AFileSystem fs;
  private static final Logger LOG =
      LoggerFactory.getLogger(ITestS3ACredentialsInURL.class);
  @Rule
  public Timeout testTimeout = new Timeout(30 * 60 * 1000);

  @After
  public void teardown() {
    IOUtils.closeStream(fs);
  }

  /**
   * Test instantiation.
   * @throws Throwable
   */
  @Test
  public void testInstantiateFromURL() throws Throwable {

    Configuration conf = new Configuration();
    String accessKey = conf.get(Constants.ACCESS_KEY);
    String secretKey = conf.get(Constants.SECRET_KEY);
    String fsname = conf.getTrimmed(TEST_FS_S3A_NAME, "");
    Assume.assumeNotNull(fsname, accessKey, secretKey);
    URI original = new URI(fsname);
    URI secretsURI = createUriWithEmbeddedSecrets(original,
        accessKey, secretKey);
    if (secretKey.contains("/")) {
      assertTrue("test URI encodes the / symbol", secretsURI.toString().
          contains("%252F"));
    }
    if (secretKey.contains("+")) {
      assertTrue("test URI encodes the + symbol", secretsURI.toString().
          contains("%252B"));
    }
    assertFalse("Does not contain secrets", original.equals(secretsURI));

    conf.set(TEST_FS_S3A_NAME, secretsURI.toString());
    conf.unset(Constants.ACCESS_KEY);
    conf.unset(Constants.SECRET_KEY);
    fs = S3ATestUtils.createTestFileSystem(conf);
    String fsURI = fs.getUri().toString();
    assertFalse("FS URI contains a @ symbol", fsURI.contains("@"));
    assertFalse("FS URI contains a % symbol", fsURI.contains("%"));
    if (!original.toString().startsWith(fsURI)) {
      fail("Filesystem URI does not match original");
    }
    validate("original path", new Path(original));
    validate("bare path", new Path("/"));
    validate("secrets path", new Path(secretsURI));
  }

  private void validate(String text, Path path) throws IOException {
    try {
      fs.canonicalizeUri(path.toUri());
      fs.checkPath(path);
      assertTrue(text + " Not a directory",
          fs.getFileStatus(new Path("/")).isDirectory());
      fs.globStatus(path);
    } catch (AssertionError e) {
      throw e;
    } catch (Exception e) {
      LOG.debug("{} failure: {}", text, e, e);
      fail(text + " Test failed");
    }
  }

  /**
   * Set up some invalid credentials, verify login is rejected.
   * @throws Throwable
   */
  @Test
  public void testInvalidCredentialsFail() throws Throwable {
    Configuration conf = new Configuration();
    String fsname = conf.getTrimmed(TEST_FS_S3A_NAME, "");
    Assume.assumeNotNull(fsname);
    URI original = new URI(fsname);
    URI testURI = createUriWithEmbeddedSecrets(original, "user", "//");

    conf.set(TEST_FS_S3A_NAME, testURI.toString());
    fs = S3ATestUtils.createTestFileSystem(conf);
    try {
      S3AFileStatus status = fs.getFileStatus(new Path("/"));
      fail("Expected an AccessDeniedException, got " + status);
    } catch (AccessDeniedException e) {
      // expected
    }

  }

  private URI createUriWithEmbeddedSecrets(URI original,
      String accessKey,
      String secretKey) throws UnsupportedEncodingException {
    String encodedSecretKey = URLEncoder.encode(secretKey, "UTF-8");
    String formattedString = String.format("%s://%s:%s@%s/%s/",
        original.getScheme(),
        accessKey,
        encodedSecretKey,
        original.getHost(),
        original.getPath());
    URI testURI;
    try {
      testURI = new Path(formattedString).toUri();
    } catch (IllegalArgumentException e) {
      // inner cause is stripped to keep any secrets out of stack traces
      throw new IllegalArgumentException("Could not encode Path");
    }
    return testURI;
  }
}
