/*
 * 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.swift;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.swift.exceptions.SwiftConnectionClosedException;
import org.apache.hadoop.fs.swift.http.SwiftProtocolConstants;
import org.apache.hadoop.fs.swift.util.SwiftTestUtils;
import org.apache.hadoop.io.IOUtils;
import org.junit.After;
import org.junit.Test;

import java.io.EOFException;
import java.io.IOException;

/**
 * Seek tests verify that
 * <ol>
 *   <li>When you seek on a 0 byte file to byte (0), it's not an error.</li>
 *   <li>When you seek past the end of a file, it's an error that should
 *   raise -what- EOFException?</li>
 *   <li>when you seek forwards, you get new data</li>
 *   <li>when you seek backwards, you get the previous data</li>
 *   <li>That this works for big multi-MB files as well as small ones.</li>
 * </ol>
 * These may seem "obvious", but the more the input streams try to be clever
 * about offsets and buffering, the more likely it is that seek() will start
 * to get confused.
 */
public class TestSeek extends SwiftFileSystemBaseTest {
  protected static final Log LOG =
    LogFactory.getLog(TestSeek.class);
  public static final int SMALL_SEEK_FILE_LEN = 256;

  private Path testPath;
  private Path smallSeekFile;
  private Path zeroByteFile;
  private FSDataInputStream instream;

  /**
   * Setup creates dirs under test/hadoop
   *
   * @throws Exception
   */
  @Override
  public void setUp() throws Exception {
    super.setUp();
    //delete the test directory
    testPath = path("/test");
    smallSeekFile = new Path(testPath, "seekfile.txt");
    zeroByteFile = new Path(testPath, "zero.txt");
    byte[] block = SwiftTestUtils.dataset(SMALL_SEEK_FILE_LEN, 0, 255);
    //this file now has a simple rule: offset => value
    createFile(smallSeekFile, block);
    createEmptyFile(zeroByteFile);
  }

  @After
  public void cleanFile() {
    IOUtils.closeStream(instream);
    instream = null;
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekZeroByteFile() throws Throwable {
    instream = fs.open(zeroByteFile);
    assertEquals(0, instream.getPos());
    //expect initial read to fai;
    int result = instream.read();
    assertMinusOne("initial byte read", result);
    byte[] buffer = new byte[1];
    //expect that seek to 0 works
    instream.seek(0);
    //reread, expect same exception
    result = instream.read();
    assertMinusOne("post-seek byte read", result);
    result = instream.read(buffer, 0, 1);
    assertMinusOne("post-seek buffer read", result);
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testBlockReadZeroByteFile() throws Throwable {
    instream = fs.open(zeroByteFile);
    assertEquals(0, instream.getPos());
    //expect that seek to 0 works
    byte[] buffer = new byte[1];
    int result = instream.read(buffer, 0, 1);
    assertMinusOne("block read zero byte file", result);
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekReadClosedFile() throws Throwable {
    instream = fs.open(smallSeekFile);
    instream.close();
    try {
      instream.seek(0);
    } catch (SwiftConnectionClosedException e) {
      //expected a closed file
    }
    try {
      instream.read();
    } catch (IOException e) {
      //expected a closed file
    }
    try {
      byte[] buffer = new byte[1];
      int result = instream.read(buffer, 0, 1);
    } catch (IOException e) {
      //expected a closed file
    }
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testNegativeSeek() throws Throwable {
    instream = fs.open(smallSeekFile);
    assertEquals(0, instream.getPos());
    try {
      instream.seek(-1);
      long p = instream.getPos();
      LOG.warn("Seek to -1 returned a position of " + p);
      int result = instream.read();
      fail(
        "expected an exception, got data " + result + " at a position of " + p);
    } catch (IOException e) {
      //bad seek -expected
    }
    assertEquals(0, instream.getPos());
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekFile() throws Throwable {
    instream = fs.open(smallSeekFile);
    assertEquals(0, instream.getPos());
    //expect that seek to 0 works
    instream.seek(0);
    int result = instream.read();
    assertEquals(0, result);
    assertEquals(1, instream.read());
    assertEquals(2, instream.getPos());
    assertEquals(2, instream.read());
    assertEquals(3, instream.getPos());
    instream.seek(128);
    assertEquals(128, instream.getPos());
    assertEquals(128, instream.read());
    instream.seek(63);
    assertEquals(63, instream.read());
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekAndReadPastEndOfFile() throws Throwable {
    instream = fs.open(smallSeekFile);
    assertEquals(0, instream.getPos());
    //expect that seek to 0 works
    //go just before the end
    instream.seek(SMALL_SEEK_FILE_LEN - 2);
    assertTrue("Premature EOF", instream.read() != -1);
    assertTrue("Premature EOF", instream.read() != -1);
    assertMinusOne("read past end of file", instream.read());
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekAndPastEndOfFileThenReseekAndRead() throws Throwable {
    instream = fs.open(smallSeekFile);
    //go just before the end. This may or may not fail; it may be delayed until the
    //read
    try {
      instream.seek(SMALL_SEEK_FILE_LEN);
      //if this doesn't trigger, then read() is expected to fail
      assertMinusOne("read after seeking past EOF", instream.read());
    } catch (EOFException expected) {
      //here an exception was raised in seek
    }
    instream.seek(1);
    assertTrue("Premature EOF", instream.read() != -1);
  }

  @Override
  protected Configuration createConfiguration() {
    Configuration conf = super.createConfiguration();
    conf.set(SwiftProtocolConstants.SWIFT_REQUEST_SIZE, "1");
    return conf;
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testSeekBigFile() throws Throwable {
    Path testSeekFile = new Path(testPath, "bigseekfile.txt");
    byte[] block = SwiftTestUtils.dataset(65536, 0, 255);
    createFile(testSeekFile, block);
    instream = fs.open(testSeekFile);
    assertEquals(0, instream.getPos());
    //expect that seek to 0 works
    instream.seek(0);
    int result = instream.read();
    assertEquals(0, result);
    assertEquals(1, instream.read());
    assertEquals(2, instream.read());

    //do seek 32KB ahead
    instream.seek(32768);
    assertEquals("@32768", block[32768], (byte) instream.read());
    instream.seek(40000);
    assertEquals("@40000", block[40000], (byte) instream.read());
    instream.seek(8191);
    assertEquals("@8191", block[8191], (byte) instream.read());
    instream.seek(0);
    assertEquals("@0", 0, (byte) instream.read());
  }

  @Test(timeout = SWIFT_TEST_TIMEOUT)
  public void testPositionedBulkReadDoesntChangePosition() throws Throwable {
    Path testSeekFile = new Path(testPath, "bigseekfile.txt");
    byte[] block = SwiftTestUtils.dataset(65536, 0, 255);
    createFile(testSeekFile, block);
    instream = fs.open(testSeekFile);
    instream.seek(39999);
    assertTrue(-1 != instream.read());
    assertEquals (40000, instream.getPos());

    byte[] readBuffer = new byte[256];
    instream.read(128, readBuffer, 0, readBuffer.length);
    //have gone back
    assertEquals(40000, instream.getPos());
    //content is the same too
    assertEquals("@40000", block[40000], (byte) instream.read());
    //now verify the picked up data
    for (int i = 0; i < 256; i++) {
      assertEquals("@" + i, block[i + 128], readBuffer[i]);
    }
  }

  /**
   * work out the expected byte from a specific offset
   * @param offset offset in the file
   * @return the value
   */
  int expectedByte(int offset) {
    return offset & 0xff;
  }
}