/*
 * Copyright 2012-2017 the original author or authors.
 *
 * 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 org.springframework.boot.loader.data;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests for {@link RandomAccessDataFile}.
 *
 * @author Phillip Webb
 */
public class RandomAccessDataFileTests {

	private static final byte[] BYTES;

	static {
		BYTES = new byte[256];
		for (int i = 0; i < BYTES.length; i++) {
			BYTES[i] = (byte) i;
		}
	}

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	@Rule
	public TemporaryFolder temporaryFolder = new TemporaryFolder();

	private File tempFile;

	private RandomAccessDataFile file;

	private InputStream inputStream;

	@Before
	public void setup() throws Exception {
		this.tempFile = this.temporaryFolder.newFile();
		FileOutputStream outputStream = new FileOutputStream(this.tempFile);
		outputStream.write(BYTES);
		outputStream.close();
		this.file = new RandomAccessDataFile(this.tempFile);
		this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
	}

	@After
	public void cleanup() throws Exception {
		this.inputStream.close();
		this.file.close();
	}

	@Test
	public void fileNotNull() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("File must not be null");
		new RandomAccessDataFile(null);
	}

	@Test
	public void fileExists() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("File must exist");
		new RandomAccessDataFile(new File("/does/not/exist"));
	}

	@Test
	public void fileNotNullWithConcurrentReads() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("File must not be null");
		new RandomAccessDataFile(null, 1);
	}

	@Test
	public void fileExistsWithConcurrentReads() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("File must exist");
		new RandomAccessDataFile(new File("/does/not/exist"), 1);
	}

	@Test
	public void inputStreamRead() throws Exception {
		for (int i = 0; i <= 255; i++) {
			assertThat(this.inputStream.read()).isEqualTo(i);
		}
	}

	@Test
	public void inputStreamReadNullBytes() throws Exception {
		this.thrown.expect(NullPointerException.class);
		this.thrown.expectMessage("Bytes must not be null");
		this.inputStream.read(null);
	}

	@Test
	public void inputStreamReadNullBytesWithOffset() throws Exception {
		this.thrown.expect(NullPointerException.class);
		this.thrown.expectMessage("Bytes must not be null");
		this.inputStream.read(null, 0, 1);
	}

	@Test
	public void inputStreamReadBytes() throws Exception {
		byte[] b = new byte[256];
		int amountRead = this.inputStream.read(b);
		assertThat(b).isEqualTo(BYTES);
		assertThat(amountRead).isEqualTo(256);
	}

	@Test
	public void inputStreamReadOffsetBytes() throws Exception {
		byte[] b = new byte[7];
		this.inputStream.skip(1);
		int amountRead = this.inputStream.read(b, 2, 3);
		assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 });
		assertThat(amountRead).isEqualTo(3);
	}

	@Test
	public void inputStreamReadMoreBytesThanAvailable() throws Exception {
		byte[] b = new byte[257];
		int amountRead = this.inputStream.read(b);
		assertThat(b).startsWith(BYTES);
		assertThat(amountRead).isEqualTo(256);
	}

	@Test
	public void inputStreamReadPastEnd() throws Exception {
		this.inputStream.skip(255);
		assertThat(this.inputStream.read()).isEqualTo(0xFF);
		assertThat(this.inputStream.read()).isEqualTo(-1);
		assertThat(this.inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void inputStreamReadZeroLength() throws Exception {
		byte[] b = new byte[] { 0x0F };
		int amountRead = this.inputStream.read(b, 0, 0);
		assertThat(b).isEqualTo(new byte[] { 0x0F });
		assertThat(amountRead).isEqualTo(0);
		assertThat(this.inputStream.read()).isEqualTo(0);
	}

	@Test
	public void inputStreamSkip() throws Exception {
		long amountSkipped = this.inputStream.skip(4);
		assertThat(this.inputStream.read()).isEqualTo(4);
		assertThat(amountSkipped).isEqualTo(4L);
	}

	@Test
	public void inputStreamSkipMoreThanAvailable() throws Exception {
		long amountSkipped = this.inputStream.skip(257);
		assertThat(this.inputStream.read()).isEqualTo(-1);
		assertThat(amountSkipped).isEqualTo(256L);
	}

	@Test
	public void inputStreamSkipPastEnd() throws Exception {
		this.inputStream.skip(256);
		long amountSkipped = this.inputStream.skip(1);
		assertThat(amountSkipped).isEqualTo(0L);
	}

	@Test
	public void subsectionNegativeOffset() throws Exception {
		this.thrown.expect(IndexOutOfBoundsException.class);
		this.file.getSubsection(-1, 1);
	}

	@Test
	public void subsectionNegativeLength() throws Exception {
		this.thrown.expect(IndexOutOfBoundsException.class);
		this.file.getSubsection(0, -1);
	}

	@Test
	public void subsectionZeroLength() throws Exception {
		RandomAccessData subsection = this.file.getSubsection(0, 0);
		assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
				.isEqualTo(-1);
	}

	@Test
	public void subsectionTooBig() throws Exception {
		this.file.getSubsection(0, 256);
		this.thrown.expect(IndexOutOfBoundsException.class);
		this.file.getSubsection(0, 257);
	}

	@Test
	public void subsectionTooBigWithOffset() throws Exception {
		this.file.getSubsection(1, 255);
		this.thrown.expect(IndexOutOfBoundsException.class);
		this.file.getSubsection(1, 256);
	}

	@Test
	public void subsection() throws Exception {
		RandomAccessData subsection = this.file.getSubsection(1, 1);
		assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
				.isEqualTo(1);
	}

	@Test
	public void inputStreamReadPastSubsection() throws Exception {
		RandomAccessData subsection = this.file.getSubsection(1, 2);
		InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
		assertThat(inputStream.read()).isEqualTo(1);
		assertThat(inputStream.read()).isEqualTo(2);
		assertThat(inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void inputStreamReadBytesPastSubsection() throws Exception {
		RandomAccessData subsection = this.file.getSubsection(1, 2);
		InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
		byte[] b = new byte[3];
		int amountRead = inputStream.read(b);
		assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
		assertThat(amountRead).isEqualTo(2);
	}

	@Test
	public void inputStreamSkipPastSubsection() throws Exception {
		RandomAccessData subsection = this.file.getSubsection(1, 2);
		InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
		assertThat(inputStream.skip(3)).isEqualTo(2L);
		assertThat(inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void inputStreamSkipNegative() throws Exception {
		assertThat(this.inputStream.skip(-1)).isEqualTo(0L);
	}

	@Test
	public void getFile() throws Exception {
		assertThat(this.file.getFile()).isEqualTo(this.tempFile);
	}

	@Test
	public void concurrentReads() throws Exception {
		ExecutorService executorService = Executors.newFixedThreadPool(20);
		List<Future<Boolean>> results = new ArrayList<>();
		for (int i = 0; i < 100; i++) {
			results.add(executorService.submit(new Callable<Boolean>() {

				@Override
				public Boolean call() throws Exception {
					InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
							.getSubsection(0, 256)
							.getInputStream(ResourceAccess.PER_READ);
					byte[] b = new byte[256];
					subsectionInputStream.read(b);
					return Arrays.equals(b, BYTES);
				}
			}));
		}
		for (Future<Boolean> future : results) {
			assertThat(future.get()).isTrue();
		}
	}

	@Test
	public void close() throws Exception {
		this.file.getInputStream(ResourceAccess.PER_READ).read();
		this.file.close();
		Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
		filePoolField.setAccessible(true);
		Object filePool = filePoolField.get(this.file);
		Field filesField = filePool.getClass().getDeclaredField("files");
		filesField.setAccessible(true);
		Queue<?> queue = (Queue<?>) filesField.get(filePool);
		assertThat(queue.size()).isEqualTo(0);
	}

}
