package study.reactivesecurity;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.junit.jupiter.api.Assertions.assertEquals;

@DirtiesContext(classMode=DirtiesContext.ClassMode.AFTER_CLASS)
@SpringBootTest(classes=SecurityLab.class, webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties={"server.address=127.0.0.1", "spring.main.web-application-type=reactive"})
class ReactiveHttpTest {
    @LocalServerPort int port;
    @Test void missingCredentialsCannotReachProtectedResource() throws Exception {
        try (var client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build()) {
            var response=client.send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:"+port+"/api/who"))
                .timeout(Duration.ofSeconds(5)).GET().build(), HttpResponse.BodyHandlers.ofString());
            System.out.println("U31OBSERVE missingCredentials status="+response.statusCode());
            assertEquals(401,response.statusCode());
        }
    }
    HttpResponse<String> get(String path, String user, String password) throws Exception {
        try (var client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build()) {
            var request=HttpRequest.newBuilder(URI.create("http://127.0.0.1:"+port+path)).timeout(Duration.ofSeconds(5));
            if(user!=null) request.header("Authorization","Basic "+Base64.getEncoder()
                .encodeToString((user+":"+password).getBytes(StandardCharsets.UTF_8)));
            var response=client.send(request.GET().build(),HttpResponse.BodyHandlers.ofString());
            System.out.println("U31OBSERVE http path="+path+" user="+user+" status="+response.statusCode());
            return response;
        }
    }
    @Test void validatedIdentitySurvivesSchedulerSwitchWithoutThreadLocal() throws Exception {
        var response=get("/api/who","alice","lab-password");
        assertEquals(200,response.statusCode());
        assertTrue(response.body().contains("ctx=alice|threadLocal=none"),response.body());
        assertTrue(response.body().contains("boundedElastic"),response.body());
    }

    @Test void wrongPasswordDeniedThenValidAccountRecoversWithoutSession() throws Exception {
        assertEquals(401,get("/api/who","alice","wrong-password").statusCode());
        assertEquals(200,get("/api/who","alice","lab-password").statusCode());
        assertEquals(401,get("/api/who",null,null).statusCode());
        assertEquals(401,get("/api/who","absent","lab-password").statusCode());
        assertEquals(200,get("/api/who","bob","lab-password").statusCode());
    }
    @Test void httpAdmissionDoesNotGrantIndependentMethodPermission() throws Exception {
        assertEquals(403,get("/api/admin","alice","lab-password").statusCode());
        assertEquals(200,get("/api/admin","admin","lab-password").statusCode());
        assertEquals(403,get("/api/method","alice","lab-password").statusCode());
        assertEquals(200,get("/api/method","admin","lab-password").statusCode());
    }

    @Test void concurrentRequestsKeepDistinctValidatedOwnersAndFailureDoesNotLeak() throws Exception {
        var jobs=new java.util.ArrayList<java.util.concurrent.CompletableFuture<Void>>();
        for(int i=0;i<12;i++) {
            final String user=i%2==0?"alice":"bob";
            jobs.add(java.util.concurrent.CompletableFuture.runAsync(()->{
                try {
                    var response=get("/api/who",user,"lab-password");
                    assertEquals(200,response.statusCode());
                    assertTrue(response.body().contains("ctx="+user+"|threadLocal=none"),response.body());
                } catch(Exception e) { throw new RuntimeException(e); }
            }));
        }
        java.util.concurrent.CompletableFuture.allOf(jobs.toArray(java.util.concurrent.CompletableFuture[]::new))
            .get(15,java.util.concurrent.TimeUnit.SECONDS);
        assertEquals(401,get("/api/who",null,null).statusCode());
        assertEquals(200,get("/api/who","alice","lab-password").statusCode());
    }
    @Test void callerCannotReplaceTrustedOwnerUsingRequestParameters() throws Exception {
        assertEquals(200,get("/api/orders/alice?owner=alice","alice","lab-password").statusCode());
        assertEquals(403,get("/api/orders/bob?owner=bob","alice","lab-password").statusCode());
        assertEquals(200,get("/api/orders/bob?owner=alice","bob","lab-password").statusCode());
        assertEquals(200,get("/api/orders/alice","alice","lab-password").statusCode());
    }

}
