package de.effigenix.infrastructure.usermanagement.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.usermanagement.web.dto.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;

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

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

/**
 * Integration tests for User Management Controller.
 */
@DisplayName("User Controller Integration Tests")
class UserControllerIntegrationTest extends AbstractIntegrationTest {

    @Autowired
    private PasswordEncoder passwordEncoder;

    private String adminToken;
    private String regularUserToken;
    private String adminUserId;
    private String regularUserId;

    private String adminRoleId;
    private String userRoleId;

    @BeforeEach
    void setUp() {
        adminRoleId = createRole(RoleName.ADMIN, "Admin role");
        userRoleId = createRole(RoleName.PRODUCTION_WORKER, "Production worker role");

        adminUserId = createUser("admin.user", "admin@example.com", Set.of(adminRoleId), "BRANCH-001");

        regularUserId = createUser("regular.user", "regular@example.com", Set.of(userRoleId), "BRANCH-001");

        adminToken = generateTestJWT(adminUserId, "admin.user", true);
        regularUserToken = generateTestJWT(regularUserId, "regular.user", false);
    }

    // ==================== CREATE USER TESTS ====================

    @Test
    @DisplayName("Create user with valid data should return 201 Created")
    void testCreateUserWithValidData() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "new.user",
                "new.user@example.com",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNotEmpty())
                .andExpect(jsonPath("$.username").value("new.user"))
                .andExpect(jsonPath("$.email").value("new.user@example.com"))
                .andExpect(jsonPath("$.status").value("ACTIVE"))
                .andExpect(jsonPath("$.createdAt").isNotEmpty())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with duplicate username should return 409 Conflict")
    void testCreateUserWithDuplicateUsername() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "admin.user", // Already exists
                "duplicate@example.com",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isConflict())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with duplicate email should return 409 Conflict")
    void testCreateUserWithDuplicateEmail() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "different.user",
                "admin@example.com", // Already exists
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isConflict())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with invalid email should return 400 Bad Request")
    void testCreateUserWithInvalidEmail() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "new.user",
                "invalid-email",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with short password should return 400 Bad Request")
    void testCreateUserWithShortPassword() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "new.user",
                "new.user@example.com",
                "Pass123", // Less than 8 characters
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with short username should return 400 Bad Request")
    void testCreateUserWithShortUsername() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "ab", // Less than 3 characters
                "new.user@example.com",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Create user with missing required fields should return 400 Bad Request")
    void testCreateUserWithMissingFields() throws Exception {
        mockMvc.perform(post("/api/users")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Create user without authentication should return 401 Unauthorized")
    void testCreateUserWithoutAuthentication() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "new.user",
                "new.user@example.com",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

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

    // ==================== LIST USERS TESTS ====================

    @Test
    @DisplayName("List users should return 200 with array of users")
    void testListUsers() throws Exception {
        mockMvc.perform(get("/api/users")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2))))
                .andExpect(jsonPath("$[0].id").isNotEmpty())
                .andExpect(jsonPath("$[0].username").isNotEmpty())
                .andExpect(jsonPath("$[0].email").isNotEmpty())
                .andReturn();
    }

    @Test
    @DisplayName("List users without authentication should return 401 Unauthorized")
    void testListUsersWithoutAuthentication() throws Exception {
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("List users with invalid token should return 401 Unauthorized")
    void testListUsersWithInvalidToken() throws Exception {
        mockMvc.perform(get("/api/users")
                        .header("Authorization", "Bearer invalid.token.here"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== GET USER BY ID TESTS ====================

    @Test
    @DisplayName("Get existing user by ID should return 200 with user data")
    void testGetExistingUserById() throws Exception {
        mockMvc.perform(get("/api/users/{id}", adminUserId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(adminUserId))
                .andExpect(jsonPath("$.username").value("admin.user"))
                .andExpect(jsonPath("$.email").value("admin@example.com"))
                .andExpect(jsonPath("$.status").value("ACTIVE"))
                .andReturn();
    }

    @Test
    @DisplayName("Get non-existent user by ID should return 404 Not Found")
    void testGetNonExistentUserById() throws Exception {
        String nonExistentId = UUID.randomUUID().toString();

        mockMvc.perform(get("/api/users/{id}", nonExistentId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound())
                .andReturn();
    }

    @Test
    @DisplayName("Get user by ID without authentication should return 401 Unauthorized")
    void testGetUserByIdWithoutAuthentication() throws Exception {
        mockMvc.perform(get("/api/users/{id}", adminUserId))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== UPDATE USER TESTS ====================

    @Test
    @DisplayName("Update user with valid data should return 200 with updated user")
    void testUpdateUserWithValidData() throws Exception {
        UpdateUserRequest request = new UpdateUserRequest(
                "updated@example.com",
                "BRANCH-002"
        );

        mockMvc.perform(put("/api/users/{id}", regularUserId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(regularUserId))
                .andExpect(jsonPath("$.email").value("updated@example.com"))
                .andExpect(jsonPath("$.branchId").value("BRANCH-002"))
                .andReturn();
    }

    @Test
    @DisplayName("Update user with duplicate email should return 409 Conflict")
    void testUpdateUserWithDuplicateEmail() throws Exception {
        UpdateUserRequest request = new UpdateUserRequest(
                "admin@example.com", // Already exists
                "BRANCH-002"
        );

        mockMvc.perform(put("/api/users/{id}", regularUserId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isConflict())
                .andReturn();
    }

    @Test
    @DisplayName("Update non-existent user should return 404 Not Found")
    void testUpdateNonExistentUser() throws Exception {
        String nonExistentId = UUID.randomUUID().toString();
        UpdateUserRequest request = new UpdateUserRequest(
                "new@example.com",
                "BRANCH-002"
        );

        mockMvc.perform(put("/api/users/{id}", nonExistentId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isNotFound())
                .andReturn();
    }

    @Test
    @DisplayName("Update user with invalid email should return 400 Bad Request")
    void testUpdateUserWithInvalidEmail() throws Exception {
        UpdateUserRequest request = new UpdateUserRequest(
                "invalid-email",
                "BRANCH-002"
        );

        mockMvc.perform(put("/api/users/{id}", regularUserId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    // ==================== LOCK USER TESTS ====================

    @Test
    @DisplayName("Lock user should return 200 with LOCKED status")
    void testLockUser() throws Exception {
        mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(regularUserId))
                .andExpect(jsonPath("$.status").value("LOCKED"))
                .andReturn();

        // Verify user is actually locked in database
        String lockedStatus = jdbc.sql("SELECT status FROM users WHERE id = ?")
                .param(regularUserId).query(String.class).single();
        assertThat(lockedStatus).isEqualTo("LOCKED");
    }

    @Test
    @DisplayName("Lock non-existent user should return 404 Not Found")
    void testLockNonExistentUser() throws Exception {
        String nonExistentId = UUID.randomUUID().toString();

        mockMvc.perform(post("/api/users/{id}/lock", nonExistentId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound())
                .andReturn();
    }

    @Test
    @DisplayName("Lock user without admin permission should return 403 Forbidden")
    void testLockUserWithoutAdminPermission() throws Exception {
        mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken))
                .andExpect(status().isForbidden())
                .andReturn();
    }

    // ==================== UNLOCK USER TESTS ====================

    @Test
    @DisplayName("Unlock locked user should return 200 with ACTIVE status")
    void testUnlockUser() throws Exception {
        // First lock the user
        jdbc.sql("UPDATE users SET status = 'LOCKED' WHERE id = ?")
                .param(regularUserId).update();

        // Now unlock
        mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("ACTIVE"))
                .andReturn();

        // Verify user is actually unlocked in database
        String unlockedStatus = jdbc.sql("SELECT status FROM users WHERE id = ?")
                .param(regularUserId).query(String.class).single();
        assertThat(unlockedStatus).isEqualTo("ACTIVE");
    }

    @Test
    @DisplayName("Unlock non-existent user should return 404 Not Found")
    void testUnlockNonExistentUser() throws Exception {
        String nonExistentId = UUID.randomUUID().toString();

        mockMvc.perform(post("/api/users/{id}/unlock", nonExistentId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound())
                .andReturn();
    }

    @Test
    @DisplayName("Unlock user without admin permission should return 403 Forbidden")
    void testUnlockUserWithoutAdminPermission() throws Exception {
        mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken))
                .andExpect(status().isForbidden())
                .andReturn();
    }

    // ==================== CHANGE PASSWORD TESTS ====================

    @Test
    @DisplayName("Change password with correct current password should return 204 No Content")
    void testChangePasswordWithValidCurrentPassword() throws Exception {
        ChangePasswordRequest request = new ChangePasswordRequest(
                TEST_PASSWORD, // Current password
                "NewSecurePass456!"
        );

        mockMvc.perform(put("/api/users/{id}/password", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isNoContent())
                .andReturn();

        // Verify password was actually changed
        String passwordHash = jdbc.sql("SELECT password_hash FROM users WHERE id = ?")
                .param(regularUserId).query(String.class).single();
        assertThat(passwordEncoder.matches("NewSecurePass456!", passwordHash)).isTrue();
    }

    @Test
    @DisplayName("Change password with incorrect current password should return 401 Unauthorized")
    void testChangePasswordWithInvalidCurrentPassword() throws Exception {
        ChangePasswordRequest request = new ChangePasswordRequest(
                "WrongPassword123", // Wrong current password
                "NewSecurePass456!"
        );

        mockMvc.perform(put("/api/users/{id}/password", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Change password with new password too short should return 400 Bad Request")
    void testChangePasswordWithShortNewPassword() throws Exception {
        ChangePasswordRequest request = new ChangePasswordRequest(
                TEST_PASSWORD,
                "Short1" // Less than 8 characters
        );

        mockMvc.perform(put("/api/users/{id}/password", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    @DisplayName("Change password for non-existent user should return 403 when not self-service")
    void testChangePasswordForNonExistentUser() throws Exception {
        String nonExistentId = UUID.randomUUID().toString();
        ChangePasswordRequest request = new ChangePasswordRequest(
                "SomePassword123",
                "NewSecurePass456!"
        );

        // Regular user has no PASSWORD_CHANGE permission for other users → 403 before user lookup
        mockMvc.perform(put("/api/users/{id}/password", nonExistentId)
                        .header("Authorization", "Bearer " + regularUserToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isForbidden())
                .andReturn();
    }

    @Test
    @DisplayName("Change password without authentication should return 401 Unauthorized")
    void testChangePasswordWithoutAuthentication() throws Exception {
        ChangePasswordRequest request = new ChangePasswordRequest(
                TEST_PASSWORD,
                "NewSecurePass456!"
        );

        mockMvc.perform(put("/api/users/{id}/password", regularUserId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== HELPER METHODS ====================

    private String generateTestJWT(String userId, String username, boolean isAdmin) {
        String permissions = isAdmin
                ? "USER_READ,USER_WRITE,USER_DELETE,USER_LOCK,USER_UNLOCK,ROLE_READ,ROLE_WRITE,ROLE_ASSIGN,ROLE_REMOVE"
                : "USER_READ";
        return generateToken(userId, username, permissions);
    }
}
