package de.effigenix.infrastructure.usermanagement.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.usermanagement.web.dto.LoginRequest;
import de.effigenix.infrastructure.usermanagement.web.dto.LoginResponse;
import de.effigenix.infrastructure.usermanagement.web.dto.RefreshTokenRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration tests for Authentication Controller.
 */
@DisplayName("Authentication Controller Integration Tests")
class AuthControllerIntegrationTest extends AbstractIntegrationTest {

    private String validUsername;
    private String validUserId;
    private String validRefreshToken;

    @BeforeEach
    void setUp() {
        validUsername = "auth.test.user";

        String adminRoleId = createRole(RoleName.ADMIN, "Admin role");

        validUserId = createUser(validUsername, "auth.test@example.com",
                Set.of(adminRoleId), "BRANCH-TEST-001");

        validRefreshToken = generateRefreshToken(validUserId, validUsername);
    }

    // ==================== LOGIN TESTS ====================

    @Test
    @DisplayName("Login with valid credentials should return 200 with JWT tokens")
    void testLoginWithValidCredentials() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        MvcResult result = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.accessToken").isNotEmpty())
                .andExpect(jsonPath("$.tokenType").value("Bearer"))
                .andExpect(jsonPath("$.expiresIn").isNumber())
                .andExpect(jsonPath("$.expiresAt").isNotEmpty())
                .andExpect(jsonPath("$.refreshToken").isNotEmpty())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);

        assertThat(response.accessToken()).isNotBlank();
        assertThat(response.refreshToken()).isNotBlank();
        assertThat(response.expiresIn()).isEqualTo(jwtExpiration / 1000);
    }

    @Test
    @DisplayName("Login with invalid username should return 401 Unauthorized")
    void testLoginWithInvalidUsername() throws Exception {
        LoginRequest request = new LoginRequest("non.existent.user", TEST_PASSWORD);

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code").exists())
                .andReturn();
    }

    @Test
    @DisplayName("Login with invalid password should return 401 Unauthorized")
    void testLoginWithInvalidPassword() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, "WrongPassword123");

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Login with locked user should return 401 Unauthorized")
    void testLoginWithLockedUser() throws Exception {
        jdbc.sql("UPDATE users SET status = 'LOCKED' WHERE id = ?")
                .param(validUserId).update();

        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Login with inactive user should return 401 Unauthorized")
    void testLoginWithInactiveUser() throws Exception {
        jdbc.sql("UPDATE users SET status = 'INACTIVE' WHERE id = ?")
                .param(validUserId).update();

        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Login with missing username should return 400 Bad Request")
    void testLoginWithMissingUsername() throws Exception {
        LoginRequest request = new LoginRequest("", TEST_PASSWORD);

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Login with missing password should return 400 Bad Request")
    void testLoginWithMissingPassword() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, "");

        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Login with missing both fields should return 400 Bad Request")
    void testLoginWithMissingBothFields() throws Exception {
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    // ==================== LOGOUT TESTS ====================

    @Test
    @DisplayName("Logout with valid JWT token should return 204 No Content")
    void testLogoutWithValidToken() throws Exception {
        // First login to get a valid token
        LoginRequest loginRequest = new LoginRequest(validUsername, TEST_PASSWORD);
        MvcResult loginResult = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(loginRequest)))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = loginResult.getResponse().getContentAsString();
        LoginResponse loginResponse = objectMapper.readValue(responseBody, LoginResponse.class);
        String accessToken = loginResponse.accessToken();

        // Now logout with the token
        mockMvc.perform(post("/api/auth/logout")
                        .header("Authorization", "Bearer " + accessToken))
                .andExpect(status().isNoContent())
                .andReturn();
    }

    @Test
    @DisplayName("Logout without token should return 401 Unauthorized")
    void testLogoutWithoutToken() throws Exception {
        mockMvc.perform(post("/api/auth/logout"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Logout with invalid token should return 401 Unauthorized")
    void testLogoutWithInvalidToken() throws Exception {
        String invalidToken = "invalid.token.here";

        mockMvc.perform(post("/api/auth/logout")
                        .header("Authorization", "Bearer " + invalidToken))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Logout with malformed Authorization header should return 401 Unauthorized")
    void testLogoutWithMalformedAuthHeader() throws Exception {
        mockMvc.perform(post("/api/auth/logout")
                        .header("Authorization", "InvalidHeader"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== REFRESH TOKEN TESTS ====================

    @Test
    @DisplayName("Refresh token with valid refresh token should return 401 (not yet implemented)")
    void testRefreshTokenWithValidToken() throws Exception {
        RefreshTokenRequest request = new RefreshTokenRequest(validRefreshToken);

        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Refresh token with invalid refresh token should return 401 Unauthorized")
    void testRefreshTokenWithInvalidToken() throws Exception {
        RefreshTokenRequest request = new RefreshTokenRequest("invalid.refresh.token");

        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Refresh token with missing token should return 400 Bad Request")
    void testRefreshTokenWithMissingToken() throws Exception {
        RefreshTokenRequest request = new RefreshTokenRequest("");

        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Refresh token with null token should return 400 Bad Request")
    void testRefreshTokenWithNullToken() throws Exception {
        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Refresh token endpoint should not require authentication")
    void testRefreshTokenIsPublic() throws Exception {
        RefreshTokenRequest request = new RefreshTokenRequest(validRefreshToken);

        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== JWT TOKEN VALIDATION TESTS ====================

    @Test
    @DisplayName("JWT token should contain valid payload information")
    void testJWTTokenContainsValidPayload() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        MvcResult result = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);
        String accessToken = response.accessToken();

        // Verify token structure (contains 3 parts separated by dots)
        String[] parts = accessToken.split("\\.");
        assertThat(parts).hasSize(3);
    }

    @Test
    @DisplayName("Token expiration time should match configured expiration")
    void testTokenExpirationTime() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        MvcResult result = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);

        // Verify expiration time is approximately correct (allowing 5 second margin)
        long expectedExpiration = jwtExpiration / 1000;
        long actualExpiration = response.expiresIn();
        assertThat(actualExpiration).isBetween(expectedExpiration - 5, expectedExpiration + 5);
    }

    @Test
    @DisplayName("Multiple logins should return different tokens")
    void testMultipleLoginsReturnDifferentTokens() throws Exception {
        LoginRequest request = new LoginRequest(validUsername, TEST_PASSWORD);

        // First login
        MvcResult result1 = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody1 = result1.getResponse().getContentAsString();
        LoginResponse response1 = objectMapper.readValue(responseBody1, LoginResponse.class);

        // Wait to ensure different iat claim (JWT timestamps are second-precision)
        Thread.sleep(1100);

        // Second login
        MvcResult result2 = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody2 = result2.getResponse().getContentAsString();
        LoginResponse response2 = objectMapper.readValue(responseBody2, LoginResponse.class);

        // Tokens should be different
        assertThat(response1.accessToken()).isNotEqualTo(response2.accessToken());
    }
}
