package de.effigenix.infrastructure.usermanagement.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.audit.AuditLogEntity;
import de.effigenix.infrastructure.audit.AuditLogJpaRepository;
import de.effigenix.infrastructure.usermanagement.web.dto.CreateUserRequest;
import de.effigenix.infrastructure.usermanagement.web.dto.LoginRequest;
import de.effigenix.infrastructure.usermanagement.web.dto.UpdateUserRequest;
import de.effigenix.application.usermanagement.AuditEvent;
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.test.web.servlet.MvcResult;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
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 Security and Authorization.
 */
@DisplayName("Security and Authorization Integration Tests")
class SecurityIntegrationTest extends AbstractIntegrationTest {

    @Autowired
    private AuditLogJpaRepository auditLogRepository;

    private String adminToken;
    private String regularUserToken;
    private String expiredToken;
    private String malformedToken;

    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("security.admin", "admin@security.test",
                Set.of(adminRoleId), "BRANCH-ADMIN");

        regularUserId = createUser("security.user", "user@security.test",
                Set.of(userRoleId), "BRANCH-USER");

        adminToken = generateTestJWT(adminUserId, "security.admin", true);
        regularUserToken = generateTestJWT(regularUserId, "security.user", false);
        expiredToken = generateExpiredToken(adminUserId, "security.admin");
        malformedToken = "not.a.valid.jwt.token";
    }

    // ==================== AUTHORIZATION TESTS ====================

    @Test
    @DisplayName("ADMIN-only endpoint with admin token should return 201")
    void testAdminEndpointWithAdminRole() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "test.new.user",
                "test.new@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())
                .andReturn();
    }

    @Test
    @DisplayName("ADMIN-only endpoint with non-admin token should return 403 Forbidden")
    void testAdminEndpointWithoutAdminRole() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
                "test.new.user",
                "test.new@example.com",
                "SecurePass123!",
                Set.of(RoleName.PRODUCTION_WORKER),
                "BRANCH-001"
        );

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

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

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

    @Test
    @DisplayName("Assign role endpoint with non-admin token should return 403 Forbidden")
    void testAssignRoleWithoutAdminRole() throws Exception {
        mockMvc.perform(post("/api/users/{id}/roles", regularUserId)
                        .header("Authorization", "Bearer " + regularUserToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"roleName\":\"ADMIN\"}"))
                .andExpect(status().isForbidden())
                .andReturn();
    }

    @Test
    @DisplayName("Remove role endpoint with non-admin token should return 403 Forbidden")
    void testRemoveRoleWithoutAdminRole() throws Exception {
        mockMvc.perform(delete("/api/users/{id}/roles/{roleName}", regularUserId, "PRODUCTION_WORKER")
                        .header("Authorization", "Bearer " + regularUserToken))
                .andExpect(status().isForbidden())
                .andReturn();
    }

    // ==================== AUTHENTICATED ENDPOINT TESTS ====================

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

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

    @Test
    @DisplayName("Authenticated endpoint with expired token should return 401 Unauthorized")
    void testAuthenticatedEndpointWithExpiredToken() throws Exception {
        mockMvc.perform(get("/api/users")
                        .header("Authorization", "Bearer " + expiredToken))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Authenticated endpoint with malformed Authorization header should return 401")
    void testAuthenticatedEndpointWithMalformedHeader() throws Exception {
        mockMvc.perform(get("/api/users")
                        .header("Authorization", "InvalidHeaderFormat"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    @Test
    @DisplayName("Authenticated endpoint with valid token should return success")
    void testAuthenticatedEndpointWithValidToken() throws Exception {
        mockMvc.perform(get("/api/users")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andReturn();
    }

    // ==================== PUBLIC ENDPOINT TESTS ====================

    @Test
    @DisplayName("Login endpoint without authentication should return 401 or 200 depending on credentials")
    void testLoginEndpointIsPublic() throws Exception {
        LoginRequest request = new LoginRequest("security.admin", TEST_PASSWORD);

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

    @Test
    @DisplayName("Refresh token endpoint without authentication should be public")
    void testRefreshEndpointIsPublic() throws Exception {
        String refreshToken = generateRefreshToken(adminUserId, "security.admin");

        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"refreshToken\":\"" + refreshToken + "\"}"))
                .andExpect(status().isUnauthorized())
                .andReturn();
    }

    // ==================== BRANCH-BASED FILTERING TESTS ====================

    @Test
    @DisplayName("Users should see data filtered by their branch (if filtering is implemented)")
    void testBranchBasedDataVisibility() throws Exception {
        String otherBranchUserId = createUser("other.branch.user", "other@branch.test",
                Set.of(userRoleId), "BRANCH-OTHER");

        mockMvc.perform(get("/api/users")
                        .header("Authorization", "Bearer " + regularUserToken))
                .andExpect(status().isOk())
                .andReturn();
    }

    // ==================== AUDIT LOGGING TESTS ====================

    @Test
    @DisplayName("Create user operation should create audit log entry")
    void testCreateUserAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

        CreateUserRequest request = new CreateUserRequest(
                "audit.test.user",
                "audit@test.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())
                .andReturn();

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.USER_CREATED)
                .isNotEmpty();

        AuditLogEntity auditLog = logs.stream()
                .filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
                .findFirst()
                .orElseThrow();

        assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
        assertThat(auditLog.getTimestamp()).isNotNull();
        assertThat(auditLog.getEvent()).isEqualTo(AuditEvent.USER_CREATED);
    }

    @Test
    @DisplayName("Audit log should contain actor information")
    void testAuditLogContainsActor() throws Exception {
        auditLogRepository.deleteAll();

        CreateUserRequest request = new CreateUserRequest(
                "audit.actor.test",
                "audit.actor@test.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())
                .andReturn();

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        AuditLogEntity auditLog = logs.stream()
                .filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
                .findFirst()
                .orElseThrow();

        assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
        assertThat(auditLog.getPerformedBy()).isNotBlank();
    }

    @Test
    @DisplayName("Audit log should contain timestamp")
    void testAuditLogContainsTimestamp() throws Exception {
        auditLogRepository.deleteAll();

        CreateUserRequest request = new CreateUserRequest(
                "audit.timestamp.test",
                "audit.timestamp@test.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())
                .andReturn();

        // Wait for async audit log to be persisted
        Thread.sleep(500);

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        AuditLogEntity auditLog = logs.stream()
                .filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
                .findFirst()
                .orElseThrow();

        assertThat(auditLog.getTimestamp()).isNotNull();
        assertThat(auditLog.getTimestamp()).isAfter(OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(1));
    }

    @Test
    @DisplayName("Lock user operation should create audit log entry")
    void testLockUserAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

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

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.USER_LOCKED)
                .isNotEmpty();

        AuditLogEntity auditLog = logs.stream()
                .filter(log -> log.getEvent() == AuditEvent.USER_LOCKED)
                .findFirst()
                .orElseThrow();

        assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
        assertThat(auditLog.getEntityId()).isEqualTo(regularUserId);
    }

    @Test
    @DisplayName("Unlock user operation should create audit log entry")
    void testUnlockUserAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

        // Lock the user first via API
        mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        auditLogRepository.deleteAll();

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

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.USER_UNLOCKED)
                .isNotEmpty();
    }

    @Test
    @DisplayName("Update user operation should create audit log entry")
    void testUpdateUserAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

        UpdateUserRequest request = new UpdateUserRequest(
                "updated.audit@example.com",
                "BRANCH-UPDATED"
        );

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

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.USER_UPDATED)
                .isNotEmpty();

        AuditLogEntity auditLog = logs.stream()
                .filter(log -> log.getEvent() == AuditEvent.USER_UPDATED)
                .findFirst()
                .orElseThrow();

        assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
        assertThat(auditLog.getEntityId()).isEqualTo(regularUserId);
    }

    @Test
    @DisplayName("Login success should create audit log entry")
    void testLoginSuccessAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

        LoginRequest request = new LoginRequest("security.admin", TEST_PASSWORD);

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

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.LOGIN_SUCCESS)
                .isNotEmpty();
    }

    @Test
    @DisplayName("Login failure should create audit log entry")
    void testLoginFailureAuditLogging() throws Exception {
        auditLogRepository.deleteAll();

        LoginRequest request = new LoginRequest("security.admin", "WrongPassword123");

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

        List<AuditLogEntity> logs = auditLogRepository.findAll();
        assertThat(logs)
                .filteredOn(log -> log.getEvent() == AuditEvent.LOGIN_FAILED)
                .isNotEmpty();
    }

    // ==================== 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);
    }
}
