/*
 * Copyright (C) 2019, Google LLC. and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.internal.storage.dfs;

import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.Deflater;

import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRng;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
import org.junit.Test;

public class DfsPackFileTest {
	InMemoryRepository db;
	boolean bypassCache;
	boolean clearCache;

	@Before
	public void setUp() {
		db = new InMemoryRepository(new DfsRepositoryDescription("test"));
	}

	@Test
	public void testCopyPackBypassCachesSmallCached() throws IOException {
		bypassCache = true;
		clearCache = false;
		setupPack(512, 256);
		assertPackSize();
	}

	@Test
	public void testCopyPackBypassCacheSmallNoCache() throws IOException {
		bypassCache = true;
		clearCache = true;
		setupPack(512, 256);
		assertPackSize();
	}

	@Test
	public void testCopyPackBypassCacheLargeCached() throws IOException {
		bypassCache = true;
		clearCache = false;
		setupPack(512, 8000);
		assertPackSize();
	}

	@Test
	public void testCopyPackBypassCacheLargeNoCache() throws IOException {
		bypassCache = true;
		clearCache = true;
		setupPack(512, 8000);
		assertPackSize();
	}

	@Test
	public void testCopyPackThroughCacheSmallCached() throws IOException {
		bypassCache = false;
		clearCache = false;
		setupPack(512, 256);
		assertPackSize();
	}

	@Test
	public void testCopyPackThroughCacheSmallNoCache() throws IOException {
		bypassCache = false;
		clearCache = true;
		setupPack(512, 256);
		assertPackSize();
	}

	@Test
	public void testCopyPackThroughCacheLargeCached() throws IOException {
		bypassCache = false;
		clearCache = false;
		setupPack(512, 8000);
		assertPackSize();
	}

	@Test
	public void testCopyPackThroughCacheLargeNoCache() throws IOException {
		bypassCache = false;
		clearCache = true;
		setupPack(512, 8000);
		assertPackSize();
	}

	@Test
	public void testLoadObjectSizeIndex() throws IOException {
		bypassCache = false;
		clearCache = true;
		setObjectSizeIndexMinBytes(0);
		ObjectId blobId = setupPack(512, 800);

		DfsReader reader = db.getObjectDatabase().newReader();
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		assertTrue(pack.hasObjectSizeIndex(reader));
		assertEquals(800, pack.getIndexedObjectSize(reader, blobId));
	}

	@Test
	public void testLoadObjectSizeIndex_noIndex() throws IOException {
		bypassCache = false;
		clearCache = true;
		setObjectSizeIndexMinBytes(-1);
		setupPack(512, 800);

		DfsReader reader = db.getObjectDatabase().newReader();
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		assertFalse(pack.hasObjectSizeIndex(reader));
	}

	private static class TestPackLoadListener implements PackLoadListener {
		final Map<PackExt, Integer> indexLoadCount = new HashMap<>();

		int blockLoadCount;

		@SuppressWarnings("boxing")
		@Override
		public void onIndexLoad(String packName, PackSource src, PackExt ext,
				long size, Object loadedIdx) {
			indexLoadCount.merge(ext, 1, Integer::sum);
		}

		@Override
		public void onBlockLoad(String packName, PackSource src, PackExt ext, long position,
				DfsBlockData dfsBlockData) {
			blockLoadCount += 1;
		}
	}

	@Test
	public void testIndexLoadCallback_indexNotInCache() throws IOException {
		bypassCache = false;
		clearCache = true;
		setObjectSizeIndexMinBytes(-1);
		setupPack(512, 800);

		TestPackLoadListener tal = new TestPackLoadListener();
		DfsReader reader = db.getObjectDatabase().newReader();
		reader.addPackLoadListener(tal);
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		pack.getPackIndex(reader);

		assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
	}

	@Test
	public void testIndexLoadCallback_indexInCache() throws IOException {
		bypassCache = false;
		clearCache = false;
		setObjectSizeIndexMinBytes(-1);
		setupPack(512, 800);

		TestPackLoadListener tal = new TestPackLoadListener();
		DfsReader reader = db.getObjectDatabase().newReader();
		reader.addPackLoadListener(tal);
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		pack.getPackIndex(reader);
		pack.getPackIndex(reader);
		pack.getPackIndex(reader);

		assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
	}

	@Test
	public void testIndexLoadCallback_multipleReads() throws IOException {
		bypassCache = false;
		clearCache = true;
		setObjectSizeIndexMinBytes(-1);
		setupPack(512, 800);

		TestPackLoadListener tal = new TestPackLoadListener();
		DfsReader reader = db.getObjectDatabase().newReader();
		reader.addPackLoadListener(tal);
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		pack.getPackIndex(reader);
		pack.getPackIndex(reader);
		pack.getPackIndex(reader);

		assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
	}


	@Test
	public void testBlockLoadCallback_loadInCache() throws IOException {
		bypassCache = false;
		clearCache = true;
		setObjectSizeIndexMinBytes(-1);
		setupPack(512, 800);

		TestPackLoadListener tal = new TestPackLoadListener();
		DfsReader reader = db.getObjectDatabase().newReader();
		reader.addPackLoadListener(tal);
		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
		ObjectId anObject = pack.getPackIndex(reader).getObjectId(0);
		pack.get(reader, anObject).getBytes();
		assertEquals(2, tal.blockLoadCount);
	}

	private ObjectId setupPack(int bs, int ps) throws IOException {
		DfsBlockCacheConfig cfg = new DfsBlockCacheConfig().setBlockSize(bs)
				.setBlockLimit(bs * 100).setStreamRatio(bypassCache ? 0F : 1F);
		DfsBlockCache.reconfigure(cfg);

		byte[] data = new TestRng(JGitTestUtil.getName()).nextBytes(ps);
		DfsInserter ins = (DfsInserter) db.newObjectInserter();
		ins.setCompressionLevel(Deflater.NO_COMPRESSION);
		ObjectId blobId = ins.insert(Constants.OBJ_BLOB, data);
		ins.flush();

		if (clearCache) {
			DfsBlockCache.reconfigure(cfg);
			db.getObjectDatabase().clearCache();
		}
		return blobId;
	}

	private void assertPackSize() throws IOException {
		try (DfsReader ctx = db.getObjectDatabase().newReader();
				PackWriter pw = new PackWriter(ctx);
				ByteArrayOutputStream os = new ByteArrayOutputStream();
				PackOutputStream out = new PackOutputStream(
						NullProgressMonitor.INSTANCE, os, pw)) {
			DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
			long packSize = pack.getPackDescription().getFileSize(PackExt.PACK);
			pack.copyPackAsIs(out, ctx);
			assertEquals(packSize - (12 + 20), os.size());
		}
	}

	private void setObjectSizeIndexMinBytes(int threshold) {
		db.getConfig().setInt(CONFIG_PACK_SECTION, null,
				CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, threshold);
	}
}
