/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.http;

import static org.apache.jena.fuseki.test.HttpTest.expect401;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Triple;
import org.apache.jena.http.sys.HttpRequestModifier;
import org.apache.jena.http.sys.RegistryRequestModifier;
import org.apache.jena.query.ARQ;
import org.apache.jena.rdflink.RDFLink;
import org.apache.jena.rdflink.RDFLinkFactory;
import org.apache.jena.rdflink.RDFLinkHTTP;
import org.apache.jena.riot.web.HttpNames;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.DatasetGraphFactory;
import org.apache.jena.sparql.exec.QueryExec;
import org.apache.jena.sparql.exec.http.DSP;
import org.apache.jena.sparql.exec.http.GSP;
import org.apache.jena.sparql.exec.http.QueryExecHTTP;
import org.apache.jena.sparql.exec.http.UpdateExecHTTP;
import org.apache.jena.sparql.graph.GraphFactory;
import org.apache.jena.sparql.sse.SSE;
import org.apache.jena.test.conn.EnvTest;

/**
 * This is more than just RDFLinkRemote - it covers the components
 */
public class TestAuthRemote {
    private static String user = "user";
    private static String password = "password";

    private EnvTest env;
    @BeforeEach public void beforeClass() {
        //FusekiLogging.setLogging();
        env = EnvTest.createAuth("/ds", DatasetGraphFactory.createTxnMem(), user, password);
    }

    @AfterEach public void after() {
        EnvTest.stop(env);
    }

    // ---- QueryExecHTTP

    @Test
    public void auth_qe_no_auth() {
        expect401(()->{
            try ( QueryExec qexec = QueryExecHTTP.newBuilder()
                    //.httpClient(hc)
                    .endpoint(env.datasetURL())
                    .queryString("ASK{}")
                    .build()) {
                qexec.ask();
            }
        });
    }

    @Test
    public void auth_qe_good_auth() {
        try ( QueryExec qexec = QueryExecHTTP.newBuilder()
                .httpClient(env.httpClientAuthGood())
                .endpoint(env.datasetURL())
                .queryString("ASK{}")
                .build()) {
            qexec.ask();
        }
    }

    @Test
    public void auth_qe_bad_auth() {
        expect401(()->{
            try ( QueryExec qexec = QueryExecHTTP.newBuilder()
                    .httpClient(env.httpClientAuthBad())
                    .endpoint(env.datasetURL())
                    .queryString("ASK{}")
                    .build()) {
                qexec.ask();
            }
        });
    }

    // Retries up to "java.net.httpclient.redirects.retrylimit" (3 by default).
    @Test
    public void auth_qe_bad_auth_retries() {
        expect401(()->{
            try ( QueryExec qexec = QueryExecHTTP.newBuilder()
                    // retry blindly.
                    .httpClient(EnvTest.httpClient(env.authenticatorBadRetries()))
                    .endpoint(env.datasetURL())
                    .queryString("ASK{}")
                    .build()) {
                qexec.ask();
            }
        });
    }

    // ---- UpdateExecHTTP

    @Test
    public void auth_update_no_auth() {
        expect401(()->
            UpdateExecHTTP.newBuilder()
                .endpoint(env.datasetURL())
                .updateString("INSERT DATA { <x:s> <x:p> <x:o> }")
                .build()
                .execute()
        );
    }

    @Test
    public void auth_update_good_auth() {
        UpdateExecHTTP.newBuilder()
            .httpClient(env.httpClientAuthGood())
            .endpoint(env.datasetURL())
            .updateString("INSERT DATA { <x:s> <x:p> <x:o> }")
            .build()
            .execute();
    }

    @Test
    public void auth_update_bad_auth() {
        expect401(()->
            UpdateExecHTTP.newBuilder()
                .httpClient(env.httpClientAuthBad())
                .endpoint(env.datasetURL())
                .updateString("INSERT DATA { <x:s> <x:p> <x:o> }")
                .build()
                .execute()
        );
    }

    // ---- GSP

    @Test
    public void auth_gsp_no_auth() {
        expect401(()->{
            GSP.service(env.datasetURL()).defaultGraph().GET();
        });
    }

    @Test
    public void auth_gsp_good_auth_1() {
        Graph graph = GSP.service(env.datasetURL()).httpClient(env.httpClientAuthGood()).defaultGraph().GET();
        assertNotNull(graph);
    }

    @Test
    public void auth_dsp_good_auth_2() {
        DatasetGraph dsg = DSP.service(env.datasetURL()).httpClient(env.httpClientAuthGood()).GET();
        assertNotNull(dsg);
    }

    @Test
    public void auth_dsp_good_auth_3() {
        DatasetGraph dsg = DSP.service(env.datasetURL()).httpClient(env.httpClientAuthGood()).GET();
        assertNotNull(dsg);
    }

    @Test
    public void auth_gsp_bad_auth_1() {
        // 401 because we didn't authenticate.
        expect401(()->
            GSP.service(env.datasetURL()).httpClient(env.httpClientAuthBad()).defaultGraph().GET()
        );
    }

    @Test
    public void auth_dsp_bad_auth_2() {
        // 401 because we didn't authenticate.
        expect401(()->
            DSP.service(env.datasetURL()).httpClient(env.httpClientAuthBad()).GET()
        );
    }

    @Test
    public void auth_dsp_bad_auth_3() {
        // 401 because we didn't authenticate.
        expect401(()->
            DSP.service(env.datasetURL()).httpClient(env.httpClientAuthBad()).GET()
        );
    }

    // RDFLink

    @Test
    public void auth_link_no_auth_1() {
        expect401(()->{
            try ( RDFLink link = RDFLinkFactory.connect(env.datasetURL()) ) {
                link.queryAsk("ASK{}");
            }
        });
    }

    @Test
    public void auth_link_no_auth_2() {
        expect401(()->{
            try ( RDFLink link = RDFLinkFactory.connect(env.datasetURL()) ) {
                link.update("INSERT DATA { <x:s> <x:p> <x:o> }");
            }
        });
    }

    @Test
    public void auth_link_no_auth_3() {
        expect401(()->{
            try ( RDFLink link = RDFLinkFactory.connect(env.datasetURL()) ) {
                link.get();
            }
        });
    }

    @Test
    public void auth_link_good_auth_1() {
        try ( RDFLink link = RDFLinkHTTP.newBuilder()
                    .destination(env.datasetURL())
                    .httpClient(env.httpClientAuthGood())
                    .build()) {
            link.queryAsk("ASK{}");
            link.update("INSERT DATA { <x:s> <x:p> <x:o> }");
            Graph graph = link.get();
            assertNotNull(graph);
            DatasetGraph dsg = link.getDataset();
            assertNotNull(dsg);
        }
    }

    @Test
    public void auth_link_bad_auth_1() {
        // 401 (not 403) because we didn't authenticate
        // 403 is recognized authenticate, not sufficient for ths resource.
        expect401(()->{
            try ( RDFLink link = RDFLinkHTTP.newBuilder()
                    .destination(env.datasetURL())
                    .httpClient(env.httpClientAuthBad())
                    .build()) {
            link.queryAsk("ASK{}");
            }
        });
    }

    @Test
    public void auth_link_bad_auth_2() {
        // 401 (not 403) because we didn't authenticate
        expect401(()->{
            try ( RDFLink link = RDFLinkHTTP.newBuilder()
                    .destination(env.datasetURL())
                    .httpClient(env.httpClientAuthBad())
                    .build()) {
            link.update("INSERT DATA { <x:s> <x:p> <x:o> }");
            }
        });
    }

    @Test
    public void auth_link_bad_auth_3() {
        // 401 (not 403) because we didn't authenticate
        expect401(()->{
            try ( RDFLink link = RDFLinkHTTP.newBuilder()
                        .destination(env.datasetURL())
                        .httpClient(env.httpClientAuthBad())
                        .build()) {
                link.get();
            }
        });
    }

    // RegistryHttpClient

    private void exec_auth_registry_exact(String key) {
        RegistryHttpClient.get().add(key, env.httpClientAuthGood());
        try { exec_register_test(); }
        finally { RegistryHttpClient.get().clear(); }
    }

    private void exec_auth_registry_prefix(String key) {
        RegistryHttpClient.get().addPrefix(key, env.httpClientAuthGood());
        try { exec_register_test(); }
        finally { RegistryHttpClient.get().clear(); }
    }

    private void exec_register_test() {
        try (QueryExec qExec = QueryExecHTTP.newBuilder()
                .endpoint(env.datasetURL())
                .queryString("ASK{  }")
                .build()) {
            boolean b = qExec.ask();
            assertTrue(b);
        }
    }

    @Test
    public void auth_registryHttpClient_exact_1() {
        exec_auth_registry_exact(env.datasetURL());
    }

    @Test
    public void auth_registryHttpClient_exact_2() {
        // Use prefix match for server-wide
        expect401(()->{
            exec_auth_registry_exact(env.serverBaseURL());
        });
    }

    @Test
    public void auth_registryHttpClient_exact_401_1() {
        expect401(()->{
            exec_auth_registry_exact("Junk");
        });
    }

    @Test
    public void auth_registryHttpClient_exact_401_2() {
        // Longer name, child
        String registerKey = env.serverPath(env.dsName()+"/XYZ");
        expect401(()->{
            exec_auth_registry_exact(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_exact_401_3() {
        // Longer name.
        // It does not match; prefixes are limited to ending in "/"
        String registerKey = env.serverPath(env.dsName()+"X");
        expect401(()->{
            exec_auth_registry_exact(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_exact_401_4() {
        // Different dataset
        String registerKey = env.serverPath("ABC");
        expect401(()->{
            exec_auth_registry_exact(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_exact_401_5() {
        // Shorter dataset name, prefix of actual one.
        // It does not match.
        String registerKey = env.serverPath("d");
        expect401(()->{
            exec_auth_registry_exact(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_prefix_1() {
        // Should use exact URL
        assertThrows(IllegalArgumentException.class, ()->exec_auth_registry_prefix(env.datasetURL()));
    }

    @Test
    public void auth_registryHttpClient_prefix_2() {
        // Everything at a server. Must end in "/"
        exec_auth_registry_prefix(env.serverBaseURL());
    }

    @Test
    public void auth_registryHttpClient_prefix_401_1() {
        expect401(()->{
            exec_auth_registry_prefix("Junk/");
        });
    }

    @Test
    public void auth_registryHttpClient_prefix_401_2() {
        // Longer name.
        String registerKey = env.serverPath(env.dsName()+"/");
        expect401(()->{
            exec_auth_registry_prefix(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_prefix_401_3() {
        // Longer name.
        String registerKey = env.serverPath(env.dsName()+"/xyz/");
        expect401(()->{
            exec_auth_registry_prefix(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_prefix_401_4() {
        // Different dataset
        String registerKey = env.serverPath("A/");
        expect401(()->{
            exec_auth_registry_prefix(registerKey);
        });
    }

    @Test
    public void auth_registryHttpClient_prefix_401_5() {
        // Does not cause 401. Shorter dataset name, prefix of actual one.
        String registerKey = env.serverPath("d");
        assertThrows(IllegalArgumentException.class, ()->exec_auth_registry_prefix(registerKey));
    }

    // Other ways of setting auth.
    // Using HttpClient is preferred but the basic operations can use
    // ServiceTuning or directly supply the headers.

    // ServiceTuning
    @Test
    public void auth_service_tuning_1_RegistryRequestModifier() {
        // using RegistryRequestModifier
        HttpRequestModifier mods = (params, headers) -> headers.put(HttpNames.hAuthorization, HttpLib.basicAuth(user, password));

        RegistryRequestModifier svcReg = new RegistryRequestModifier();
        svcReg.add(env.datasetURL(), mods);
        ARQ.getContext().put(ARQ.httpRegistryRequestModifer, svcReg);

        try {
            // Component level
            UpdateExecHTTP.newBuilder()
                .endpoint(env.datasetURL())
                .updateString("INSERT DATA { <x:s> <x:p> <x:o> }")
                .build()
                .execute();
            try ( QueryExec qExec = QueryExecHTTP.newBuilder()
                    .endpoint(env.datasetURL())
                    .queryString("ASK{ <x:s> <x:p> <x:o> }")
                    .build()){
                boolean b = qExec.ask();
                assertTrue(b);
            }
        } finally {
            // clear up
            ARQ.getContext().remove(ARQ.httpRegistryRequestModifer);
        }
    }

    @Test
    public void auth_service_tuning_2_HttpRequestModifer() {
        // As HttpRequestModifer
        HttpRequestModifier mods = (params, headers) -> headers.put(HttpNames.hAuthorization, HttpLib.basicAuth(user, password));
        ARQ.getContext().put(ARQ.httpRequestModifer, mods);
        try {
            try ( RDFLink link = RDFLinkHTTP.newBuilder()
                    .destination(env.datasetURL())
                    .build()) {
                link.update("INSERT DATA { <x:s> <x:p> <x:z> }");
                boolean b = link.queryAsk("ASK{ <x:s> <x:p> <x:z> }");
                assertTrue(b);
            }
        } finally {
            // clear up
            ARQ.getContext().remove(ARQ.httpRequestModifer);
        }
    }

    @Test
    public void auth_header_1() {
        Triple triple = SSE.parseTriple("(<x:s> <x:p> <x:o>)");
        Graph graph = GraphFactory.createDefaultGraph();
        graph.add(triple);

        GSP.service(env.datasetURL())
            .httpHeader(HttpNames.hAuthorization, HttpLib.basicAuth(user, password))
            .defaultGraph()
            .POST(graph);
        // By query.
        try ( QueryExec qExec = QueryExecHTTP.newBuilder()
                .endpoint(env.datasetURL())
                .queryString("ASK{ <x:s> <x:p> <x:o> }")
                .httpHeader(HttpNames.hAuthorization, HttpLib.basicAuth(user, password))
                .build()) {
            boolean b = qExec.ask();
            assertTrue(b);
        }
        // By GSP
        Graph graph2 =
            GSP.service(env.datasetURL())
                .httpHeader(HttpNames.hAuthorization, HttpLib.basicAuth(user, password))
                .defaultGraph()
                .GET();
        assertTrue(graph.isIsomorphicWith(graph2));
    }
}
