/*
 * Copyright (c) 2022, 2023, 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 8294241
 * @library /test/lib
 * @modules java.base/java.net:+open
 * @summary Test URL::fromURI(URI, URLStreamHandler)
 * @run junit/othervm URLFromURITest
 */

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.VarHandle;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import jdk.test.lib.RandomFactory;

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

import static org.junit.jupiter.api.Assertions.*;

public class URLFromURITest {

    static final Random RAND = RandomFactory.getRandom();

    record TestInput(String uri, URLStreamHandler handler) {
        static TestInput withNoHandler(String uri) {
            return new TestInput(uri, null);
        }
        TestInput withCustomHandler() {
            return new TestInput(uri(), new CustomStreamHandler());
        }
        TestInput withUrlPrefix() {return inputWithUrlPrefix(this);}
    }

    static URI uriWithUrlPrefix(URI uri) {
        return URI.create(stringWithUrlPrefix(uri.toString()));
    }

    static String stringWithUrlPrefix(String uriStr) {
        if (uriStr.regionMatches(true, 0, "url:", 0, 4)) return uriStr;
        return RAND.nextBoolean() ? "url:" + uriStr : "Url:" + uriStr;
    }

    static TestInput inputWithUrlPrefix(TestInput input) {
        String uriStr = input.uri();
        var handler = input.handler();

        var urlUriStr = stringWithUrlPrefix(uriStr);
        if (uriStr.equals(urlUriStr)) return null;

        var urlURI = URI.create(urlUriStr);
        try {
            new URL(null, urlURI.toString(), handler);
        } catch (Throwable t) {
            System.err.println("skipping new URL(null, \"" + urlURI + "\", handler): " + t);
            return null;
        }
        return new TestInput(urlUriStr, handler);
    }

    static Stream<String> uris() {
        var uris = Stream.of(
                "http://jag:cafebabe@java.sun.com:94/b/c/d?q#g",
                "http://[1080:0:0:0:8:800:200C:417A]/index.html",
                "http://a/b/c/d;p?q",
                "mailto:mduerst@ifi.unizh.ch",
                "http:comp.infosystems.www.servers.unix",
                "http://j%41g:cafeb%41be@java.sun.com:94/%41/b/c/d?q#g",
                "jar:file:///x.jar!/",
                "jmod:/java.base",
                "jmod:///java.base");

        if (hasFtp()) {
            uris = Stream.concat(uris,
                    Stream.of("ftp://ftp.is.co.za/rfc/rfc1808.txt"));
        }

        return uris;
    }

    static Stream<String> nonOverridableUris() {
        return Stream.of("file:///nohost/%41/",
                "file://with.host/%41/",
                "file:/x/y/z",
                "jrt:/java.base/java/lang/Integer.class",
                "jrt:///java.base/java/lang/Integer.class");
    }
    static Stream<TestInput> withNoHandler() {
        return Stream.concat(uris(), nonOverridableUris())
                .map(TestInput::withNoHandler);
    }

    static Stream<TestInput> withCustomHandler() {
        var withHandlers = uris()
                .map(TestInput::withNoHandler)
                .map(TestInput::withCustomHandler);
        return Stream.concat(withHandlers, Stream.of(
                new TestInput("foo:bar:baz", new CustomStreamHandler()),
                new TestInput("jar:file:///x.jar!/", new CustomStreamHandler()),
                new TestInput("jar:jar:file///x.jar!/bing", new CustomStreamHandler()),
                new TestInput("blah://localhost:80/x/y/z", new CustomStreamHandler())
        ));
    }

    static Stream<TestInput> overridingNonOverridable() {
        return nonOverridableUris().map(TestInput::withNoHandler)
                .map(TestInput::withCustomHandler);
    }

    @Test
    public void checkExceptions() {
        var noscheme = URI.create("http");
        var unknown = URI.create("unknown:///foo/bar");
        var opaque = URI.create("opaque:opaque-path");
        var jrt = URI.create("jrt:/java.base/java.lang.Integer.class");
        var file = URI.create("file:/");
        var unoscheme = uriWithUrlPrefix(noscheme);
        var uunknown = uriWithUrlPrefix(unknown);
        var uopaque = uriWithUrlPrefix(opaque);
        var ujrt = uriWithUrlPrefix(jrt);
        var ufile = uriWithUrlPrefix(file);
        var handler = new CustomStreamHandler();
        assertThrows(NullPointerException.class, () -> URL.of(null, null));
        assertThrows(NullPointerException.class, () -> URL.of(null, handler));
        assertThrows(IllegalArgumentException.class, () -> URL.of(noscheme, null));
        assertThrows(IllegalArgumentException.class, () -> URL.of(noscheme, handler));
        assertThrows(IllegalArgumentException.class, () -> URL.of(jrt, handler));
        assertThrows(IllegalArgumentException.class, () -> URL.of(file, handler));
        assertThrows(IllegalArgumentException.class, () -> URL.of(ujrt, handler));
        assertThrows(IllegalArgumentException.class, () -> URL.of(ufile, handler));
        assertThrows(MalformedURLException.class, () -> URL.of(unknown, null));
        assertThrows(MalformedURLException.class, () -> URL.of(opaque, null));
        assertThrows(MalformedURLException.class, () -> URL.of(uunknown, null));
        assertThrows(MalformedURLException.class, () -> URL.of(uopaque, null));
        assertThrows(MalformedURLException.class, () -> URL.of(unoscheme, null));
        assertThrows(MalformedURLException.class, () -> URL.of(unoscheme, handler));
    }

    @ParameterizedTest
    @MethodSource(value = "withNoHandler")
    public void testWithNoHandler(TestInput input) throws Exception {
        String uriStr = input.uri();
        URLStreamHandler handler = input.handler();
        System.err.println("testWithNoHandler: " + uriStr);
        assertNull(handler, input + ": input handler");
        URI uri = new URI(uriStr);
        URL url = URL.of(uri, handler);
        checkNoHandler(input, uri, url);
        var urlInput = input.withUrlPrefix();
        if (urlInput != null) {
            try {
                var urlURI = URI.create(input.uri());
                checkNoHandler(urlInput, uri, URL.of(urlURI, null));
            } catch (Throwable x) {
                throw new AssertionError("Failed: " + urlInput.uri() + " with: " + x, x);
            }
        }
    }

    private void checkNoHandler(TestInput input, URI uri, URL url) throws Exception {
        System.err.println("Testing: " + uri);
        checkURL(input, uri, url);
        URLStreamHandler urlHandler = URLAccess.getHandler(url);
        assertNotNull(urlHandler, input + ": URL.handler");
        assertNull(urlHandler.getClass().getClassLoader(),
                input + ": URL.handler class loader");
    }

    @ParameterizedTest
    @MethodSource(value = "withCustomHandler")
    public void checkCustomHandler(TestInput input) throws Exception {
        String uriStr = input.uri();
        URLStreamHandler handler = input.handler();
        System.err.println("testWithCustomHandler: " + input);
        assertNotNull(handler, input + ": input handler");
        URI uri = new URI(uriStr);
        URL url = URL.of(uri, handler);
        checkCustomHandler(input, uri, url, handler);
        var urlInput = input.withUrlPrefix();
        if (urlInput != null) {
            urlInput = urlInput.withCustomHandler();
            handler = urlInput.handler();
            try {
                var urlURI = URI.create(urlInput.uri());
                checkCustomHandler(urlInput, uri, URL.of(urlURI, handler), handler);
            } catch (Throwable x) {
                throw new AssertionError("Failed with handler: " + urlInput.uri() + " with: " + x, x);
            }
        }
    }

    private void checkCustomHandler(TestInput input, URI uri, URL url,
                                    URLStreamHandler handler) throws Exception {
        System.err.println("Testing: " + uri);
        checkURL(input, uri, url);
        URLStreamHandler urlHandler = URLAccess.getHandler(url);
        assertSame(handler, urlHandler, input + ": URL.handler");
        URLConnection c = url.openConnection();
        assertNotNull(c, input + ": opened connection");
        assertEquals(CustomURLConnection.class, c.getClass(),
                input + ": connection class");
        assertEquals(CustomStreamHandler.class, urlHandler.getClass(),
                input + ": handler class");
        assertEquals(((CustomURLConnection)c).handler, handler, input + ": handler");
        assertEquals(c.getURL(), url, input + ": connection url");
        var customHandler = (CustomStreamHandler)urlHandler;
        assertEquals(customHandler.parseURLCalled(), 1, "parseURL calls");
    }

    @ParameterizedTest
    @MethodSource(value = "overridingNonOverridable")
    public void testOverridingNonOverridable(TestInput input) throws Exception {
        String uriStr = input.uri();
        URLStreamHandler handler = input.handler();
        System.err.println("testOverridingNonOverridable: " + input);
        assertNotNull(handler, input + ": input handler");
        URI uri = new URI(uriStr);
        try {
            URL url = URL.of(uri, handler);
            throw new AssertionError("Should not be able to specify handler for: " + uriStr);
        } catch (IllegalArgumentException x) {
            System.err.println("Got expected exception: " + x);
        }
    }

    private static boolean isFileBased(URI uri) {
        String scheme = uri.getScheme();
        boolean isJrt = "jrt".equals(scheme.toLowerCase(Locale.ROOT));
        boolean isJmod = "jmod".equals(scheme.toLowerCase(Locale.ROOT));
        boolean isFile = "file".equals(scheme.toLowerCase(Locale.ROOT));
        return isJmod || isJrt || isFile;
    }

    private static void checkURL(TestInput input, URI uri, URL url) throws MalformedURLException {
        String scheme = uri.getScheme();
        assertEquals(scheme, url.getProtocol(), input + ": scheme");

        if (uri.isOpaque()) {
            String ssp = uri.getSchemeSpecificPart();
            assertEquals(ssp, url.getPath(), input + ": ssp");
        } else {
            String authority = uri.getRawAuthority();
            boolean isHierarchical = uri.toString().startsWith(scheme + "://");
            boolean isFileBased = isFileBased(uri);

            // Network based URLs usually follow URI, but file based
            // protocol handlers have a few discrepancies in how they
            // treat an absent authority:
            // - URI authority is null if there is no authority, always
            // - URL authority is null or empty depending on the protocol
            //   and on whether the URL is hierarchical or not.
            if (isFileBased && authority == null) {
                // jrt: takes a fastpath - so that jrt:/ is equivalent to jrt:///
                if (scheme.equals("jrt")) {
                    authority = "";
                }
                if (isHierarchical) {
                    authority = "";
                }
            }
            assertEquals(authority, url.getAuthority(), input + ": authority");

            // Network based URLs usually follow URI, but file based
            // protocol handlers have a few discrepancies in how they
            // treat an absent host:
            String host = uri.getHost();
            if (isFileBased && host == null) {
                host = "";
            }

            assertEquals(host, url.getHost(), input + ": host");
            if (host != null) {
                String userInfo = uri.getRawUserInfo();
                assertEquals(userInfo, url.getUserInfo(), input + ": userInfo");
                assertEquals(uri.getPort(), url.getPort(), input + ": port");
            }

            String path = uri.getRawPath();
            assertEquals(path, url.getPath(), input + ": path");

            String query = uri.getQuery();
            assertEquals(query, url.getQuery(), input + ": query");
        }
        String frag = uri.getRawFragment();
        assertEquals(frag, url.getRef(), input + ": fragment");
    }

    @SuppressWarnings("deprecation")
    private static boolean hasFtp() {
        try {
            return new java.net.URL("ftp://localhost/") != null;
        } catch (java.net.MalformedURLException x) {
            System.err.println("FTP not supported by this runtime.");
            return false;
        }
    }

    static class CustomURLConnection extends URLConnection {

        public final CustomStreamHandler handler;
        CustomURLConnection(CustomStreamHandler handler, URL url) {
            super(url);
            this.handler = handler;
        }

        @Override
        public void connect() throws IOException {

        }
    }
    static class CustomStreamHandler extends URLStreamHandler {

        final AtomicInteger parseURLCalled = new AtomicInteger();

        @Override
        protected void parseURL(URL u, String spec, int start, int limit) {
            parseURLCalled.incrementAndGet();
            super.parseURL(u, spec, start, limit);
        }

        @Override
        protected URLConnection openConnection(URL u) throws IOException {
            return new CustomURLConnection(this, u);
        }

        public int parseURLCalled() {
            return parseURLCalled.get();
        }
    }

    static final class URLAccess {
        static final VarHandle HANDLER;
        static {
            try {
                Lookup lookup = MethodHandles.privateLookupIn(URL.class, MethodHandles.lookup());
                HANDLER = lookup.findVarHandle(URL.class, "handler", URLStreamHandler.class);
            } catch (Exception x) {
                throw new ExceptionInInitializerError(x);
            }
        }
        static URLStreamHandler getHandler(URL url) {
            return (URLStreamHandler)HANDLER.get(url);
        }
    }
}