/*
 * Copyright (C) 2023, 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.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRng;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.junit.Before;
import org.junit.Test;

public class DfsReaderTest {
	InMemoryRepository db;

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

	@Test
	public void isNotLargerThan_objAboveThreshold()
			throws IOException {
		setObjectSizeIndexMinBytes(100);
		ObjectId obj = insertBlobWithSize(200);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			assertFalse("limit < threshold < obj",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
			assertEquals(1, ctx.stats.isNotLargerThanCallCount);
			assertEquals(1, ctx.stats.objectSizeIndexHit);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);

			assertFalse("limit = threshold < obj",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
			assertEquals(2, ctx.stats.isNotLargerThanCallCount);
			assertEquals(2, ctx.stats.objectSizeIndexHit);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);

			assertFalse("threshold < limit < obj",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 150));
			assertEquals(3, ctx.stats.isNotLargerThanCallCount);
			assertEquals(3, ctx.stats.objectSizeIndexHit);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);

			assertTrue("threshold < limit = obj",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 200));
			assertEquals(4, ctx.stats.isNotLargerThanCallCount);
			assertEquals(4, ctx.stats.objectSizeIndexHit);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);

			assertTrue("threshold < obj < limit",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 250));
			assertEquals(5, ctx.stats.isNotLargerThanCallCount);
			assertEquals(5, ctx.stats.objectSizeIndexHit);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);
		}
	}


	@Test
	public void isNotLargerThan_objBelowThreshold()
			throws IOException {
		setObjectSizeIndexMinBytes(100);
		insertBlobWithSize(1000); // index not empty
		ObjectId obj = insertBlobWithSize(50);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			assertFalse("limit < obj < threshold",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
			assertEquals(1, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
			assertEquals(1, ctx.stats.objectSizeIndexMiss);

			assertTrue("limit = obj < threshold",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
			assertEquals(2, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
			assertEquals(2, ctx.stats.objectSizeIndexMiss);

			assertTrue("obj < limit < threshold",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 80));
			assertEquals(3, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
			assertEquals(3, ctx.stats.objectSizeIndexMiss);

			assertTrue("obj < limit = threshold",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
			assertEquals(4, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
			assertEquals(4, ctx.stats.objectSizeIndexMiss);

			assertTrue("obj < threshold < limit",
					ctx.isNotLargerThan(obj, OBJ_BLOB, 120));
			assertEquals(5, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
			assertEquals(5, ctx.stats.objectSizeIndexMiss);
		}
	}

	@Test
	public void isNotLargerThan_emptyIdx() throws IOException {
		setObjectSizeIndexMinBytes(100);
		ObjectId obj = insertBlobWithSize(10);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100));

			assertEquals(5, ctx.stats.isNotLargerThanCallCount);
			assertEquals(5, ctx.stats.objectSizeIndexMiss);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
		}
	}

	@Test
	public void isNotLargerThan_noObjectSizeIndex() throws IOException {
		setObjectSizeIndexMinBytes(-1);
		ObjectId obj = insertBlobWithSize(10);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
			assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100));

			assertEquals(5, ctx.stats.isNotLargerThanCallCount);
			assertEquals(0, ctx.stats.objectSizeIndexMiss);
			assertEquals(0, ctx.stats.objectSizeIndexHit);
		}
	}

	@Test
	public void packLoadListener_noInvocations() throws IOException {
		insertBlobWithSize(100);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			CounterPackLoadListener listener = new CounterPackLoadListener();
			ctx.addPackLoadListener(listener);
			assertEquals(null, listener.callsPerExt.get(PackExt.INDEX));
		}
	}

	@Test
	public void packLoadListener_has_openIdx() throws IOException {
		ObjectId obj = insertBlobWithSize(100);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			CounterPackLoadListener listener = new CounterPackLoadListener();
			ctx.addPackLoadListener(listener);
			boolean has = ctx.has(obj);
			assertTrue(has);
			assertEquals(Integer.valueOf(1), listener.callsPerExt.get(PackExt.INDEX));
		}
	}

	@Test
	public void packLoadListener_notLargerThan_openMultipleIndices() throws IOException {
			setObjectSizeIndexMinBytes(100);
			ObjectId obj = insertBlobWithSize(200);
			try (DfsReader ctx = db.getObjectDatabase().newReader()) {
				CounterPackLoadListener listener = new CounterPackLoadListener();
				ctx.addPackLoadListener(listener);
				boolean notLargerThan = ctx.isNotLargerThan(obj, OBJ_BLOB, 1000);
				assertTrue(notLargerThan);
				assertEquals(Integer.valueOf(1), listener.callsPerExt.get(PackExt.INDEX));
				assertEquals(Integer.valueOf(1), listener.callsPerExt.get(PackExt.OBJECT_SIZE_INDEX));
			}
	}

	@Test
	public void packLoadListener_has_openMultipleIndices() throws IOException {
		setObjectSizeIndexMinBytes(100);
		insertBlobWithSize(200);
		insertBlobWithSize(230);
		insertBlobWithSize(100);
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			CounterPackLoadListener listener = new CounterPackLoadListener();
			ctx.addPackLoadListener(listener);
			ObjectId oid = ObjectId.fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129");
			boolean has = ctx.has(oid);
			assertFalse(has);
			// Open 3 indices trying to find the pack
			assertEquals(Integer.valueOf(3), listener.callsPerExt.get(PackExt.INDEX));
		}
	}


	@Test
	public void packLoadListener_has_repeatedCalls_openMultipleIndices() throws IOException {
		// Two objects NOT in the repo
		ObjectId oid = ObjectId.fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129");
		ObjectId oid2 = ObjectId.fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f130");

		setObjectSizeIndexMinBytes(100);
		insertBlobWithSize(200);
		insertBlobWithSize(230);
		insertBlobWithSize(100);
		CounterPackLoadListener listener = new CounterPackLoadListener();
		try (DfsReader ctx = db.getObjectDatabase().newReader()) {
			ctx.addPackLoadListener(listener);
			boolean has = ctx.has(oid);
			ctx.has(oid);
			ctx.has(oid2);
			assertFalse(has);
			// The 3 indices were loaded only once each
			assertEquals(Integer.valueOf(3), listener.callsPerExt.get(PackExt.INDEX));
		}
	}

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

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

		@Override
		public void onBlockLoad(String packName, PackSource src, PackExt ext,
				long size, DfsBlockData dfsBlockData) {
			// empty
		}
	}

	private ObjectId insertBlobWithSize(int size)
			throws IOException {
		TestRng testRng = new TestRng(JGitTestUtil.getName());
		ObjectId oid;
		try (ObjectInserter ins = db.newObjectInserter()) {
				oid = ins.insert(OBJ_BLOB,
						testRng.nextBytes(size));
			ins.flush();
		}
		return oid;
	}

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