package org.raddatz.familienarchiv.timeline;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
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 java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(TimelineEventController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class TimelineEventControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean TimelineEventService timelineEventService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private static final String VALID_JSON =
            "{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\"}";

    /** Default principal resolution for @WithMockUser's "user"; capture tests override with a known id. */
    @BeforeEach
    void resolveDefaultPrincipal() {
        when(userService.findByEmail("user"))
                .thenReturn(AppUser.builder().id(UUID.randomUUID()).email("user").build());
    }

    private TimelineEventView sampleView() {
        return new TimelineEventView(
                UUID.randomUUID(), "Hochzeit", EventType.PERSONAL, LocalDate.of(1914, 1, 1),
                org.raddatz.familienarchiv.document.DatePrecision.YEAR, null, null, 0L,
                UUID.randomUUID(), LocalDateTime.now(), UUID.randomUUID(), LocalDateTime.now(),
                List.of(), List.of());
    }

    // ─── POST /api/timeline/events ───────────────────────────────────────────

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

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void create_returns403_whenOnlyReadAll() throws Exception {
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON).content(VALID_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns201_andViewBody_whenWriteAll() throws Exception {
        when(timelineEventService.create(any(), any())).thenReturn(sampleView());

        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON).content(VALID_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.title").value("Hochzeit"))
                .andExpect(jsonPath("$.version").value(0));
    }

    // ─── PUT /api/timeline/events/{id} ───────────────────────────────────────

    @Test
    void update_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(put("/api/timeline/events/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON).content(VALID_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void update_returns403_whenOnlyReadAll() throws Exception {
        mockMvc.perform(put("/api/timeline/events/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON).content(VALID_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void update_returns200_andViewBody_whenWriteAll() throws Exception {
        when(timelineEventService.update(any(), any(), any())).thenReturn(sampleView());

        mockMvc.perform(put("/api/timeline/events/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON).content(VALID_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Hochzeit"));
    }

    // ─── DELETE /api/timeline/events/{id} ────────────────────────────────────

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

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void delete_returns403_whenOnlyReadAll() throws Exception {
        mockMvc.perform(delete("/api/timeline/events/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void delete_returns204_whenWriteAll() throws Exception {
        mockMvc.perform(delete("/api/timeline/events/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isNoContent());
    }

    // ─── GET /api/timeline/events/{id} ───────────────────────────────────────

    @Test
    void getEvent_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/timeline/events/" + UUID.randomUUID()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void getEvent_returns200_whenAuthenticated() throws Exception {
        when(timelineEventService.getEvent(any())).thenReturn(sampleView());

        mockMvc.perform(get("/api/timeline/events/" + UUID.randomUUID()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Hochzeit"));
    }

    @Test
    @WithMockUser
    void getEvent_returns404_whenMissing() throws Exception {
        when(timelineEventService.getEvent(any()))
                .thenThrow(DomainException.notFound(ErrorCode.TIMELINE_EVENT_NOT_FOUND, "not found"));

        mockMvc.perform(get("/api/timeline/events/" + UUID.randomUUID()))
                .andExpect(status().isNotFound());
    }

    // ─── service-thrown link errors map to status ────────────────────────────

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns404_whenServiceThrowsPersonNotFound() throws Exception {
        when(timelineEventService.create(any(), any()))
                .thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "One or more person IDs not found"));

        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\",\"personIds\":[\""
                                + UUID.randomUUID() + "\"]}"))
                .andExpect(status().isNotFound());
    }

    // ─── Bean Validation 400s carry code VALIDATION_ERROR (R1) ───────────────

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns400_VALIDATION_ERROR_whenTitleBlank() throws Exception {
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"  \",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns400_VALIDATION_ERROR_whenTypeNull() throws Exception {
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"eventDate\":\"1914-07-28\"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns400_VALIDATION_ERROR_whenDescriptionTooLong() throws Exception {
        String description = "x".repeat(5001);
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\",\"description\":\""
                                + description + "\"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void create_returns400_VALIDATION_ERROR_whenTooManyPersonIds() throws Exception {
        String ids = Stream.generate(() -> "\"" + UUID.randomUUID() + "\"").limit(51).collect(Collectors.joining(","));
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\",\"personIds\":["
                                + ids + "]}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    // ─── actorId is the resolved session principal, not a body field (both write paths) ──

    @Test
    @WithMockUser(username = "curator@example.com", authorities = "WRITE_ALL")
    void create_passesResolvedPrincipalIdAsActor_ignoringBodyCreatedBy() throws Exception {
        UUID principalId = UUID.randomUUID();
        when(userService.findByEmail("curator@example.com"))
                .thenReturn(AppUser.builder().id(principalId).email("curator@example.com").build());
        when(timelineEventService.create(any(), any())).thenReturn(sampleView());

        // body carries a forged createdBy — it must be ignored, actor comes from the principal
        mockMvc.perform(post("/api/timeline/events").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\",\"createdBy\":\""
                                + UUID.randomUUID() + "\"}"))
                .andExpect(status().isCreated());

        verify(timelineEventService).create(any(), eq(principalId));
    }

    @Test
    @WithMockUser(username = "curator@example.com", authorities = "WRITE_ALL")
    void update_passesResolvedPrincipalIdAsActor_ignoringBodyUpdatedBy() throws Exception {
        UUID principalId = UUID.randomUUID();
        UUID eventId = UUID.randomUUID();
        when(userService.findByEmail("curator@example.com"))
                .thenReturn(AppUser.builder().id(principalId).email("curator@example.com").build());
        when(timelineEventService.update(any(), any(), any())).thenReturn(sampleView());

        mockMvc.perform(put("/api/timeline/events/" + eventId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"Hochzeit\",\"type\":\"PERSONAL\",\"eventDate\":\"1914-07-28\",\"updatedBy\":\""
                                + UUID.randomUUID() + "\"}"))
                .andExpect(status().isOk());

        verify(timelineEventService).update(eq(eventId), any(), eq(principalId));
    }
}
