/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8293590
 * @summary URL built-in protocol handlers should parse the URL early
 *          to avoid constructing URLs for which openConnection
 *          would later throw an exception, when possible.
 *          A jdk.net.url.delayParsing property allows to switch that
 *          behavior off to mitigate risks of regression
 * @run junit  EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing=true EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing=false EarlyOrDelayedParsing
 */

import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static java.lang.System.err;
import static org.junit.jupiter.api.Assertions.*;

public class EarlyOrDelayedParsing {

    public final boolean EARLY_PARSING;
    {
        String value = System.getProperty("jdk.net.url.delayParsing", "false");
        EARLY_PARSING = !value.isEmpty() && !Boolean.parseBoolean(value);
    }

    // Some characters that when included at the wrong place
    // in the authority component, without being escaped, would
    // cause an exception.
    private static final String EXCLUDED_DELIMS = "<>\" ";
    private static final String UNWISE = "{}|\\^`";
    private static final String DELIMS = "[]/?#@";

    // Test data used to test exceptions thrown by URL
    // at some point, when constructed with some illegal input.
    sealed interface URLArgTest
            permits OneArgTest, TwoArgsTest, ThreeArgsTest, FourArgsTest {

        // Some character that is expected to cause an exception
        // at some point, and which this test case is built for
        int character();

        // An URL string containing the illegal character
        String url();

        // Some characters are already checked at construction
        // time. They will cause an exception to be thrown,
        // whether delayed parsing is activated or not.
        // This method returns true if an exception is
        // expected at construction time for this test case,
        // even when delayed parsing is activated.
        boolean early(int c);

        // The URL scheme this test case is built for.
        // Typically, one of "http", "https", "ftp"...
        default String scheme() {
            return scheme(url());
        }

        // Return the URL string of this test case, after
        // substituting its scheme with the given scheme.
        default String urlWithScheme(String scheme) {
            String url = url();
            int colon = url.indexOf(':');
            String urlWithScheme = scheme + url.substring(colon);
            return urlWithScheme;
        }

        // Which exception to expect when parsing is delayed
        default boolean acceptDelayedException(Throwable exception) {
            return exception instanceof MalformedURLException
                    || exception instanceof UnknownHostException;
        }

        default String describe() {
            return this.getClass().getSimpleName() + "(url=" + url() + ")";
        }

        static int port(String protocol) {
            return switch (protocol) {
                case "http" -> 80;
                case "https" -> 443;
                case "ftp" -> 21;
                default -> -1;
            };
        }

        static String scheme(String url) {
            return url.substring(0, url.indexOf(':'));
        }
    }

    // Test data for the one arg constructor
    // public URL(String spec) throws MalformedURLException
    sealed interface OneArgTest extends URLArgTest {

        // Create a new test case identical to this one but
        // with a different URL scheme
        default OneArgTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHost) {
                return new OfHost(character(), urlWithScheme);
            }
            if (this instanceof OfUserInfo) {
                return new OfUserInfo(character(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return this instanceof OfHost &&
                    (c < 31 || c == 127);
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && character() == '\\'
                    && exception instanceof IOException;
        }

        record OfHost(int character, String url) implements OneArgTest { }
        record OfUserInfo(int character, String url) implements OneArgTest { }

        static OneArgTest ofHost(int c) {
            return new OfHost(c, "http://local%shost/".formatted(Character.toString(c)));
        }
        static OneArgTest ofUserInfo(int c) {
            return new OfUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
        }
    }

    // Test data for the two arg constructor
    // public URL(URL context, String spec) throws MalformedURLException
    sealed interface TwoArgsTest extends URLArgTest {

        // Create a new test case identical to this one but
        // with a different URL scheme
        default TwoArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfTwoArgsHost) {
                return new OfTwoArgsHost(character(), urlWithScheme);
            }
            if (this instanceof OfTwoArgsUserInfo) {
                return new OfTwoArgsUserInfo(character(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return this instanceof OfTwoArgsHost &&
                    (c < 31 || c == 127);
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && character() == '\\'
                    && exception instanceof IOException;
        }

        record OfTwoArgsHost(int character, String url) implements TwoArgsTest { }
        record OfTwoArgsUserInfo(int character, String url) implements TwoArgsTest { }

        static TwoArgsTest ofHost(int c) {
            return new OfTwoArgsHost(c, "http://local%shost/".formatted(Character.toString(c)));
        }
        static TwoArgsTest ofUserInfo(int c) {
            return new OfTwoArgsUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
        }
        static TwoArgsTest ofOneArgTest(OneArgTest test) {
            if (test instanceof OneArgTest.OfHost) {
                return ofHost(test.character());
            } else if (test instanceof OneArgTest.OfUserInfo) {
                return ofUserInfo(test.character());
            }
            throw new AssertionError("can't convert to TwoArgsTest: "
                    + test.getClass());
        }
    }


    // Test data for the three args constructor
    // public URL(String scheme, String host, String file)
    //     throws MalformedURLException
    sealed interface ThreeArgsTest extends URLArgTest {

        // the host component
        String host();

        // the path + query components
        String file();

        // Create a new test case identical to this one but
        // with a different URL scheme and port
        default ThreeArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHostFile) {
                return new OfHostFile(character(), host(), file(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return (c < 31 || c == 127 || c == '/');
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && exception instanceof IOException;
        }

        record OfHostFile(int character, String host, String file, String url)
                implements ThreeArgsTest {
        }

        static ThreeArgsTest ofHostFile(int c) {
            String host = "local%shost".formatted(Character.toString(c));
            String url = "http://" + host + "/";
            return new OfHostFile(c, host, "/", url);
        }
    }

    // Test data for the four args constructor
    // public URL(String scheme, String host, int port, String file)
    //     throws MalformedURLException
    sealed interface FourArgsTest extends URLArgTest {

        // the host component
        String host();

        // the port component
        int port();

        // the path + query components
        String file();

        // Create a new test case identical to this one but
        // with a different URL scheme and port
        default FourArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHostFilePort) {
                int port = URLArgTest.port(scheme);
                return new OfHostFilePort(character(), host(), port, file(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return (c < 31 || c == 127 || c == '/');
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && exception instanceof IOException;
        }

        record OfHostFilePort(int character, String host, int port, String file, String url)
                implements FourArgsTest {
        }

        static FourArgsTest ofHostPortFile(int c) {
            String host = "local%shost".formatted(Character.toString(c));
            String url = "http://" + host + "/";
            int port = URLArgTest.port(URLArgTest.scheme(url));
            return new OfHostFilePort(c, host, port, "/", url);
        }
    }


    // Generate test data for the URL one arg constructor, with variations
    // of the host component.
    static Stream<OneArgTest> oneArgHostTests() {
        List<OneArgTest> tests = new ArrayList<>();
        List<OneArgTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS).chars()
                .mapToObj(OneArgTest::ofHost).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(OneArgTest::ofHost).toList());
        for (String scheme : List.of("http", "https", "ftp")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Generate test data for the URL one arg constructor, with variations
    // of the user info component.
    static Stream<OneArgTest> oneArgUserInfoTests() {
        List<OneArgTest> tests = new ArrayList<>();
        List<OneArgTest> urls = new ArrayList<>();
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(OneArgTest::ofUserInfo).toList());
        urls.add(OneArgTest.ofUserInfo('\\'));
        for (String scheme : List.of("http", "https", "ftp")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Test data with all variations for the URL one arg
    // constructor (spec)
    static Stream<OneArgTest> oneArgTests() {
        return Stream.concat(oneArgHostTests(), oneArgUserInfoTests());
    }

    // Test data with all variations for the URL two arg
    // constructor (URL, spec)
    static Stream<TwoArgsTest> twoArgTests() {
        return oneArgTests().map(TwoArgsTest::ofOneArgTest);
    }

    // Generate test data for the URL three arguments constructor
    // (scheme, host, file)
    static Stream<ThreeArgsTest> threeArgsTests() {
        List<ThreeArgsTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
                .mapToObj(ThreeArgsTest::ofHostFile).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(ThreeArgsTest::ofHostFile).toList());
        List<ThreeArgsTest> tests = new ArrayList<>();
        for (String scheme : List.of("http", "https", "ftp", "file")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Generate test data for the URL four arguments constructor
    // (scheme, host, port, file)
    static Stream<FourArgsTest> fourArgsTests() {
        List<FourArgsTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
                .mapToObj(FourArgsTest::ofHostPortFile).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(FourArgsTest::ofHostPortFile).toList());
        List<FourArgsTest> tests = new ArrayList<>();
        for (String scheme : List.of("http", "https", "ftp", "file")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }



    @ParameterizedTest
    @MethodSource("oneArgTests")
    public void testOneArgConstructor(OneArgTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + test.describe());
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(url);
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + test.describe());
            URL u = new URL(url);
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                    err.println("unexpected exception type: " + exception);
                    throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("twoArgTests")
    public void testTwoArgConstructor(TwoArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        String scheme = URLArgTest.scheme(url);
        URL u = new URL(scheme, null,"");
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + test.describe());
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(u, url);
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + test.describe());
            URL u2 = new URL(u, url);
            var exception = assertThrows(IOException.class, () -> {
                u2.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("threeArgsTests")
    public void testThreeArgsConstructor(ThreeArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + url);
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(test.scheme(), test.host(), test.file());
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + url);
            URL u = new URL(test.scheme(), test.host(), test.file());
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("fourArgsTests")
    public void testFourArgsConstructor(FourArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + url);
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(test.scheme(), test.host(), test.port(), test.file());
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + url);
            URL u = new URL(test.scheme(), test.host(), test.port(), test.file());
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

}
