package de.is24.deadcode4j.analyzer;

import de.is24.deadcode4j.AnalysisContextBuilder;
import de.is24.deadcode4j.IntermediateResult;
import org.codehaus.plexus.util.ReflectionUtils;
import org.hamcrest.Matcher;
import org.hamcrest.collection.IsArrayContaining;
import org.junit.Test;
import org.mockito.internal.matchers.VarargMatcher;
import org.slf4j.Logger;

import java.util.Map;

import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static de.is24.deadcode4j.AnalysisContextBuilder.givenAnalysisContext;
import static de.is24.deadcode4j.IntermediateResultMapBuilder.givenIntermediateResultMap;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public final class A_HibernateAnnotationsAnalyzer extends AByteCodeAnalyzer<HibernateAnnotationsAnalyzer> {

    private static <T> Matcher<T[]> hasVarArgItem(Matcher<? super T> elementMatcher) {
        return new MyVarArgsMatcher<T>(elementMatcher);
    }

    @Override
    protected HibernateAnnotationsAnalyzer createAnalyzer() {
        return new HibernateAnnotationsAnalyzer();
    }

    @Test
    public void recognizesDependencyFromClassWithTypeAnnotatedFieldToTypeDefAnnotatedClass() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassWithTypeDef.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassUsingTypeAtField.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.ClassUsingTypeAtField",
                "de.is24.deadcode4j.analyzer.hibernateannotations.ClassWithTypeDef");
    }

    @Test
    public void recognizesDependencyFromClassWithTypeAnnotatedMethodToTypeDefAnnotatedClass() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassWithTypeDef.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassUsingTypeAtMethod.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.ClassUsingTypeAtMethod",
                "de.is24.deadcode4j.analyzer.hibernateannotations.ClassWithTypeDef");
    }

    @Test
    public void recognizesDependencyFromTypeAnnotatedClassesToTypeDefsAnnotatedPackage() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/Entity.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/AnotherEntity.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.Entity",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.AnotherEntity",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void recognizesDependencyFromClassWithTypeAnnotatedMethodToReferencedClass() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassUsingTypeWithoutTypeDef.class");
        analyzeFile("IndependentClass.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.ClassUsingTypeWithoutTypeDef",
                "IndependentClass");
    }

    @Test
    public void reportsDependencyToDefinedStrategyIfStrategyIsPartOfTheAnalysis() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/knownStrategies/ClassDefiningGenericGenerator.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.knownStrategies.ClassDefiningGenericGenerator",
                "IndependentClass");
    }

    @Test
    public void doesNotReportDependencyToDefinedStrategyIfStrategyIsNoPartOfTheAnalysis() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassDefiningGenericGenerator.class");

        assertThatNoDependenciesAreReported();
    }

    @Test
    public void reportsDependencyFromPackageToDefinedStrategies() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/knownStrategies/package-info.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.knownStrategies.package-info",
                "IndependentClass", "DependingClass");
    }

    @Test
    public void recognizesDependencyFromClassWithGeneratedValueAnnotatedFieldToGenericGeneratorAnnotatedClass() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassDefiningGenericGenerator.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassUsingGeneratedValueAtField.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.ClassUsingGeneratedValueAtField",
                "de.is24.deadcode4j.analyzer.hibernateannotations.ClassDefiningGenericGenerator");
    }

    @Test
    public void recognizesDependencyFromClassWithGeneratedValueAnnotatedMethodToGenericGeneratorAnnotatedPackage() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassDefiningGenericGenerator.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassUsingGeneratedValueAtMethod.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.ClassUsingGeneratedValueAtMethod",
                "de.is24.deadcode4j.analyzer.hibernateannotations.ClassDefiningGenericGenerator");
    }

    @Test
    public void recognizesDependencyFromGeneratedValueAnnotatedClassesToGenericGeneratorAnnotatedPackage() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/AnotherEntityWithGeneratedValue.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/EntityWithGeneratedValue.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.AnotherEntityWithGeneratedValue",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.EntityWithGeneratedValue",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void storesTypeDefinitionsAsIntermediateResults() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatIntermediateResultIsStored();
    }

    @Test
    public void storesTypeUsagesAsIntermediateResults() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/Entity.class");

        assertThatIntermediateResultIsStored();
    }

    @Test
    public void considersTypeDefinitionsFromIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|typeDefinitions",
                givenIntermediateResultMap("byteClass", "Foo"));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/Entity.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.Entity", "Foo");
    }

    @Test
    public void prefersOwnTypeDefinitionsOverIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|typeDefinitions",
                givenIntermediateResultMap("byteClass", "Foo"));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/Entity.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.Entity",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void considersTypeUsagesFromIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|typeUsages",
                givenIntermediateResultMap("byteClass", newHashSet("Foo", "Bar")));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatDependenciesAreReportedFor("Foo", "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
        assertThatDependenciesAreReportedFor("Bar", "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void ignoresTypeUsagesFromIntermediateResultsForTypeDefinitionsFromIntermediateResults() {
        Map<Object, IntermediateResult> intermediateResults = newHashMap();
        intermediateResults.put(HibernateAnnotationsAnalyzer.class.getName() + "|typeUsages",
                givenIntermediateResultMap("byteClass", newHashSet("Bar")));
        intermediateResults.put(HibernateAnnotationsAnalyzer.class.getName() + "|typeDefinitions",
                givenIntermediateResultMap("byteClass", "Foo"));
        this.analysisContext = AnalysisContextBuilder.givenAnalysisContext(this.analysisContext.getModule(), intermediateResults);

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/EntityWithGeneratedValue.class");

        assertThatNoDependenciesAreReported();
    }

    @Test
    public void storesGeneratorDefinitionsAsIntermediateResults() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatIntermediateResultIsStored();
    }

    @Test
    public void storesGeneratorUsagesAsIntermediateResults() {
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/EntityWithGeneratedValue.class");

        assertThatIntermediateResultIsStored();
    }

    @Test
    public void considersGeneratorDefinitionsFromIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|generatorDefinitions",
                givenIntermediateResultMap("generatorOne", "Foo"));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/EntityWithGeneratedValue.class");

        assertThatDependenciesAreReportedFor(
                "de.is24.deadcode4j.analyzer.hibernateannotations.EntityWithGeneratedValue", "Foo");
    }

    @Test
    public void prefersOwnGeneratorDefinitionsOverIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|generatorDefinitions",
                givenIntermediateResultMap("generatorOne", "Foo"));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/EntityWithGeneratedValue.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatDependenciesAreReportedFor("de.is24.deadcode4j.analyzer.hibernateannotations.EntityWithGeneratedValue",
                "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void considersGeneratorUsagesFromIntermediateResults() {
        this.analysisContext = givenAnalysisContext(
                this.analysisContext.getModule(),
                HibernateAnnotationsAnalyzer.class.getName() + "|generatorUsages",
                givenIntermediateResultMap("generatorOne", newHashSet("Foo", "Bar")));

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/package-info.class");

        assertThatDependenciesAreReportedFor("Foo", "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
        assertThatDependenciesAreReportedFor("Bar", "de.is24.deadcode4j.analyzer.hibernateannotations.package-info");
    }

    @Test
    public void ignoresGeneratorUsagesFromIntermediateResultsForGeneratorDefinitionsFromIntermediateResults() {
        Map<Object, IntermediateResult> intermediateResults = newHashMap();
        intermediateResults.put(HibernateAnnotationsAnalyzer.class.getName() + "|generatorUsages",
                givenIntermediateResultMap("generatorOne", newHashSet("Bar")));
        intermediateResults.put(HibernateAnnotationsAnalyzer.class.getName() + "|generatorDefinitions",
                givenIntermediateResultMap("generatorOne", "Foo"));
        this.analysisContext = AnalysisContextBuilder.givenAnalysisContext(this.analysisContext.getModule(), intermediateResults);

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/Entity.class");

        assertThatNoDependenciesAreReported();
    }

    @Test
    public void issuesWarningForDuplicatedTypeDef() throws IllegalAccessException {
        Logger loggerMock = mock(Logger.class);
        ReflectionUtils.setVariableValueInObject(objectUnderTest, "logger", loggerMock);

        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassWithTypeDef.class");
        analyzeFile("de/is24/deadcode4j/analyzer/hibernateannotations/ClassWithDuplicatedTypeDef.class");
        finishAnalysis();

        verify(loggerMock).warn(
                org.mockito.Matchers.contains("@TypeDef"),
                (Object[]) argThat(hasVarArgItem(equalTo("aRandomType"))));
    }

    private static class MyVarArgsMatcher<T> extends IsArrayContaining<T> implements VarargMatcher {
        public MyVarArgsMatcher(Matcher<? super T> elementMatcher) {
            super(elementMatcher);
        }
    }

}