/*******************************************************************************
 * Copyright (c) 2004, 2011 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.tests.services.datalocation;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.eclipse.core.runtime.Platform;
import org.eclipse.osgi.storagemanager.ManagedOutputStream;
import org.eclipse.osgi.storagemanager.StorageManager;
import org.eclipse.osgi.tests.OSGiTest;

public class StreamManagerTests extends OSGiTest {
	StorageManager manager1;
	StorageManager manager2;
	File base;
	String reliableFile;

	/**
	 * Constructs a test case with the given name.
	 */
	public StreamManagerTests(String name) {
		super(name);
	}

	public static Test suite() {
		return new TestSuite(StreamManagerTests.class);
	}

	protected void setUp() throws Exception {
		super.setUp();
		base = new File(Platform.getConfigurationLocation().getURL().getPath(), "StreamManagerTests");
		manager1 = null;
		manager2 = null;
		reliableFile = System.getProperty("osgi.useReliableFiles");
	}

	protected void tearDown() throws Exception {
		super.tearDown();
		if (manager1 != null)
			manager1.close();
		if (manager2 != null)
			manager2.close();
		rm(base);
		if (reliableFile == null)
			System.getProperties().remove("osgi.useReliableFiles");
		else
			System.setProperty("osgi.useReliableFiles", reliableFile);
	}

	private void rm(File folder) {
		if (folder.isDirectory()) {
			File[] files = folder.listFiles();
			if (files != null) {
				for (File file : files) {
					rm(file);
				}
			}
		}
		folder.delete();
	}

	private String getInputStreamContents(InputStream is) throws IOException {
		StringBuilder sb = new StringBuilder();
		byte[] data = new byte[64];
		int len;
		try {
			while ((len = is.read(data)) != -1) {
				sb.append(new String(data, 0, len));
			}
		} finally {
			is.close();
		}
		return sb.toString();
	}

	void writeToFile(File file, String str) throws IOException {
		FileOutputStream fos = new FileOutputStream(file);
		try {
			fos.write(str.getBytes());
			fos.flush();
			fos.getFD().sync();
		} finally {
			fos.close();
		}
	}

	/**
	 * This tests that FM will keep a backup version of a reliableFile and that
	 * corrupting the reliableFile will recover the previous contents.
	 *
	 */
	public void testReliableFile() {
		String fileName = "testReliableFile.txt";
		File file1 = new File(base, fileName + ".1");
		File file2 = new File(base, fileName + ".2");
		File file3 = new File(base, fileName + ".3");
		String contents1 = "test reliable file cOntents #1";
		String contents2 = "test reliable file cOntents #2";
		try {
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager1 = new StorageManager(base, null);
			manager1.open(true);
			ManagedOutputStream fmos = manager1.getOutputStream(fileName);
			assertNotNull(fmos);
			fmos.write(contents1.getBytes());
			fmos.close();

			// Write verion2 of the file
			fmos = manager1.getOutputStream(fileName);
			assertNotNull(fmos);
			fmos.write(contents2.getBytes());
			fmos.close();
			assertTrue(file1.exists());
			assertTrue(file2.exists());
			assertTrue(!file3.exists());
			manager1.close();
			manager1 = null;

			//now, open new manager, verify file contents are #2
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager2 = new StorageManager(base, null);
			manager2.open(true);
			InputStream is = manager2.getInputStream(fileName);
			assertNotNull(is);
			assertEquals(contents2, getInputStreamContents(is));
			manager2.close();
			manager2 = null;

			// need to sleep, FAT32 doesn't have too fine granularity in timestamps
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {/*ignore*/
			}
			//now, corrupt version 2 of the file
			RandomAccessFile raf = new RandomAccessFile(file2, "rw");
			raf.seek(20);
			raf.write('0'); // change 'O' to '0'
			raf.close();

			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager1 = new StorageManager(base, null);
			manager1.open(true);

			//request any valid stream available
			is = manager1.getInputStream(fileName);
			assertNotNull(is);
			assertEquals(contents1, getInputStreamContents(is));

			//now request only the primary file
			try {
				InputStream[] isSet = manager1.getInputStreamSet(new String[] {fileName});
				for (InputStream set : isSet) {
					if (set != null) {
						set.close();
					}
				}
				fail("getInputStreamSet was successful");
			} catch (IOException e) {
				//good
			}

			//now, corrupt version 1 of the file
			raf = new RandomAccessFile(file1, "rw");
			raf.seek(20);
			raf.write('0'); // change 'O' to '0'
			raf.close();

			// get input stream should fail
			try {
				is = manager1.getInputStream(fileName);
				fail("get input stream succedded");
			} catch (IOException e) {
				//good
			}
			manager1.close();
			manager1 = null;
		} catch (IOException e) {
			fail("unexepected exception", e);
		} finally {
			System.setProperty("osgi.useReliableFiles", "false"); // force reliable files off
		}
	}

	public void testBigReliableFile() {
		String fileName = getName() + ".txt";
		try {
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager1 = new StorageManager(base, null);
			manager1.open(true);
			try (ManagedOutputStream fmos = manager1.getOutputStream(fileName)) {
				assertNotNull(fmos);
				try (DataOutputStream bufferedOut = new DataOutputStream(new BufferedOutputStream(fmos))) {
					// 200 K of integers (200 * 1024 / 4)
					for (int i = 0; i < (200 * 1024 / 4); i++)
						bufferedOut.writeInt(i);
				}
			}
			manager1.close();
			manager1 = null;

			//now, open new manager, verify file contents are there
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager2 = new StorageManager(base, null);
			manager2.open(true);
			try (InputStream is = manager2.getInputStream(fileName)) {
				assertNotNull(is);
				try (DataInputStream bufferedIn = new DataInputStream(new BufferedInputStream(is))) {
					for (int i = 0; i < (200 * 1024 / 4); i++)
						assertEquals("Wrong content found", i, bufferedIn.readInt());
				}
			}
			manager2.close();
			manager2 = null;

		} catch (IOException e) {
			fail("unexepected exception", e);
		}
	}

	/**
	 * This tests if migration from a prior (non-ReliableFile) .fileTable
	 * to the current .fileTable is correct.
	 *
	 */
	public void testMigration() {
		File testDir = new File(base, "testMigrationManager");
		File managerDir = new File(testDir, ".manager");
		String fileName = "testMigration.txt";
		File file2 = new File(testDir, fileName + ".2");
		File file5 = new File(testDir, fileName + ".5");
		File fileTable = new File(managerDir, ".fileTable");
		File fileTable1 = new File(managerDir, ".fileTable.1");
		File fileTable2 = new File(managerDir, ".fileTable.2");
		File fileTable3 = new File(managerDir, ".fileTable.3");
		String contents1 = "test reliable file contents #1";
		String contents2 = "test reliable file contents #2";
		String contents3 = "test reliable file contents #3";
		try {
			// create a .fileTable and a normal file
			managerDir.mkdirs();
			writeToFile(fileTable, "#safe table\n" + fileName + "=2\n");
			writeToFile(file2, contents1);
			manager1 = new StorageManager(testDir, null);
			manager1.open(true);
			File test = manager1.lookup(fileName, false);
			assertNotNull(test);
			assertTrue(test.exists());

			// update a new file
			File testFile = manager1.createTempFile(fileName);
			writeToFile(testFile, contents2);
			manager1.update(new String[] {fileName}, new String[] {testFile.getName()});
			// again
			testFile = manager1.createTempFile(fileName);
			writeToFile(testFile, contents3);
			manager1.update(new String[] {fileName}, new String[] {testFile.getName()});
			// again
			testFile = manager1.createTempFile(fileName);
			writeToFile(testFile, contents1);
			manager1.update(new String[] {fileName}, new String[] {testFile.getName()});
			manager1.close();
			manager1 = null;

			String[] files = managerDir.list();
			assertEquals(4, files.length);
			//original file never gets deleted
			assertTrue(fileTable.exists());
			//behaves like a reliableFile?
			assertFalse(fileTable1.exists());
			assertTrue(fileTable2.exists());
			assertTrue(fileTable3.exists());
			//files are as expected?
			files = testDir.list();
			assertEquals(2, files.length);
			assertTrue(file5.exists());

			manager2 = new StorageManager(testDir, null);
			manager2.open(true);
			testFile = manager2.lookup(fileName, false);
			assertNotNull(testFile);
			assertTrue(testFile.exists());
			assertTrue(testFile.getName().endsWith(".5"));
			manager2.close();
			manager2 = null;

		} catch (IOException e) {
			fail("unexepected exception", e);
		}
	}

	/**
	 * This tests that an output stream abort behave as expected.
	 *
	 */
	public void testAbort() {
		testAbort(true);
		testAbort(false);
	}

	private void testAbort(boolean reliable) {
		String fileName;
		if (reliable)
			fileName = "abortFileReliable.txt";
		else
			fileName = "abortFileStd.txt";
		File file1 = new File(base, fileName + ".1");
		File file2 = new File(base, fileName + ".2");
		File file3 = new File(base, fileName + ".3");
		String contents1 = "test reliable file contents #1";
		String contents2 = "test reliable file contents #2";
		try {
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager1 = new StorageManager(base, null);
			manager1.open(true);
			//create version 1
			ManagedOutputStream smos = manager1.getOutputStream(fileName);
			smos.write(contents1.getBytes());
			smos.close();

			//start creating version 2
			smos = manager1.getOutputStream(fileName);
			smos.write(contents2.getBytes());
			smos.abort();
			smos.close(); // shouldn't cause exception, check!

			// now see if we're still on version #1
			assertEquals(1, manager1.getId(fileName));
			// check contents also
			InputStream is = manager1.getInputStream(fileName);
			assertNotNull(is);
			assertEquals(contents1, getInputStreamContents(is));
			manager1.close();
			manager1 = null;

			// open a new manager & check the same thing to ensure the database is correct
			System.setProperty("osgi.useReliableFiles", "true"); // force reliable files
			manager2 = new StorageManager(base, null);
			manager2.open(true);
			//create version 1
			// now see if we're still on version #1
			assertEquals(1, manager2.getId(fileName));
			// check contents also
			is = manager2.getInputStream(fileName);
			assertNotNull(is);
			assertEquals(contents1, getInputStreamContents(is));
			manager2.close();
			manager2 = null;
			assertTrue(file1.exists());
			assertFalse(file2.exists());
			assertFalse(file3.exists());
		} catch (IOException e) {
			fail("unexepected exception", e);
		}
	}

	/**
	 * This tests if getting an output stream-set work properly.
	 *
	 */
	public void testGetOutputStreamSet() {
		testGetOutputStreamSet(true);
		testGetOutputStreamSet(false);
	}

	private void testGetOutputStreamSet(boolean reliable) {
		File mgrDir;
		if (reliable)
			mgrDir = new File(base, "getSetReliable");
		else
			mgrDir = new File(base, "getSetStd");
		String fileName1 = "testSet1.txt";
		String fileName2 = "testSet2.txt";
		File file1_1 = new File(mgrDir, fileName1 + ".1");
		File file1_2 = new File(mgrDir, fileName1 + ".2");
		File file2_1 = new File(mgrDir, fileName2 + ".1");
		File file2_2 = new File(mgrDir, fileName2 + ".2");
		String contents1 = "test reliable file contents #1";
		String contents2 = "test reliable file contents #2";
		try {
			System.setProperty("osgi.useReliableFiles", reliable ? "true" : "false"); // force reliable files
			manager1 = new StorageManager(mgrDir, null);
			manager1.open(true);
			ManagedOutputStream[] outs = manager1.getOutputStreamSet(new String[] {fileName1, fileName2});
			assertNotNull(outs);
			assertEquals(2, outs.length);

			outs[0].write(contents1.getBytes());
			outs[1].write(contents2.getBytes());
			outs[1].close();
			assertFalse(file1_1.exists());
			assertFalse(file2_1.exists());
			outs[0].close();
			assertTrue(file1_1.exists());
			assertTrue(file2_1.exists());

			outs = manager1.getOutputStreamSet(new String[] {fileName1, fileName2});
			assertNotNull(outs);

			outs[0].write("new data #1".getBytes());
			outs[1].write("new data #2".getBytes());
			outs[0].close();
			assertFalse(file1_2.exists());
			assertFalse(file2_2.exists());
			outs[1].close();
			assertTrue(file1_2.exists());
			assertTrue(file2_2.exists());
			manager1.close();
			manager1 = null;

			if (reliable) {
				// verify FM thinks they are reliable
				assertTrue(file1_1.exists());
				assertTrue(file2_1.exists());
			} else {
				// verify FM thinks they are not reliable
				assertFalse(file1_1.exists());
				assertFalse(file2_1.exists());
			}

		} catch (IOException e) {
			fail("unexepected exception", e);
		}
	}

	/**
	 * This tests if aborting a managed stream-set works as expected
	 *
	 */
	public void testAbortStreamSet() {
		testAbortSet(true);
		testAbortSet(false);
	}

	private void testAbortSet(boolean reliable) {
		File mgrDir;
		if (reliable)
			mgrDir = new File(base, "abortSetReliable");
		else
			mgrDir = new File(base, "abortSetStd");
		String fileName1 = "test1.txt";
		String fileName2 = "test2.txt";
		String fileName3 = "test3.txt";
		String fileName4 = "test4.txt";
		String contents1 = "test reliable file contents #1";
		String contents2 = "test reliable file contents #2";
		try {
			mgrDir.mkdirs();
			String[] list = mgrDir.list();
			assertEquals(0, list.length);
			System.setProperty("osgi.useReliableFiles", reliable ? "true" : "false"); // force reliable files
			manager1 = new StorageManager(mgrDir, null);
			manager1.open(true);
			ManagedOutputStream[] outs = manager1.getOutputStreamSet(new String[] {fileName1, fileName2, fileName3, fileName4});
			assertNotNull(outs);

			outs[0].write(contents1.getBytes());
			outs[1].write(contents2.getBytes());
			outs[2].write(contents2.getBytes());
			outs[3].write(contents1.getBytes());
			//sanity check
			list = mgrDir.list();
			assertEquals(5, list.length);
			outs[2].close();
			outs[1].abort();
			outs[0].close(); //noop
			outs[3].close(); //noop
			outs[2].close(); //noop
			outs[1].close(); //noop
			list = mgrDir.list();
			assertEquals(1, list.length);
			assertNull(manager1.lookup(fileName1, false));
			assertNull(manager1.lookup(fileName2, false));
			assertNull(manager1.lookup(fileName3, false));
			assertNull(manager1.lookup(fileName4, false));
			manager1.close();
			manager1 = null;

			// open a new manager & check the same thing to ensure the database is correct
			System.setProperty("osgi.useReliableFiles", reliable ? "true" : "false"); // force reliable files
			manager2 = new StorageManager(mgrDir, null);
			manager2.open(true);
			//create version 1
			assertNull(manager2.lookup(fileName1, false));
			assertNull(manager2.lookup(fileName2, false));
			assertNull(manager2.lookup(fileName3, false));
			assertNull(manager2.lookup(fileName4, false));
			list = mgrDir.list();
			assertEquals(1, list.length);
			manager2.close();
			manager2 = null;

		} catch (IOException e) {
			fail("unexepected exception", e);
		}
	}

}
