package org.raddatz.familienarchiv.document.annotation;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.annotation.DocumentAnnotation;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.document.annotation.AnnotationService;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.document.DocumentService;
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.util.List;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
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(AnnotationController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class AnnotationControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean AnnotationService annotationService;
    @MockitoBean DocumentService documentService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private static final String ANNOTATION_JSON =
            "{\"pageNumber\":1,\"x\":0.1,\"y\":0.1,\"width\":0.2,\"height\":0.2,\"color\":\"#ff0000\"}";

    // ─── GET /api/documents/{documentId}/annotations ──────────────────────────

    @Test
    void listAnnotations_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/documents/" + UUID.randomUUID() + "/annotations"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void listAnnotations_returns200_whenAuthenticated() throws Exception {
        when(annotationService.listAnnotations(any())).thenReturn(List.of());

        mockMvc.perform(get("/api/documents/" + UUID.randomUUID() + "/annotations"))
                .andExpect(status().isOk());
    }

    // ─── POST /api/documents/{documentId}/annotations ─────────────────────────

    @Test
    void createAnnotation_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void createAnnotation_returns403_whenMissingAnnotatePermission() throws Exception {
        mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void createAnnotation_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void createAnnotation_returns201_whenHasWriteAllPermission() throws Exception {
        UUID docId = UUID.randomUUID();
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
        when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
        when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);

        mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isCreated());
    }

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

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void createAnnotation_returns201_whenHasAnnotatePermission() throws Exception {
        UUID docId = UUID.randomUUID();
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
        when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
        when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);

        mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.pageNumber").value(1));
    }

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void createAnnotation_returns201_whenAnnotationsOverlap() throws Exception {
        // Overlapping annotations are allowed — historical letter lines often intersect
        UUID docId = UUID.randomUUID();
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.3).height(0.3).color("#ff0000").build();
        when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
        when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);

        mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isCreated());
    }

    // ─── DELETE /api/documents/{documentId}/annotations/{annotationId} ─────────

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

    @Test
    @WithMockUser
    void deleteAnnotation_returns403_whenMissingAnnotatePermission() throws Exception {
        mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

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

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void deleteAnnotation_returns204_whenHasAnnotatePermission() throws Exception {
        mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isNoContent());
    }

    // ─── PATCH /api/documents/{documentId}/annotations/{annotationId} ─────────

    private static final String PATCH_JSON = "{\"x\":0.2,\"y\":0.3}";

    @Test
    void patchAnnotation_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void patchAnnotation_returns403_withoutPermission() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void patchAnnotation_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns200_withWriteAllPermission() throws Exception {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        DocumentAnnotation updated = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).pageNumber(1)
                .x(0.2).y(0.3).width(0.2).height(0.2).color("#ff0000").build();
        when(annotationService.updateAnnotation(any(), any(), any())).thenReturn(updated);

        mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.x").value(0.2))
                .andExpect(jsonPath("$.y").value(0.3));
    }

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void patchAnnotation_returns200_withAnnotateAllPermission() throws Exception {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        DocumentAnnotation updated = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).pageNumber(1)
                .x(0.2).y(0.3).width(0.2).height(0.2).color("#ff0000").build();
        when(annotationService.updateAnnotation(any(), any(), any())).thenReturn(updated);

        mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns404_whenAnnotationBelongsToDifferentDocument() throws Exception {
        when(annotationService.updateAnnotation(any(), any(), any()))
                .thenThrow(DomainException.notFound(ErrorCode.ANNOTATION_NOT_FOUND, "not found"));

        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(PATCH_JSON))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns400_withOutOfBoundsCoordinates() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"x\":-0.1,\"y\":0.3}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns400_withWidthBelowMinimum() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"width\":0.005}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns400_withHeightBelowMinimum() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"height\":0.005}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void patchAnnotation_returns400_withXAboveMaximum() throws Exception {
        mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"x\":1.1}"))
                .andExpect(status().isBadRequest());
    }

    // ─── resolveUserId — unauthenticated / null user / exception branches ─────

    @Test
    void createAnnotation_returns401_whenUnauthenticated_resolveUserIdReturnsNull() throws Exception {
        // authentication == null → resolveUserId returns null
        mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
        // findByEmail throws → catch block → resolveUserId returns null
        UUID docId = UUID.randomUUID();
        when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
        when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
        when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);

        mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isCreated());
    }

    @Test
    @WithMockUser(authorities = "ANNOTATE_ALL")
    void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
        // findByEmail returns null → user != null = false → resolveUserId returns null
        UUID docId = UUID.randomUUID();
        when(userService.findByEmail(any())).thenReturn(null);
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
        when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
        when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);

        mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ANNOTATION_JSON))
                .andExpect(status().isCreated());
    }
}
