package org.raddatz.familienarchiv.document.transcription;

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.user.AppUser;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockVersion;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.document.transcription.TranscriptionService;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(TranscriptionBlockController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class TranscriptionBlockControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean TranscriptionService transcriptionService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private static final UUID DOC_ID  = UUID.randomUUID();
    private static final UUID BLOCK_ID = UUID.randomUUID();
    private static final String URL_BASE    = "/api/documents/" + DOC_ID + "/transcription-blocks";
    private static final String URL_BLOCK   = URL_BASE + "/" + BLOCK_ID;
    private static final String URL_REORDER = URL_BASE + "/reorder";
    private static final String URL_HISTORY = URL_BLOCK + "/history";

    private static final String CREATE_JSON =
            "{\"pageNumber\":1,\"x\":0.1,\"y\":0.2,\"width\":0.3,\"height\":0.4,\"text\":\"Liebe Mutter,\"}";
    private static final String UPDATE_JSON =
            "{\"text\":\"Neue Fassung\",\"label\":\"Anrede\"}";
    private static final String REORDER_JSON =
            "{\"blockIds\":[\"" + UUID.randomUUID() + "\",\"" + UUID.randomUUID() + "\"]}";

    private AppUser mockUser() {
        return AppUser.builder().id(UUID.randomUUID()).email("user@example.com").build();
    }

    private TranscriptionBlock sampleBlock() {
        return TranscriptionBlock.builder()
                .id(BLOCK_ID).documentId(DOC_ID)
                .annotationId(UUID.randomUUID())
                .text("Liebe Mutter,").sortOrder(0).build();
    }

    // ─── GET /api/documents/{id}/transcription-blocks ────────────────────────

    @Test
    void listBlocks_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get(URL_BASE))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void listBlocks_returns403_whenMissingReadAllPermission() throws Exception {
        mockMvc.perform(get(URL_BASE))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void listBlocks_returns200_withBlocks_whenAuthorised() throws Exception {
        TranscriptionBlock b = sampleBlock();
        when(transcriptionService.listBlocks(DOC_ID)).thenReturn(List.of(b));

        mockMvc.perform(get(URL_BASE))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].text").value("Liebe Mutter,"));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void listBlocks_returns200_withEmptyArray_whenNoBlocksExist() throws Exception {
        when(transcriptionService.listBlocks(any())).thenReturn(List.of());

        mockMvc.perform(get(URL_BASE))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$").isEmpty());
    }

    // ─── GET /api/documents/{id}/transcription-blocks/{blockId} ─────────────

    @Test
    void getBlock_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get(URL_BLOCK))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void getBlock_returns403_whenMissingReadAllPermission() throws Exception {
        mockMvc.perform(get(URL_BLOCK))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getBlock_returns200_withBlockData_whenFound() throws Exception {
        when(transcriptionService.getBlock(DOC_ID, BLOCK_ID)).thenReturn(sampleBlock());

        mockMvc.perform(get(URL_BLOCK))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(BLOCK_ID.toString()))
                .andExpect(jsonPath("$.text").value("Liebe Mutter,"))
                .andExpect(jsonPath("$.sortOrder").value(0));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getBlock_returns404_whenBlockDoesNotExist() throws Exception {
        when(transcriptionService.getBlock(any(), any()))
                .thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"));

        mockMvc.perform(get(URL_BLOCK))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("TRANSCRIPTION_BLOCK_NOT_FOUND"));
    }

    // ─── POST /api/documents/{id}/transcription-blocks ───────────────────────

    @Test
    void createBlock_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(CREATE_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void createBlock_returns403_whenMissingWriteAllPermission() throws Exception {
        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(CREATE_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void createBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(CREATE_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void createBlock_returns201_withSavedBlock_whenAuthorised() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        when(transcriptionService.createBlock(eq(DOC_ID), any(), any())).thenReturn(sampleBlock());

        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(CREATE_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.text").value("Liebe Mutter,"))
                .andExpect(jsonPath("$.documentId").value(DOC_ID.toString()));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void createBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
        when(userService.findByEmail(any())).thenReturn(null);

        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(CREATE_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void createBlock_returns400_whenMentionedPersonDisplayNameExceeds200Chars() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        String longName = "A".repeat(201);
        String body = "{\"pageNumber\":1,\"x\":0.1,\"y\":0.2,\"width\":0.3,\"height\":0.4,\"text\":\"x\","
                + "\"mentionedPersons\":[{\"personId\":\"" + UUID.randomUUID()
                + "\",\"displayName\":\"" + longName + "\"}]}";

        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void createBlock_returns400_whenMentionedPersonPersonIdIsNull() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        String body = "{\"pageNumber\":1,\"x\":0.1,\"y\":0.2,\"width\":0.3,\"height\":0.4,\"text\":\"x\","
                + "\"mentionedPersons\":[{\"personId\":null,\"displayName\":\"Auguste Raddatz\"}]}";

        mockMvc.perform(post(URL_BASE).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    // ─── PUT /api/documents/{id}/transcription-blocks/{blockId} ─────────────

    @Test
    void updateBlock_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void updateBlock_returns403_whenMissingWriteAllPermission() throws Exception {
        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void updateBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void updateBlock_returns200_withUpdatedBlock_whenAuthorised() throws Exception {
        TranscriptionBlock updated = sampleBlock();
        updated.setText("Neue Fassung");
        updated.setLabel("Anrede");

        when(userService.findByEmail(any())).thenReturn(mockUser());
        when(transcriptionService.updateBlock(eq(DOC_ID), eq(BLOCK_ID), any(), any()))
                .thenReturn(updated);

        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.text").value("Neue Fassung"))
                .andExpect(jsonPath("$.label").value("Anrede"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void updateBlock_returns400_whenMentionedPersonDisplayNameExceeds200Chars() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        String longName = "A".repeat(201);
        String body = "{\"text\":\"x\",\"mentionedPersons\":[{\"personId\":\""
                + UUID.randomUUID() + "\",\"displayName\":\"" + longName + "\"}]}";

        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void updateBlock_returns400_whenMentionedPersonPersonIdIsNull() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        String body = "{\"text\":\"x\",\"mentionedPersons\":[{\"personId\":null,\"displayName\":\"Auguste Raddatz\"}]}";

        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void updateBlock_returns404_whenBlockDoesNotExist() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        when(transcriptionService.updateBlock(any(), any(), any(), any()))
                .thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"));

        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void updateBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
        when(userService.findByEmail(any())).thenReturn(null);

        mockMvc.perform(put(URL_BLOCK).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(UPDATE_JSON))
                .andExpect(status().isUnauthorized());
    }

    // ─── DELETE /api/documents/{id}/transcription-blocks/{blockId} ───────────

    @Test
    void deleteBlock_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(delete(URL_BLOCK).with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void deleteBlock_returns403_whenMissingWriteAllPermission() throws Exception {
        mockMvc.perform(delete(URL_BLOCK).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void deleteBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(delete(URL_BLOCK).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void deleteBlock_returns204_whenAuthorised() throws Exception {
        mockMvc.perform(delete(URL_BLOCK).with(csrf()))
                .andExpect(status().isNoContent());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void deleteBlock_returns404_whenBlockDoesNotExist() throws Exception {
        org.mockito.Mockito.doThrow(
                DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"))
                .when(transcriptionService).deleteBlock(any(), any());

        mockMvc.perform(delete(URL_BLOCK).with(csrf()))
                .andExpect(status().isNotFound());
    }

    // ─── PUT /api/documents/{id}/transcription-blocks/reorder ────────────────

    @Test
    void reorderBlocks_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(put(URL_REORDER).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(REORDER_JSON))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void reorderBlocks_returns403_whenMissingWriteAllPermission() throws Exception {
        mockMvc.perform(put(URL_REORDER).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(REORDER_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void reorderBlocks_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(put(URL_REORDER).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(REORDER_JSON))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void reorderBlocks_returns200_withReorderedBlocks_whenAuthorised() throws Exception {
        when(transcriptionService.listBlocks(DOC_ID)).thenReturn(List.of(sampleBlock()));

        mockMvc.perform(put(URL_REORDER).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(REORDER_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray());
    }

    // ─── GET /api/documents/{id}/transcription-blocks/{blockId}/history ──────

    @Test
    void getBlockHistory_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get(URL_HISTORY))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void getBlockHistory_returns403_whenMissingReadAllPermission() throws Exception {
        mockMvc.perform(get(URL_HISTORY))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getBlockHistory_returns200_withVersionList_whenAuthorised() throws Exception {
        TranscriptionBlockVersion v = TranscriptionBlockVersion.builder()
                .id(UUID.randomUUID()).blockId(BLOCK_ID).text("v1").build();
        when(transcriptionService.getBlockHistory(DOC_ID, BLOCK_ID)).thenReturn(List.of(v));

        mockMvc.perform(get(URL_HISTORY))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].text").value("v1"))
                .andExpect(jsonPath("$[0].blockId").value(BLOCK_ID.toString()));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getBlockHistory_returns404_whenBlockDoesNotExist() throws Exception {
        when(transcriptionService.getBlockHistory(any(), any()))
                .thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"));

        mockMvc.perform(get(URL_HISTORY))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getBlockHistory_returns200_withEmptyList_whenNoVersionsExist() throws Exception {
        when(transcriptionService.getBlockHistory(any(), any())).thenReturn(List.of());

        mockMvc.perform(get(URL_HISTORY))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isEmpty());
    }

    // ─── PUT .../review ──────────────────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void reviewBlock_returns200_withToggledBlock() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        TranscriptionBlock reviewed = TranscriptionBlock.builder()
                .id(BLOCK_ID).documentId(DOC_ID).annotationId(UUID.randomUUID())
                .text("text").sortOrder(0).reviewed(true).build();
        when(transcriptionService.reviewBlock(eq(DOC_ID), eq(BLOCK_ID), any())).thenReturn(reviewed);

        mockMvc.perform(put("/api/documents/{documentId}/transcription-blocks/{blockId}/review",
                        DOC_ID, BLOCK_ID).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.reviewed").value(true));
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void reviewBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
        mockMvc.perform(put("/api/documents/{documentId}/transcription-blocks/{blockId}/review",
                        DOC_ID, BLOCK_ID).with(csrf()))
                .andExpect(status().isForbidden());
    }

    // ─── PUT .../review-all ───────────────────────────────────────────────────

    private static final String URL_REVIEW_ALL = URL_BASE + "/review-all";

    @Test
    void markAllBlocksReviewed_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(put(URL_REVIEW_ALL).with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void markAllBlocksReviewed_returns403_whenMissingWriteAllPermission() throws Exception {
        mockMvc.perform(put(URL_REVIEW_ALL).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void markAllBlocksReviewed_returns200_withAllReviewedBlocks_whenAuthorised() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        TranscriptionBlock b1 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(UUID.randomUUID())
                .text("Block 1").sortOrder(0).reviewed(true).build();
        TranscriptionBlock b2 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(UUID.randomUUID())
                .text("Block 2").sortOrder(1).reviewed(true).build();
        when(transcriptionService.markAllBlocksReviewed(eq(DOC_ID), any()))
                .thenReturn(List.of(b1, b2));

        mockMvc.perform(put(URL_REVIEW_ALL).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$[0].reviewed").value(true))
                .andExpect(jsonPath("$[1].reviewed").value(true));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void markAllBlocksReviewed_returns200_withEmptyList_whenNoBlocksExist() throws Exception {
        when(userService.findByEmail(any())).thenReturn(mockUser());
        when(transcriptionService.markAllBlocksReviewed(eq(DOC_ID), any()))
                .thenReturn(List.of());

        mockMvc.perform(put(URL_REVIEW_ALL).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$").isEmpty());
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void markAllBlocksReviewed_returns401_whenUserNotFoundInDatabase() throws Exception {
        when(userService.findByEmail(any())).thenReturn(null);

        mockMvc.perform(put(URL_REVIEW_ALL).with(csrf()))
                .andExpect(status().isUnauthorized());
    }
}
