package performance;

import graphql.Assert;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.i18n.I18n;
import graphql.language.Document;
import graphql.parser.Parser;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaGenerator;
import graphql.validation.LanguageTraversal;
import graphql.validation.RulesVisitor;
import graphql.validation.ValidationContext;
import graphql.validation.ValidationError;
import graphql.validation.ValidationErrorCollector;
import graphql.validation.rules.OverlappingFieldsCanBeMerged;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import static graphql.Assert.assertTrue;

@State(Scope.Benchmark)
@Warmup(iterations = 2, time = 5)
@Measurement(iterations = 3)
@Fork(2)
public class OverlappingFieldValidationPerformance {


    static String schemaSdl = " type Query { viewer: Viewer } interface Abstract { field: Abstract leaf: Int } interface Abstract1 { field: Abstract leaf: Int } interface Abstract2 { field: Abstract leaf: Int }" +
            " type Concrete1 implements Abstract1{ field: Abstract leaf: Int}  " +
            "type Concrete2 implements Abstract2{ field: Abstract leaf: Int} " +
            "type Viewer { xingId: XingId } type XingId { firstName: String! lastName: String! }";

    @State(Scope.Benchmark)
    public static class MyState {

        GraphQLSchema schema;
        GraphQLSchema schema2;
        Document document;

        @Param({"100"})
        int size;

        Document overlapFrag;
        Document overlapNoFrag;
        Document noOverlapFrag;
        Document noOverlapNoFrag;
        Document repeatedFields;
        Document deepAbstractConcrete;

        @Setup
        public void setup() {
            try {
                overlapFrag = makeQuery(size, true, true);
                overlapNoFrag = makeQuery(size, true, false);
                noOverlapFrag = makeQuery(size, false, true);
                noOverlapNoFrag = makeQuery(size, false, false);
                repeatedFields = makeRepeatedFieldsQuery(size);
                deepAbstractConcrete = makeDeepAbstractConcreteQuery(size);


                schema2 = SchemaGenerator.createdMockedSchema(schemaSdl);

                String schemaString = PerformanceTestingUtils.loadResource("large-schema-4.graphqls");
                String query = PerformanceTestingUtils.loadResource("large-schema-4-query.graphql");
                schema = SchemaGenerator.createdMockedSchema(schemaString);
                document = Parser.parse(query);

                // make sure this is a valid query overall
                GraphQL graphQL = GraphQL.newGraphQL(schema).build();
                ExecutionResult executionResult = graphQL.execute(query);
                assertTrue(executionResult.getErrors().size() == 0);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void overlappingFieldValidationAvgTime(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema, myState.document));
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void overlappingFieldValidationThroughput(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema, myState.document));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkRepeatedFields(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.repeatedFields));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkOverlapFrag(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.overlapFrag));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkOverlapNoFrag(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.overlapNoFrag));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkNoOverlapFrag(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.noOverlapFrag));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkNoOverlapNoFrag(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.noOverlapNoFrag));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void benchmarkDeepAbstractConcrete(MyState myState, Blackhole blackhole) {
        blackhole.consume(validateQuery(myState.schema2, myState.deepAbstractConcrete));
    }

    private List<ValidationError> validateQuery(GraphQLSchema schema, Document document) {
        ValidationErrorCollector errorCollector = new ValidationErrorCollector();
        I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH);
        ValidationContext validationContext = new ValidationContext(schema, document, i18n);
        OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector);
        LanguageTraversal languageTraversal = new LanguageTraversal();
        languageTraversal.traverse(document, new RulesVisitor(validationContext, Collections.singletonList(overlappingFieldsCanBeMerged)));
        Assert.assertTrue(errorCollector.getErrors().size() == 0);
        return errorCollector.getErrors();
    }


    private static Document makeQuery(int size, boolean overlapping, boolean fragments) {
        if (fragments) {
            return makeQueryWithFragments(size, overlapping);
        } else {
            return makeQueryWithoutFragments(size, overlapping);
        }
    }

    private static Document makeRepeatedFieldsQuery(int size) {
        StringBuilder b = new StringBuilder();

        b.append(" query testQuery {  viewer {   xingId {");

        b.append("firstName\n".repeat(Math.max(0, size)));

        b.append("} } }");

        return Parser.parse(b.toString());
    }


    private static Document makeQueryWithFragments(int size, boolean overlapping) {
        StringBuilder b = new StringBuilder();

        for (int i = 1; i <= size; i++) {
            if (overlapping) {
                b.append(" fragment mergeIdenticalFields" + i + " on Query {viewer { xingId { firstName lastName  }}}");
            } else {
                b.append("fragment mergeIdenticalFields" + i + " on Query {viewer" + i + " {  xingId" + i + " {  firstName" + i + "  lastName" + i + "  } }}");
            }

            b.append("\n\n");
        }

        b.append("query testQuery {");
        for (int i = 1; i <= size; i++) {
            b.append("...mergeIdenticalFields" + i + "\n");
        }
        b.append("}");
        return Parser.parse(b.toString());
    }

    private static Document makeQueryWithoutFragments(int size, boolean overlapping) {
        StringBuilder b = new StringBuilder();

        b.append("query testQuery {");

        for (int i = 1; i <= size; i++) {
            if (overlapping) {
                b.append(" viewer {   xingId {      firstName   } } ");
            } else {
                b.append(" viewer" + i + " {    xingId" + i + " {      firstName" + i + "    }  } ");
            }

            b.append("\n\n");
        }

        b.append("}");

        return Parser.parse(b.toString());
    }

    private static Document makeDeepAbstractConcreteQuery(int depth) {
        StringBuilder q = new StringBuilder();

        q.append("fragment multiply on Whatever {   field {      " +
                "... on Abstract1 { field { leaf } }      " +
                "... on Abstract2 { field { leaf } }      " +
                "... on Concrete1 { field { leaf } }      " +
                "... on Concrete2 { field { leaf } }    } } " +
                "query DeepAbstractConcrete { ");

        for (int i = 1; i <= depth; i++) {
            q.append("field { ...multiply ");
        }

        for (int i = 1; i <= depth; i++) {
            q.append(" }");
        }

        q.append("\n}");

        return Parser.parse(q.toString());
    }
}
