/*
 * 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.rxjava4.validators;

import java.io.File;
import java.nio.file.Files;
import java.util.*;

import org.junit.Test;

import io.reactivex.rxjava4.core.*;
import io.reactivex.rxjava4.core.Observable;
import io.reactivex.rxjava4.disposables.*;
import io.reactivex.rxjava4.flowables.ConnectableFlowable;
import io.reactivex.rxjava4.observables.ConnectableObservable;
import io.reactivex.rxjava4.parallel.ParallelFlowable;
import io.reactivex.rxjava4.processors.*;
import io.reactivex.rxjava4.schedulers.Schedulers;
import io.reactivex.rxjava4.subjects.*;
import io.reactivex.rxjava4.testsupport.TestHelper;

/**
 * Check if the parameter name in Objects.requireNonNull
 * and ObjectHelper.verifyPositive calls match the parameter
 *  name in the message.
 */
public class ParamValidationNaming {

    @Test
    public void checkCompletable() throws Exception {
        processFile(Completable.class);
    }

    @Test
    public void checkSingle() throws Exception {
        processFile(Single.class);
    }

    @Test
    public void checkMaybe() throws Exception {
        processFile(Maybe.class);
    }

    @Test
    public void checkObservable() throws Exception {
        processFile(Observable.class);
    }

    @Test
    public void checkFlowable() throws Exception {
        processFile(Flowable.class);
    }

    @Test
    public void checkParallelFlowable() throws Exception {
        processFile(ParallelFlowable.class);
    }

    @Test
    public void checkConnectableObservable() throws Exception {
        processFile(ConnectableObservable.class);
    }

    @Test
    public void checkConnectableFlowable() throws Exception {
        processFile(ConnectableFlowable.class);
    }

    @Test
    public void checkSubject() throws Exception {
        processFile(Subject.class);
    }

    @Test
    public void checkFlowableProcessor() throws Exception {
        processFile(FlowableProcessor.class);
    }

    @Test
    public void checkDisposable() throws Exception {
        processFile(Disposable.class);
    }

    @Test
    public void checkScheduler() throws Exception {
        processFile(Scheduler.class);
    }

    @Test
    public void checkSchedulers() throws Exception {
        processFile(Schedulers.class);
    }

    @Test
    public void checkAsyncSubject() throws Exception {
        processFile(AsyncSubject.class);
    }

    @Test
    public void checkBehaviorSubject() throws Exception {
        processFile(BehaviorSubject.class);
    }

    @Test
    public void checkPublishSubject() throws Exception {
        processFile(PublishSubject.class);
    }

    @Test
    public void checkReplaySubject() throws Exception {
        processFile(ReplaySubject.class);
    }

    @Test
    public void checkUnicastSubject() throws Exception {
        processFile(UnicastSubject.class);
    }

    @Test
    public void checkSingleSubject() throws Exception {
        processFile(SingleSubject.class);
    }

    @Test
    public void checkMaybeSubject() throws Exception {
        processFile(MaybeSubject.class);
    }

    @Test
    public void checkCompletableSubject() throws Exception {
        processFile(CompletableSubject.class);
    }

    @Test
    public void checkAsyncProcessor() throws Exception {
        processFile(AsyncProcessor.class);
    }

    @Test
    public void checkBehaviorProcessor() throws Exception {
        processFile(BehaviorProcessor.class);
    }

    @Test
    public void checkPublishProcessor() throws Exception {
        processFile(PublishProcessor.class);
    }

    @Test
    public void checkReplayProcessor() throws Exception {
        processFile(ReplayProcessor.class);
    }

    @Test
    public void checkUnicastProcessor() throws Exception {
        processFile(UnicastProcessor.class);
    }

    @Test
    public void checkMulticastProcessor() throws Exception {
        processFile(MulticastProcessor.class);
    }

    @Test
    public void checkCompositeDisposable() throws Exception {
        processFile(CompositeDisposable.class);
    }

    static void processFile(Class<?> clazz) throws Exception {
        String baseClassName = clazz.getSimpleName();
        File f = TestHelper.findSource(baseClassName, clazz.getPackage().getName());
        if (f == null) {
            return;
        }
        String fullClassName = clazz.getName();

        int errorCount = 0;
        StringBuilder errors = new StringBuilder();

        List<String> lines = Files.readAllLines(f.toPath());

        for (int j = 0; j < lines.size(); j++) {
            String line = lines.get(j).trim();

            for (ValidatorStrings validatorStr : VALIDATOR_STRINGS) {
                int strIdx = line.indexOf(validatorStr.code);
                if (strIdx >= 0) {

                    int comma = line.indexOf(',', strIdx + validatorStr.code.length());

                    String paramName = line.substring(strIdx + validatorStr.code.length(), comma);

                    int quote = line.indexOf('"', comma);

                    String message = line.substring(quote + 1, Math.min(line.length(), quote + 2 + paramName.length()));

                    if (line.contains("\"A Disposable")) {
                        continue;
                    }

                    if (!line.contains("\"The RxJavaPlugins")
                            && !(message.startsWith(paramName)
                            && (message.endsWith(" ") || message.endsWith("\"")))) {
                        errorCount++;
                        errors.append("L")
                        .append(j)
                        .append(" : Wrong validator message parameter name\r\n    ")
                        .append(line)
                        .append("\r\n")
                        .append("    ").append(paramName).append(" != ").append(message)
                        .append("\r\n at ")
                        .append(fullClassName)
                        .append(".method(")
                        .append(f.getName())
                        .append(":")
                        .append(j + 1)
                        .append(")\r\n")
                        ;
                    }

                    int midx = j - 1;
                    // find the method declaration
                    for (; midx >= 0; midx--) {
                        String linek = lines.get(midx).trim();
                        if (linek.startsWith("public") || linek.startsWith("private")
                                || linek.startsWith("protected")
                                || linek.startsWith("static")
                                || linek.startsWith(baseClassName)) {
                            break;
                        }
                    }

                    if (line.contains("\"The RxJavaPlugins")) {
                        continue;
                    }

                    // find JavaDoc of throws
                    boolean found = false;
                    for (int k = midx - 1; k >= 0; k--) {
                        String linek = lines.get(k).trim();
                        if (linek.startsWith("/** {@inheritDoc} */")) {
                            found = true;
                            // the docs in in the sealed doc class, skip the check
                            break;
                        }
                        if (linek.startsWith("/**")) {
                            break;
                        }
                        if (linek.startsWith("}")) {
                            found = true; // no method JavaDoc present
                            break;
                        }
                        if (linek.startsWith(validatorStr.javadoc)) {
                            // see if a @code paramName is present
                            String paramStr = "{@code " + paramName + "}";
                            for (int m = k; m < lines.size(); m++) {
                                String linem = lines.get(m).trim();
                                if (linem.startsWith("* @see")
                                        || linem.startsWith("* @since")
                                        || linem.startsWith("*/")) {
                                    break;
                                }
                                if (linem.contains(paramStr)) {
                                    found = true;
                                    break;
                                }
                            }
                            break;
                        }
                    }

                    if (!found) {
                        errorCount++;
                        errors.append("L")
                        .append(j)
                        .append(" : missing '")
                        .append(validatorStr.javadoc)
                        .append("' for argument validation: ")
                        .append(paramName)
                        .append("\r\n    ")
                        .append(line)
                        .append("\r\n at ")
                        .append(fullClassName)
                        .append(".method(")
                        .append(f.getName())
                        .append(":")
                        .append(j + 1)
                        .append(")\r\n")
                        ;
                    }
                }
            }

            for (ValidatorStrings validatorStr : EXCEPTION_STRINGS) {
                int strIdx = line.indexOf(validatorStr.code);
                if (strIdx >= 0) {

                    int midx = j - 1;
                    // find the method declaration
                    for (; midx >= 0; midx--) {
                        String linek = lines.get(midx).trim();
                        if (linek.startsWith("public") || linek.startsWith("private")
                                || linek.startsWith("protected")
                                || linek.startsWith("static")
                                || linek.startsWith(baseClassName)) {
                            break;
                        }
                    }

                    // find JavaDoc of throws
                    boolean found = false;
                    for (int k = midx - 1; k >= 0; k--) {
                        String linek = lines.get(k).trim();
                        if (linek.startsWith("/**")) {
                            break;
                        }
                        if (linek.startsWith("}")) {
                            found = true; // no JavaDoc
                            break;
                        }
                        if (linek.startsWith(validatorStr.javadoc)) {
                            found = true;
                        }
                    }

                    if (!found) {
                        errorCount++;
                        errors.append("L")
                        .append(j)
                        .append(" : missing '")
                        .append(validatorStr.javadoc)
                        .append("' for exception\r\n    ")
                        .append(line)
                        .append("\r\n at ")
                        .append(fullClassName)
                        .append(".method(")
                        .append(f.getName())
                        .append(":")
                        .append(j + 1)
                        .append(")\r\n")
                        ;
                    }
                }
            }

            if (line.startsWith("public") || line.startsWith("protected") || line.startsWith("final") || line.startsWith("private")
                    || line.startsWith("static")) {
                for (ValidatorStrings validatorStr : TYPICAL_ARGUMENT_STRINGS) {
                    // find the method declaration ending {
                    for (int i = j; i < lines.size(); i++) {
                        String linei = lines.get(i).trim();

                        // space + code for capturing type declarations
                        String varPattern = " " + validatorStr.code;
                        if (linei.contains(varPattern + ")")
                                || linei.contains(varPattern + ",")
                                || linei.endsWith(varPattern)) {
                            // ignore nullable-annotated arguments
                            if (!linei.matches(".*\\@Nullable\\s.*" + validatorStr.code + ".*")) {
                                boolean found = false;
                                for (int k = i - 1; k >= 0; k--) {
                                    String linek = lines.get(k).trim();
                                    if (linek.startsWith("/** {@inheritDoc} */")) {
                                        found = true;
                                        break;
                                        // the document is in the sealed doc interface
                                    }
                                    if (linek.startsWith("/**")) {
                                        break;
                                    }
                                    if (linek.startsWith("}")) {
                                        found = true; // no method JavaDoc present
                                        break;
                                    }
                                    if (linek.startsWith(validatorStr.javadoc)) {
                                        // see if a @code paramName is present
                                        String paramStr = "{@code " + validatorStr.code + "}";
                                        for (int m = k; m < lines.size(); m++) {
                                            String linem = lines.get(m).trim();
                                            if (linem.startsWith("* @see")
                                                    || linem.startsWith("* @since")
                                                    || linem.startsWith("*/")) {
                                                break;
                                            }
                                            if (linem.contains(paramStr)) {
                                                found = true;
                                                break;
                                            }
                                        }
                                        break;
                                    }
                                }

                                if (!found) {
                                    errorCount++;
                                    errors.append("L")
                                    .append(j)
                                    .append(" : missing '")
                                    .append(validatorStr.javadoc)
                                    .append("' for typical argument: ")
                                    .append(validatorStr.code)
                                    .append("\r\n    ")
                                    .append(line)
                                    .append("\r\n at ")
                                    .append(fullClassName)
                                    .append(".method(")
                                    .append(f.getName())
                                    .append(":")
                                    .append(j + 1)
                                    .append(")\r\n")
                                    ;
                                }
                            }
                        }

                        if (linei.endsWith("{") || linei.endsWith(";")) {
                            break;
                        }
                    }
                }
            }
        }

        if (errorCount != 0) {
            errors.insert(0, errorCount + " problems\r\n");
            errors.setLength(errors.length() - 2);
            throw new AssertionError(errors.toString());
        }
    }

    static final class ValidatorStrings {
        final String code;
        final String javadoc;
        ValidatorStrings(String code, String javadoc) {
            this.code = code;
            this.javadoc = javadoc;
        }
    }

    static final List<ValidatorStrings> VALIDATOR_STRINGS = Arrays.asList(
            new ValidatorStrings("Objects.requireNonNull(", "* @throws NullPointerException"),
            new ValidatorStrings("ObjectHelper.verifyPositive(", "* @throws IllegalArgumentException")
    );

    static final List<ValidatorStrings> EXCEPTION_STRINGS = Arrays.asList(
            new ValidatorStrings("throw new NullPointerException(", "* @throws NullPointerException"),
            new ValidatorStrings("throw new IllegalArgumentException(", "* @throws IllegalArgumentException"),
            new ValidatorStrings("throw new IndexOutOfBoundsException(", "* @throws IndexOutOfBoundsException")
    );

    static final List<ValidatorStrings> TYPICAL_ARGUMENT_STRINGS = Arrays.asList(
            new ValidatorStrings("source", "* @throws NullPointerException"),
            new ValidatorStrings("source1", "* @throws NullPointerException"),
            new ValidatorStrings("source2", "* @throws NullPointerException"),
            new ValidatorStrings("source3", "* @throws NullPointerException"),
            new ValidatorStrings("source4", "* @throws NullPointerException"),
            new ValidatorStrings("source5", "* @throws NullPointerException"),
            new ValidatorStrings("source6", "* @throws NullPointerException"),
            new ValidatorStrings("source7", "* @throws NullPointerException"),
            new ValidatorStrings("source8", "* @throws NullPointerException"),
            new ValidatorStrings("source9", "* @throws NullPointerException"),
            new ValidatorStrings("sources", "* @throws NullPointerException"),
            new ValidatorStrings("mapper", "* @throws NullPointerException"),
            new ValidatorStrings("combiner", "* @throws NullPointerException"),
            new ValidatorStrings("zipper", "* @throws NullPointerException"),
            new ValidatorStrings("predicate", "* @throws NullPointerException"),
            new ValidatorStrings("item", "* @throws NullPointerException"),
            new ValidatorStrings("item1", "* @throws NullPointerException"),
            new ValidatorStrings("item2", "* @throws NullPointerException"),
            new ValidatorStrings("item3", "* @throws NullPointerException"),
            new ValidatorStrings("item4", "* @throws NullPointerException"),
            new ValidatorStrings("item5", "* @throws NullPointerException"),
            new ValidatorStrings("item6", "* @throws NullPointerException"),
            new ValidatorStrings("item7", "* @throws NullPointerException"),
            new ValidatorStrings("item8", "* @throws NullPointerException"),
            new ValidatorStrings("item9", "* @throws NullPointerException"),
            new ValidatorStrings("item10", "* @throws NullPointerException"),
            new ValidatorStrings("unit", "* @throws NullPointerException"),
            new ValidatorStrings("scheduler", "* @throws NullPointerException"),
            new ValidatorStrings("other", "* @throws NullPointerException"),
            new ValidatorStrings("fallback", "* @throws NullPointerException"),
            new ValidatorStrings("defaultItem", "* @throws NullPointerException"),
            new ValidatorStrings("defaultValue", "* @throws NullPointerException"),
            new ValidatorStrings("stop", "* @throws NullPointerException"),
            new ValidatorStrings("stopPredicate", "* @throws NullPointerException"),
            new ValidatorStrings("handler", "* @throws NullPointerException"),
            new ValidatorStrings("bufferSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("openingIndicator", "* @throws NullPointerException"),
            new ValidatorStrings("closingIndicator", "* @throws NullPointerException"),
            new ValidatorStrings("boundary", "* @throws NullPointerException"),
            new ValidatorStrings("boundaryIndicator", "* @throws NullPointerException"),
            new ValidatorStrings("selector", "* @throws NullPointerException"),
            new ValidatorStrings("resultSelector", "* @throws NullPointerException"),
            new ValidatorStrings("keySelector", "* @throws NullPointerException"),
            new ValidatorStrings("valueSelector", "* @throws NullPointerException"),
            new ValidatorStrings("valueSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("collectionSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("onNext", "* @throws NullPointerException"),
            new ValidatorStrings("onError", "* @throws NullPointerException"),
            new ValidatorStrings("onComplete", "* @throws NullPointerException"),
            new ValidatorStrings("onEvent", "* @throws NullPointerException"),
            new ValidatorStrings("onAfterNext", "* @throws NullPointerException"),
            new ValidatorStrings("onAfterTerminate", "* @throws NullPointerException"),
            new ValidatorStrings("onTerminate", "* @throws NullPointerException"),
            new ValidatorStrings("onSuccess", "* @throws NullPointerException"),
            new ValidatorStrings("onSubscribe", "* @throws NullPointerException"),
            new ValidatorStrings("onNotification", "* @throws NullPointerException"),
            new ValidatorStrings("onCancel", "* @throws NullPointerException"),
            new ValidatorStrings("onDispose", "* @throws NullPointerException"),
            new ValidatorStrings("onRequest", "* @throws NullPointerException"),
            new ValidatorStrings("onNextMapper", "* @throws NullPointerException"),
            new ValidatorStrings("onErrorMapper", "* @throws NullPointerException"),
            new ValidatorStrings("onCompleteSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("clazz", "* @throws NullPointerException"),
            new ValidatorStrings("next", "* @throws NullPointerException"),
            new ValidatorStrings("reducer", "* @throws NullPointerException"),
            new ValidatorStrings("seed", "* @throws NullPointerException"),
            new ValidatorStrings("seedSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("mapSupplier", "* @throws NullPointerException"),
            new ValidatorStrings("collectionFactory", "* @throws NullPointerException"),
            new ValidatorStrings("factory", "* @throws NullPointerException"),
            new ValidatorStrings("stage", "* @throws NullPointerException"),
            new ValidatorStrings("stream", "* @throws NullPointerException"),
            new ValidatorStrings("collector", "* @throws NullPointerException"),
            new ValidatorStrings("subscriptionIndicator", "* @throws NullPointerException"),
            new ValidatorStrings("itemDelayIndicator", "* @throws NullPointerException"),
            new ValidatorStrings("future", "* @throws NullPointerException"),

            new ValidatorStrings("maxConcurrency", "* @throws IllegalArgumentException"),
            new ValidatorStrings("parallelism", "* @throws IllegalArgumentException"),
            new ValidatorStrings("prefetch", "* @throws IllegalArgumentException"),
            new ValidatorStrings("bufferSize", "* @throws IllegalArgumentException"),
            new ValidatorStrings("capacityHint", "* @throws IllegalArgumentException"),
            new ValidatorStrings("capacity", "* @throws IllegalArgumentException"),
            new ValidatorStrings("count", "* @throws IllegalArgumentException"),
            new ValidatorStrings("skip", "* @throws IllegalArgumentException"),
            new ValidatorStrings("times", "* @throws IllegalArgumentException"),
            new ValidatorStrings("n", "* @throws IllegalArgumentException")
    );

}
