package com.cloudhandson.vpdbackoffice.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.ForwardedHeaderFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

class TransportSecurityTest {

  private AnnotationConfigWebApplicationContext context;
  private MockMvc mockMvc;

  @BeforeEach
  void setUp() {
    context = new AnnotationConfigWebApplicationContext();
    context.setServletContext(new MockServletContext());
    TestPropertyValues.of(
        "backoffice.security.admin-user=admin",
        "backoffice.security.admin-password=test-password",
        "backoffice.security.guest-enabled=true",
        "backoffice.security.guest-user=guest",
        "backoffice.security.guest-password=guest-password",
        "backoffice.security.require-https=true",
        "backoffice.security.remember-me-enabled=true",
        "backoffice.security.remember-me-key=test-only-remember-me-key",
        "backoffice.security.remember-me-days=14",
        "backoffice.token.max-days=365",
        "backoffice.ords.base-url=https://ords.example.test",
        "backoffice.ords.timeout=10s",
        "backoffice.ai.enabled=false",
        "backoffice.ai.timeout=30s"
    ).applyTo(context);
    context.register(TestWebConfig.class);
    context.refresh();

    mockMvc = MockMvcBuilders.webAppContextSetup(context)
        .addFilters(new ForwardedHeaderFilter())
        .apply(springSecurity())
        .build();
  }

  @AfterEach
  void tearDown() {
    context.close();
  }

  @Test
  void directHttpRequestRedirectsToHttps() throws Exception {
    mockMvc.perform(get("/login"))
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("https://localhost/login"));
  }

  @Test
  void forwardedHttpsRequestIsAcceptedAndReturnsHsts() throws Exception {
    mockMvc.perform(get("/login")
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().isOk())
        .andExpect(header().string(
            "Strict-Transport-Security",
            containsString("max-age=31536000")
        ));
  }

  @Test
  void forwardedHostIsUsedForAuthenticationRedirect() throws Exception {
    mockMvc.perform(get("/")
            .accept(MediaType.TEXT_HTML)
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("https://admin.example.test/login"));
  }

  @Test
  void rememberMeIsIssuedOnlyWhenTheLoginCheckboxIsSelected() throws Exception {
    mockMvc.perform(post("/login")
            .with(csrf())
            .param("username", "admin")
            .param("password", "test-password")
            .param("remember-me", "true")
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().is3xxRedirection())
        .andExpect(cookie().exists("VPD_REMEMBER_ME"))
        .andExpect(cookie().secure("VPD_REMEMBER_ME", true))
        .andExpect(cookie().httpOnly("VPD_REMEMBER_ME", true));
  }

  @Test
  void guestLoginSucceedsWhenConfigured() throws Exception {
    mockMvc.perform(post("/login")
            .with(csrf())
            .param("username", "guest")
            .param("password", "guest-password")
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("https://admin.example.test/"));
  }

  @Test
  void viewerCanReadButCannotPostMutations() throws Exception {
    mockMvc.perform(get("/")
            .with(user("guest").roles("VIEWER"))
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().isOk());

    mockMvc.perform(post("/mutation")
            .with(csrf())
            .with(user("guest").roles("VIEWER"))
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().isForbidden());
  }

  @Test
  void viewerCanPostReadOnlyExecutionForms() throws Exception {
    mockMvc.perform(post("/probe")
            .with(csrf())
            .with(user("guest").roles("VIEWER"))
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().isOk());
  }

  @Test
  void adminCanPostMutations() throws Exception {
    mockMvc.perform(post("/mutation")
            .with(csrf())
            .with(user("admin").roles("ADMIN"))
            .header("X-Forwarded-Proto", "https")
            .header("X-Forwarded-Host", "admin.example.test"))
        .andExpect(status().isOk());
  }

  @Test
  void productionSessionCookieSettingsAreBound() throws Exception {
    var environment = new StandardEnvironment();
    environment.getPropertySources().addFirst(new MapPropertySource("test-overrides", Map.of(
        "BACKOFFICE_FORWARD_HEADERS_STRATEGY", "framework",
        "BACKOFFICE_SESSION_COOKIE_SECURE", "true"
    )));
    var loader = new YamlPropertySourceLoader();
    for (var source : loader.load("application", new ClassPathResource("application.yml"))) {
      environment.getPropertySources().addLast(source);
    }

    ServerProperties properties = Binder.get(environment)
        .bind("server", ServerProperties.class)
        .orElseThrow(() -> new AssertionError("server properties were not bound"));
    var cookie = properties.getServlet().getSession().getCookie();

    assertThat(cookie.getSecure()).isTrue();
    assertThat(cookie.getHttpOnly()).isTrue();
    assertThat(cookie.getSameSite().attributeValue()).isEqualTo("Lax");
    assertThat(properties.getForwardHeadersStrategy().name()).isEqualTo("FRAMEWORK");
  }

  @Configuration(proxyBeanMethods = false)
  @EnableWebMvc
  @EnableWebSecurity
  @EnableConfigurationProperties(BackofficeProperties.class)
  @Import({SecurityConfig.class, TestController.class})
  static class TestWebConfig {
  }

  @RestController
  static class TestController {

    @GetMapping("/login")
    String login() {
      return "login";
    }

    @GetMapping("/")
    String home() {
      return "home";
    }

    @PostMapping("/mutation")
    String mutation() {
      return "changed";
    }

    @PostMapping("/probe")
    String probe() {
      return "queried";
    }
  }
}
