package rocks.inspectit.server.instrumentation.classcache;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import rocks.inspectit.server.instrumentation.config.ClassCacheSearchNarrower;
import rocks.inspectit.server.instrumentation.config.applier.IInstrumentationApplier;
import rocks.inspectit.shared.all.instrumentation.classcache.AnnotationType;
import rocks.inspectit.shared.all.instrumentation.classcache.ClassType;
import rocks.inspectit.shared.all.instrumentation.classcache.ImmutableClassType;
import rocks.inspectit.shared.all.instrumentation.classcache.InterfaceType;
import rocks.inspectit.shared.all.instrumentation.classcache.MethodType;
import rocks.inspectit.shared.all.instrumentation.classcache.Type;
import rocks.inspectit.shared.all.instrumentation.config.impl.AgentConfig;
import rocks.inspectit.shared.all.instrumentation.config.impl.InstrumentationDefinition;
import rocks.inspectit.shared.all.instrumentation.config.impl.MethodInstrumentationConfig;
import rocks.inspectit.shared.all.testbase.TestBase;
import rocks.inspectit.shared.cs.ci.assignment.AbstractClassSensorAssignment;
@SuppressWarnings({ "all", "unchecked" })
public class ClassCacheInstrumentationTest extends TestBase {
	private static final String FQN = "FQN";
	@InjectMocks
	ClassCacheInstrumentation instrumentation;
	@Mock
	Logger log;
	@Mock
	ClassCache classCache;
	@Mock
	ClassCacheLookup lookup;
	@Mock
	AgentConfig agentConfiguration;
	@Mock
	ClassType classType;
	@Mock
	IInstrumentationApplier instrumentationApplier;
	@Mock
	ClassCacheSearchNarrower searchNarrower;
	@Mock
	AbstractClassSensorAssignment<?> assignment;
	@BeforeMethod
	public void setup() throws Exception {
		when(classCache.getLookupService()).thenReturn(lookup);
		Answer<Object> callableAnswer = new Answer<Object>() {
			@Override
			public Object answer(InvocationOnMock invocation) throws Throwable {
				Callable<?> callable = (Callable<?>) invocation.getArguments()[0];
				return callable.call();
			}
		};
		doAnswer(callableAnswer).when(classCache).executeWithReadLock(Matchers.<Callable<?>> anyObject());
		doAnswer(callableAnswer).when(classCache).executeWithWriteLock(Matchers.<Callable<?>> anyObject());
		instrumentation.init(classCache);
	}
	public static class AddAndGetInstrumentationResult extends ClassCacheInstrumentationTest {
		@Test
		public void notInitialized() {
			when(classType.isInitialized()).thenReturn(false);
			InstrumentationDefinition result = instrumentation.addAndGetInstrumentationResult(classType, agentConfiguration, Collections.singleton(instrumentationApplier));
			assertThat(result, is(nullValue()));
		}
		@Test
		public void notInstrumented() {
			when(classType.isInitialized()).thenReturn(true);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(false);
			InstrumentationDefinition result = instrumentation.addAndGetInstrumentationResult(classType, agentConfiguration, Collections.singleton(instrumentationApplier));
			assertThat(result, is(nullValue()));
		}
		@Test
		public void instrumented() {
			Collection<MethodInstrumentationConfig> configs = mock(Collection.class);
			when(classType.isInitialized()).thenReturn(true);
			when(classType.getFQN()).thenReturn(FQN);
			when(classType.hasInstrumentationPoints()).thenReturn(true);
			when(classType.getInstrumentationPoints()).thenReturn(configs);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(true);
			InstrumentationDefinition result = instrumentation.addAndGetInstrumentationResult(classType, agentConfiguration, Collections.singleton(instrumentationApplier));
			assertThat(result, is(notNullValue()));
			assertThat(result.getClassName(), is(FQN));
			assertThat(result.getMethodInstrumentationConfigs(), is(configs));
		}
	}
	public static class RemoveInstrumentationPoints extends ClassCacheInstrumentationTest {
		@Test
		public void removeAll() throws Exception {
			MethodType methodType = mock(MethodType.class);
			when(classType.isClass()).thenReturn(true);
			when(classType.castToClass()).thenReturn(classType);
			when(classType.isInitialized()).thenReturn(true);
			when(classType.hasInstrumentationPoints()).thenReturn(true);
			when(classType.getMethods()).thenReturn(Collections.singleton(methodType));
			doReturn(Collections.singleton(classType)).when(lookup).findAll();
			instrumentation.removeInstrumentationPoints();
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(methodType, times(1)).setMethodInstrumentationConfig(null);
		}
		@Test
		public void removeNothingWhenEmpty() throws Exception {
			doReturn(Collections.emptyList()).when(lookup).findAll();
			instrumentation.removeInstrumentationPoints();
			// not touching the write lock
			verify(classCache, times(0)).executeWithWriteLock(Matchers.<Callable<?>> any());
		}
		@Test
		public void removeNothingForAnnotationTypes() throws Exception {
			AnnotationType annotationType = new AnnotationType("");
			instrumentation.removeInstrumentationPoints(Collections.singleton(annotationType), Collections.singleton(instrumentationApplier));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verifyZeroInteractions(instrumentationApplier);
		}
		@Test
		public void removeNothingForInterfaceTypes() throws Exception {
			InterfaceType interfaceType = new InterfaceType("");
			instrumentation.removeInstrumentationPoints(Collections.singleton(interfaceType), Collections.singleton(instrumentationApplier));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verifyZeroInteractions(instrumentationApplier);
		}
	}
	public static class AddInstrumentationPoints extends ClassCacheInstrumentationTest {
		@Test
		public void add() throws Exception {
			when(classType.isClass()).thenReturn(true);
			when(classType.castToClass()).thenReturn(classType);
			when(classType.isInitialized()).thenReturn(true);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(true);
			doReturn(Collections.singleton(classType)).when(lookup).findAll();
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat((Collection<ClassType>) result, hasItem(classType));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).addInstrumentationPoints(agentConfiguration, classType);
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void searchNarrowAdd() throws Exception {
			when(classType.isClass()).thenReturn(true);
			when(classType.castToClass()).thenReturn(classType);
			when(classType.isInitialized()).thenReturn(true);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(true);
			doReturn(assignment).when(instrumentationApplier).getSensorAssignment();
			doReturn(Collections.singleton(classType)).when(searchNarrower).narrowByClassSensorAssignment(classCache, assignment);
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat((Collection<ClassType>) result, hasItem(classType));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).addInstrumentationPoints(agentConfiguration, classType);
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void addNothingWhenInstrumenterDoesNotAdd() throws Exception {
			when(classType.isClass()).thenReturn(true);
			when(classType.castToClass()).thenReturn(classType);
			when(classType.isInitialized()).thenReturn(true);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(false);
			doReturn(Collections.singleton(classType)).when(lookup).findAll();
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat((Collection<ClassType>) result, is(empty()));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).addInstrumentationPoints(agentConfiguration, classType);
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void searchNarrowAddNothingWhenInstrumenterDoesNotAdd() throws Exception {
			when(classType.isClass()).thenReturn(true);
			when(classType.castToClass()).thenReturn(classType);
			when(classType.isInitialized()).thenReturn(true);
			when(instrumentationApplier.addInstrumentationPoints(agentConfiguration, classType)).thenReturn(false);
			doReturn(assignment).when(instrumentationApplier).getSensorAssignment();
			doReturn(Collections.singleton(classType)).when(searchNarrower).narrowByClassSensorAssignment(classCache, assignment);
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat((Collection<ClassType>) result, is(empty()));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).addInstrumentationPoints(agentConfiguration, classType);
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void addNothingForNonInitializedType() throws Exception {
			when(classType.isInitialized()).thenReturn(false);
			doReturn(Collections.singleton(classType)).when(lookup).findAll();
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat(result, is(empty()));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void searchNarrowAddNothingForNonInitializedType() throws Exception {
			when(classType.isInitialized()).thenReturn(false);
			doReturn(assignment).when(instrumentationApplier).getSensorAssignment();
			doReturn(Collections.singleton(classType)).when(searchNarrower).narrowByClassSensorAssignment(classCache, assignment);
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat(result, is(empty()));
			// must be write lock
			verify(classCache, times(1)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void addNothingWhenEmpty() throws Exception {
			doReturn(Collections.emptyList()).when(lookup).findAll();
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat(result, is(empty()));
			// not touching the write lock
			verify(classCache, times(0)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void searchNarrowAddNothingWhenEmpty() throws Exception {
			doReturn(assignment).when(instrumentationApplier).getSensorAssignment();
			doReturn(Collections.emptyList()).when(searchNarrower).narrowByClassSensorAssignment(classCache, assignment);
			Collection<? extends ImmutableClassType> result = instrumentation.addInstrumentationPoints(agentConfiguration, Collections.singleton(instrumentationApplier));
			// assert result
			assertThat(result, is(empty()));
			// not touching the write lock
			verify(classCache, times(0)).executeWithWriteLock(Matchers.<Callable<?>> any());
			verify(instrumentationApplier, times(1)).getSensorAssignment();
			verifyNoMoreInteractions(instrumentationApplier);
		}
		@Test
		public void addNothingForNonClassTypes() throws Exception {
			AnnotationType annotationType = new AnnotationType("");


Answer: 			InterfaceType interfaceType = new InterfaceType("");