package org.raddatz.familienarchiv.auth;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.AppUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import software.amazon.awssdk.services.s3.S3Client;

import java.io.IOException;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class AuthSessionIntegrationTest {

    @LocalServerPort int port;
    @MockitoBean S3Client s3Client;
    @Autowired AppUserRepository userRepository;
    @Autowired PasswordEncoder passwordEncoder;
    @Autowired JdbcTemplate jdbcTemplate;

    private RestTemplate http;
    private String baseUrl;

    private static final String TEST_EMAIL = "session-it@test.de";
    private static final String TEST_PASSWORD = "pass4Session!";

    @BeforeEach
    void setUp() {
        http = noThrowRestTemplate();
        baseUrl = "http://localhost:" + port;
        // spring_session_attributes cascades on delete — removing the parent row is enough
        jdbcTemplate.update("DELETE FROM spring_session");
        jdbcTemplate.update("DELETE FROM app_users WHERE email = ?", TEST_EMAIL);
        userRepository.save(AppUser.builder()
                .email(TEST_EMAIL)
                .password(passwordEncoder.encode(TEST_PASSWORD))
                .build());
    }

    // ─── Task 13: full session lifecycle ──────────────────────────────────────

    @Test
    void login_sets_opaque_fa_session_cookie() {
        String xsrf = fetchXsrfToken();
        ResponseEntity<String> response = doLogin(xsrf);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        String cookie = extractFaSessionCookie(response);
        assertThat(cookie).isNotBlank();
        // Opaque token — must not look like Basic-auth credentials (email:password)
        assertThat(cookie).doesNotContain(":");
    }

    @Test
    void session_cookie_authenticates_subsequent_request() {
        String xsrf = fetchXsrfToken();
        String cookie = extractFaSessionCookie(doLogin(xsrf));

        ResponseEntity<String> me = http.exchange(
                baseUrl + "/api/users/me", HttpMethod.GET,
                new HttpEntity<>(cookieHeaders(cookie)), String.class);

        assertThat(me.getStatusCode().value()).isEqualTo(200);
    }

    @Test
    void logout_invalidates_session_and_cookie_returns_401_on_reuse() {
        String xsrf = fetchXsrfToken();
        String sessionCookie = extractFaSessionCookie(doLogin(xsrf));

        ResponseEntity<Void> logout = http.postForEntity(
                baseUrl + "/api/auth/logout",
                new HttpEntity<>(csrfAndSessionHeaders(sessionCookie, xsrf)), Void.class);
        assertThat(logout.getStatusCode().value()).isEqualTo(204);

        ResponseEntity<String> me = http.exchange(
                baseUrl + "/api/users/me", HttpMethod.GET,
                new HttpEntity<>(cookieHeaders(sessionCookie)), String.class);
        assertThat(me.getStatusCode().value()).isEqualTo(401);
    }

    // ─── Task 14: idle-timeout ────────────────────────────────────────────────

    @Test
    void session_expired_by_idle_timeout_returns_401() {
        String xsrf = fetchXsrfToken();
        String cookie = extractFaSessionCookie(doLogin(xsrf));

        // Backdate LAST_ACCESS_TIME by 9 hours so lastAccess + maxInactiveInterval(8h) < now
        long nineHoursAgoMs = System.currentTimeMillis() - 9L * 3600 * 1000;
        jdbcTemplate.update(
                "UPDATE spring_session SET LAST_ACCESS_TIME = ?, EXPIRY_TIME = ?",
                nineHoursAgoMs, nineHoursAgoMs);

        ResponseEntity<String> me = http.exchange(
                baseUrl + "/api/users/me", HttpMethod.GET,
                new HttpEntity<>(cookieHeaders(cookie)), String.class);
        assertThat(me.getStatusCode().value()).isEqualTo(401);
    }

    // ─── Task: CSRF rejection at integration layer ────────────────────────────

    @Test
    void post_without_csrf_token_returns_403_CSRF_TOKEN_MISSING() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        // Deliberately omit XSRF-TOKEN cookie and X-XSRF-TOKEN header
        ResponseEntity<String> response = http.postForEntity(
                baseUrl + "/api/auth/logout",
                new HttpEntity<>("{}", headers), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(403);
        assertThat(response.getBody()).contains("CSRF_TOKEN_MISSING");
    }

    // ─── helpers ─────────────────────────────────────────────────────────────

    /**
     * Generates an XSRF token for use in integration tests.
     * CookieCsrfTokenRepository validates that Cookie: XSRF-TOKEN=X matches X-XSRF-TOKEN: X.
     * By supplying both with the same value we simulate exactly what a browser does.
     */
    private String fetchXsrfToken() {
        return java.util.UUID.randomUUID().toString();
    }

    private ResponseEntity<String> doLogin(String xsrfToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Cookie", "XSRF-TOKEN=" + xsrfToken);
        headers.set("X-XSRF-TOKEN", xsrfToken);
        String body = "{\"email\":\"" + TEST_EMAIL + "\",\"password\":\"" + TEST_PASSWORD + "\"}";
        return http.postForEntity(baseUrl + "/api/auth/login",
                new HttpEntity<>(body, headers), String.class);
    }

    private HttpHeaders cookieHeaders(String sessionId) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cookie", "fa_session=" + sessionId);
        return headers;
    }

    private HttpHeaders csrfAndSessionHeaders(String sessionId, String xsrfToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cookie", "fa_session=" + sessionId + "; XSRF-TOKEN=" + xsrfToken);
        headers.set("X-XSRF-TOKEN", xsrfToken);
        return headers;
    }

    private String extractFaSessionCookie(ResponseEntity<?> response) {
        List<String> setCookieHeader = response.getHeaders().get("Set-Cookie");
        if (setCookieHeader == null) return "";
        return setCookieHeader.stream()
                .filter(c -> c.startsWith("fa_session="))
                .map(c -> c.split(";")[0].substring("fa_session=".length()))
                .findFirst()
                .orElse("");
    }


    private RestTemplate noThrowRestTemplate() {
        RestTemplate template = new RestTemplate();
        template.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public boolean hasError(ClientHttpResponse response) throws IOException {
                return false;
            }
        });
        return template;
    }
}
