/**
 * Copyright (c) 2016-present, RxJava Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
 * the License for the specific language governing permissions and limitations under the License.
 */

package io.reactivex.rxjava3.validators;

import static org.junit.Assert.fail;

import java.io.*;

import org.junit.Test;

import io.reactivex.rxjava3.core.*;
import io.reactivex.rxjava3.testsupport.TestHelper;

/**
 * Checks the source code of the base reactive types and locates missing
 * mention of {@code Backpressure:} and {@code Scheduler:} of methods.
 */
public class JavadocForAnnotations {

    static void checkSource(String baseClassName, boolean scheduler) throws Exception {
        File f = TestHelper.findSource(baseClassName);
        if (f == null) {
            return;
        }

        StringBuilder b = readFile(f);

        StringBuilder e = new StringBuilder();

        if (scheduler) {
            scanFor(b, "@SchedulerSupport", "Scheduler:", e, baseClassName);
        } else {
            scanFor(b, "@BackpressureSupport", "Backpressure:", e, baseClassName);
        }

        if (e.length() != 0) {
            System.out.println(e);

            fail(e.toString());
        }
    }

    public static StringBuilder readFile(File f) throws Exception {
        StringBuilder b = new StringBuilder();

        BufferedReader in = new BufferedReader(new FileReader(f));
        try {
            for (;;) {
                String line = in.readLine();

                if (line == null) {
                    break;
                }

                b.append(line).append('\n');
            }
        } finally {
            in.close();
        }

        return b;
    }

    static final void scanFor(StringBuilder sourceCode, String annotation, String inDoc,
            StringBuilder e, String baseClassName) {
        int index = 0;
        for (;;) {
            int idx = sourceCode.indexOf(annotation, index);

            if (idx < 0) {
                break;
            }

            int j = sourceCode.lastIndexOf("/**", idx);

            // see if the last /** is not before the index (last time the annotation was found
            // indicating an uncommented method like subscribe()
            if (j > index) {
                int k = sourceCode.indexOf(inDoc, j);

                if (k < 0 || k > idx) {
                    // when printed on the console, IDEs will create a clickable link to help navigate to the offending point
                    e.append("java.lang.RuntimeException: missing ").append(inDoc).append(" section\r\n")
                    ;
                    int lc = lineNumber(sourceCode, idx);

                    e.append(" at io.reactivex.").append(baseClassName)
                    .append(" (").append(baseClassName).append(".java:")
                    .append(lc).append(")").append("\r\n\r\n");
                }
            }

            index = idx + annotation.length();
        }
    }

    static final void scanForBadMethod(StringBuilder sourceCode, String annotation, String inDoc,
            StringBuilder e, String baseClassName) {
        int index = 0;
        for (;;) {
            int idx = sourceCode.indexOf(annotation, index);

            if (idx < 0) {
                break;
            }

            int j = sourceCode.lastIndexOf("/**", idx);

            // see if the last /** is not before the index (last time the annotation was found
            // indicating an uncommented method like subscribe()
            if (j > index) {
                int k = sourceCode.indexOf(inDoc, j);

                if (k >= 0 && k <= idx) {

                    int ll = sourceCode.indexOf("You specify", k);
                    if (ll < 0) {
                        ll = sourceCode.indexOf("you specify", k);
                    }
                    int lm = sourceCode.indexOf("This operator", k);
                    if (lm < 0) {
                        lm = sourceCode.indexOf("this operator", k);
                    }
                    if ((ll < 0 || ll > idx) && (lm < 0 || lm > idx)) {

                        int n = sourceCode.indexOf("{@code ", k);
                        int endDD = sourceCode.indexOf("</dd>", k);
                        // make sure the {@code is within the dt/dd section

                        if (n < idx && n < endDD) {
                            int m = sourceCode.indexOf("}", n);

                            if (m < idx) {
                                String mname = sourceCode.substring(n + 7, m);

                                if (!"Scheduler".equals(mname)) {

                                    int q = sourceCode.indexOf("@SuppressWarnings({", idx);

                                    int o = sourceCode.indexOf("{", idx);

                                    if (q + 18 == o) {
                                        o = sourceCode.indexOf("{", q + 20);
                                    }

                                    if (o >= 0) {

                                        int p = sourceCode.indexOf(" " + mname + "(", idx);

                                        if (p < 0 || p > o) {
                                            // when printed on the console, IDEs will create a clickable link to help navigate to the offending point
                                            e.append("java.lang.RuntimeException: wrong method name in description of ").append(inDoc).append(" '").append(mname).append("'\r\n")
                                            ;
                                            int lc = lineNumber(sourceCode, idx);

                                            e.append(" at io.reactivex.rxjava3.core.").append(baseClassName)
                                            .append(".method(").append(baseClassName).append(".java:")
                                            .append(lc).append(")").append("\r\n");
                                        }
                                    }
                                }
                            }

                        }

                    }
                }
            }

            index = idx + annotation.length();
        }
    }

    static void checkSchedulerBadMethod(String baseClassName) throws Exception {
        File f = TestHelper.findSource(baseClassName);
        if (f == null) {
            return;
        }

        StringBuilder b = readFile(f);

        StringBuilder e = new StringBuilder();

        scanForBadMethod(b, "@SchedulerSupport", "Scheduler:", e, baseClassName);

        if (e.length() != 0) {
            System.out.println(e);

            fail(e.toString());
        }
    }

    public static int lineNumber(StringBuilder s, int index) {
        int cnt = 1;
        for (int i = 0; i < index; i++) {
            if (s.charAt(i) == '\n') {
                cnt++;
            }
        }
        return cnt;
    }

    @Test
    public void checkFlowableBackpressure() throws Exception {
        checkSource(Flowable.class.getSimpleName(), false);
    }

    @Test
    public void checkFlowableScheduler() throws Exception {
        checkSource(Flowable.class.getSimpleName(), true);
    }

    @Test
    public void checkObservableBackpressure() throws Exception {
        checkSource(Observable.class.getSimpleName(), false);
    }

    @Test
    public void checkObservableScheduler() throws Exception {
        checkSource(Observable.class.getSimpleName(), true);
    }

    @Test
    public void checkSingleBackpressure() throws Exception {
        checkSource(Single.class.getSimpleName(), false);
    }

    @Test
    public void checkSingleScheduler() throws Exception {
        checkSource(Single.class.getSimpleName(), true);
    }

    @Test
    public void checkCompletableBackpressure() throws Exception {
        checkSource(Completable.class.getSimpleName(), false);
    }

    @Test
    public void checkCompletableScheduler() throws Exception {
        checkSource(Completable.class.getSimpleName(), true);
    }

    @Test
    public void checkMaybeBackpressure() throws Exception {
        checkSource(Maybe.class.getSimpleName(), false);
    }

    @Test
    public void checkMaybeScheduler() throws Exception {
        checkSource(Maybe.class.getSimpleName(), true);
    }

    @Test
    public void checkFlowableSchedulerDoc() throws Exception {
        checkSchedulerBadMethod(Flowable.class.getSimpleName());
    }

    @Test
    public void checkObservableSchedulerDoc() throws Exception {
        checkSchedulerBadMethod(Observable.class.getSimpleName());
    }

    @Test
    public void checkSingleSchedulerDoc() throws Exception {
        checkSchedulerBadMethod(Single.class.getSimpleName());
    }

    @Test
    public void checkCompletableSchedulerDoc() throws Exception {
        checkSchedulerBadMethod(Completable.class.getSimpleName());
    }

    @Test
    public void checkMaybeSchedulerDoc() throws Exception {
        checkSchedulerBadMethod(Maybe.class.getSimpleName());
    }
}
