package org.raddatz.familienarchiv.geschichte;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.geschichte.GeschichteUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.geschichte.Geschichte;
import org.raddatz.familienarchiv.geschichte.GeschichteStatus;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.geschichte.GeschichteService;
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.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
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(GeschichteController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class GeschichteControllerTest {

    @Autowired
    MockMvc mockMvc;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @MockitoBean
    GeschichteService geschichteService;

    @MockitoBean
    CustomUserDetailsService customUserDetailsService;

    // ─── GET /api/geschichten ────────────────────────────────────────────────

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

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void list_returns200_forReader() throws Exception {
        when(geschichteService.list(any(), any(), any(), anyInt()))
                .thenReturn(List.of(published(UUID.randomUUID(), "Story A")));

        mockMvc.perform(get("/api/geschichten"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value("Story A"));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void list_passesSinglePersonIdFilterToServiceAsListOfOne() throws Exception {
        UUID personId = UUID.randomUUID();
        when(geschichteService.list(any(), eq(List.of(personId)), any(), anyInt()))
                .thenReturn(List.of());

        mockMvc.perform(get("/api/geschichten").param("personId", personId.toString()))
                .andExpect(status().isOk());

        verify(geschichteService).list(any(), eq(List.of(personId)), any(), anyInt());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void list_passesRepeatedPersonIdParamsAsListForAndFilter() throws Exception {
        UUID a = UUID.randomUUID();
        UUID b = UUID.randomUUID();
        when(geschichteService.list(any(), eq(List.of(a, b)), any(), anyInt()))
                .thenReturn(List.of());

        mockMvc.perform(get("/api/geschichten")
                        .param("personId", a.toString())
                        .param("personId", b.toString()))
                .andExpect(status().isOk());

        verify(geschichteService).list(any(), eq(List.of(a, b)), any(), anyInt());
    }

    // ─── GET /api/geschichten/{id} ───────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getById_returns200_whenFound() throws Exception {
        UUID id = UUID.randomUUID();
        when(geschichteService.getById(id)).thenReturn(published(id, "Hello"));

        mockMvc.perform(get("/api/geschichten/{id}", id))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(id.toString()))
                .andExpect(jsonPath("$.title").value("Hello"));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getById_returns404_whenServiceThrowsNotFound() throws Exception {
        UUID id = UUID.randomUUID();
        when(geschichteService.getById(id))
                .thenThrow(DomainException.notFound(ErrorCode.GESCHICHTE_NOT_FOUND, "x"));

        mockMvc.perform(get("/api/geschichten/{id}", id))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("GESCHICHTE_NOT_FOUND"));
    }

    // ─── POST /api/geschichten ───────────────────────────────────────────────

    @Test
    void create_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/geschichten").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"x\"}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void create_returns403_whenLackingBlogWrite() throws Exception {
        mockMvc.perform(post("/api/geschichten").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"x\"}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "BLOG_WRITE")
    void create_returns201_withBlogWrite() throws Exception {
        UUID id = UUID.randomUUID();
        when(geschichteService.create(any(GeschichteUpdateDTO.class)))
                .thenReturn(draft(id, "New"));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("New");

        mockMvc.perform(post("/api/geschichten").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(id.toString()));
    }

    // ─── PATCH /api/geschichten/{id} ─────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void update_returns403_whenLackingBlogWrite() throws Exception {
        mockMvc.perform(patch("/api/geschichten/{id}", UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "BLOG_WRITE")
    void update_returns200_withBlogWrite() throws Exception {
        UUID id = UUID.randomUUID();
        when(geschichteService.update(eq(id), any(GeschichteUpdateDTO.class)))
                .thenReturn(published(id, "Updated"));

        mockMvc.perform(patch("/api/geschichten/{id}", id).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"status\":\"PUBLISHED\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("PUBLISHED"));
    }

    // ─── DELETE /api/geschichten/{id} ────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void delete_returns403_whenLackingBlogWrite() throws Exception {
        mockMvc.perform(delete("/api/geschichten/{id}", UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "BLOG_WRITE")
    void delete_returns204_withBlogWrite() throws Exception {
        UUID id = UUID.randomUUID();

        mockMvc.perform(delete("/api/geschichten/{id}", id).with(csrf()))
                .andExpect(status().isNoContent());

        verify(geschichteService).delete(id);
    }

    // ─── helpers ─────────────────────────────────────────────────────────────

    private Geschichte published(UUID id, String title) {
        return Geschichte.builder()
                .id(id)
                .title(title)
                .body("<p>x</p>")
                .status(GeschichteStatus.PUBLISHED)
                .publishedAt(LocalDateTime.now())
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .persons(new HashSet<>())
                .documents(new HashSet<>())
                .build();
    }

    private Geschichte draft(UUID id, String title) {
        return Geschichte.builder()
                .id(id)
                .title(title)
                .status(GeschichteStatus.DRAFT)
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .persons(new HashSet<>())
                .documents(new HashSet<>())
                .build();
    }
}
