package org.raddatz.familienarchiv.user;

import tools.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.user.CreateInviteRequest;
import org.raddatz.familienarchiv.user.InviteListItemDTO;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.InviteToken;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.user.InviteService;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import org.mockito.ArgumentCaptor;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(InviteController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class InviteControllerTest {

    @Autowired MockMvc mockMvc;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @MockitoBean InviteService inviteService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private InviteListItemDTO makeInviteDTO(UUID id, String code) {
        return InviteListItemDTO.builder()
                .id(id)
                .code(code)
                .displayCode(InviteService.formatDisplayCode(code))
                .useCount(0)
                .revoked(false)
                .status("active")
                .createdAt(LocalDateTime.now())
                .shareableUrl("http://localhost:3000/register?code=" + code)
                .build();
    }

    // ─── GET /api/invites ─────────────────────────────────────────────────────

    @Test
    void listInvites_returns403_whenUserLacksAdminUserPermission() throws Exception {
        mockMvc.perform(get("/api/invites"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "user@test.com")
    void listInvites_returns403_whenAuthenticatedWithoutPermission() throws Exception {
        mockMvc.perform(get("/api/invites"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin@test.com", authorities = {"ADMIN_USER"})
    void listInvites_returns200_withActiveInvites_byDefault() throws Exception {
        UUID id = UUID.randomUUID();
        when(inviteService.listInvites(eq(true), anyString()))
                .thenReturn(List.of(makeInviteDTO(id, "ABCDE12345")));

        mockMvc.perform(get("/api/invites"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].code").value("ABCDE12345"))
                .andExpect(jsonPath("$[0].status").value("active"));
    }

    @Test
    @WithMockUser(username = "admin@test.com", authorities = {"ADMIN_USER"})
    void listInvites_returns200_withAllInvites_whenStatusAll() throws Exception {
        UUID id = UUID.randomUUID();
        InviteListItemDTO revoked = makeInviteDTO(id, "REVOKED1234");
        when(inviteService.listInvites(eq(false), anyString()))
                .thenReturn(List.of(revoked));

        mockMvc.perform(get("/api/invites").param("status", "all"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].code").value("REVOKED1234"));
    }

    // ─── POST /api/invites ────────────────────────────────────────────────────

    @Test
    void createInvite_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/invites").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "user@test.com")
    void createInvite_returns403_whenUserLacksAdminUserPermission() throws Exception {
        mockMvc.perform(post("/api/invites").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin@test.com", authorities = {"ADMIN_USER"})
    void createInvite_returns201_withCreatedInvite() throws Exception {
        AppUser admin = AppUser.builder().id(UUID.randomUUID()).email("admin@test.com").build();
        when(userService.findByEmail("admin@test.com")).thenReturn(admin);

        InviteToken savedToken = InviteToken.builder()
                .id(UUID.randomUUID())
                .code("NEWCODE123")
                .label("Für Familie")
                .maxUses(1)
                .useCount(0)
                .build();
        when(inviteService.createInvite(any(), eq(admin))).thenReturn(savedToken);

        UUID id = savedToken.getId();
        InviteListItemDTO dto = makeInviteDTO(id, "NEWCODE123");
        dto.setLabel("Für Familie");
        when(inviteService.toListItemDTO(eq(savedToken), anyString())).thenReturn(dto);

        CreateInviteRequest req = new CreateInviteRequest();
        req.setLabel("Für Familie");
        req.setMaxUses(1);

        mockMvc.perform(post("/api/invites").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value("NEWCODE123"))
                .andExpect(jsonPath("$.label").value("Für Familie"));
    }

    @Test
    @WithMockUser(username = "admin@test.com", authorities = {"ADMIN_USER"})
    void createInvite_forwardsGroupIdsToService() throws Exception {
        UUID groupId = UUID.randomUUID();
        AppUser admin = AppUser.builder().id(UUID.randomUUID()).email("admin@test.com").build();
        when(userService.findByEmail("admin@test.com")).thenReturn(admin);

        InviteToken savedToken = InviteToken.builder()
                .id(UUID.randomUUID()).code("ABCDE12345").useCount(0).build();
        when(inviteService.createInvite(any(), eq(admin))).thenReturn(savedToken);
        when(inviteService.toListItemDTO(any(), anyString()))
                .thenReturn(makeInviteDTO(savedToken.getId(), "ABCDE12345"));

        String body = "{\"groupIds\":[\"" + groupId + "\"]}";
        mockMvc.perform(post("/api/invites").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isCreated());

        ArgumentCaptor<CreateInviteRequest> captor = ArgumentCaptor.forClass(CreateInviteRequest.class);
        verify(inviteService).createInvite(captor.capture(), eq(admin));
        assertThat(captor.getValue().getGroupIds()).containsExactly(groupId);
    }

    // ─── DELETE /api/invites/{id} ─────────────────────────────────────────────

    @Test
    void revokeInvite_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(delete("/api/invites/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "user@test.com")
    void revokeInvite_returns403_whenUserLacksAdminUserPermission() throws Exception {
        mockMvc.perform(delete("/api/invites/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin@test.com", authorities = {"ADMIN_USER"})
    void revokeInvite_returns204_whenSuccessful() throws Exception {
        UUID id = UUID.randomUUID();

        mockMvc.perform(delete("/api/invites/" + id).with(csrf()))
                .andExpect(status().isNoContent());

        verify(inviteService).revokeInvite(id);
    }
}
