package org.raddatz.familienarchiv.ocr;

import tools.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.ocr.BatchOcrDTO;
import org.raddatz.familienarchiv.ocr.OcrStatusDTO;
import org.raddatz.familienarchiv.ocr.TrainingHistoryResponse;
import org.raddatz.familienarchiv.ocr.TrainingInfoResponse;
import org.raddatz.familienarchiv.ocr.TriggerOcrDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.UserService;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.security.PermissionAspect;
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 java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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.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(OcrController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class OcrControllerTest {

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

    @MockitoBean OcrService ocrService;
    @MockitoBean OcrBatchService ocrBatchService;
    @MockitoBean OcrProgressService ocrProgressService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;
    @MockitoBean TrainingDataExportService trainingDataExportService;
    @MockitoBean SegmentationTrainingExportService segmentationTrainingExportService;
    @MockitoBean OcrTrainingService ocrTrainingService;
    @MockitoBean SenderModelService senderModelService;

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void triggerOcr_returns202_withJobId() throws Exception {
        UUID docId = UUID.randomUUID();
        UUID jobId = UUID.randomUUID();
        TriggerOcrDTO dto = new TriggerOcrDTO(ScriptType.TYPEWRITER, false);

        when(ocrService.startOcr(eq(docId), eq(ScriptType.TYPEWRITER), any(), anyBoolean())).thenReturn(jobId);

        mockMvc.perform(post("/api/documents/{id}/ocr", docId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.jobId").value(jobId.toString()));
    }

    @Test
    @WithMockUser(authorities = "WRITE_ALL")
    void triggerOcr_returns400_whenDocumentNotUploaded() throws Exception {
        UUID docId = UUID.randomUUID();
        when(ocrService.startOcr(eq(docId), any(), any(), anyBoolean()))
                .thenThrow(DomainException.badRequest(ErrorCode.OCR_DOCUMENT_NOT_UPLOADED, "Not uploaded"));

        mockMvc.perform(post("/api/documents/{id}/ocr", docId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getJobStatus_returns404_whenJobNotFound() throws Exception {
        UUID jobId = UUID.randomUUID();
        when(ocrService.getJob(jobId))
                .thenThrow(DomainException.notFound(ErrorCode.OCR_JOB_NOT_FOUND, "OCR job not found"));

        mockMvc.perform(get("/api/ocr/jobs/{jobId}", jobId))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getJobStatus_returnsJobInfo_whenFound() throws Exception {
        UUID jobId = UUID.randomUUID();
        OcrJob job = OcrJob.builder()
                .id(jobId)
                .status(OcrJobStatus.RUNNING)
                .totalDocuments(10)
                .processedDocuments(3)
                .errorCount(1)
                .skippedCount(0)
                .build();
        when(ocrService.getJob(jobId)).thenReturn(job);

        mockMvc.perform(get("/api/ocr/jobs/{jobId}", jobId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("RUNNING"))
                .andExpect(jsonPath("$.totalDocuments").value(10))
                .andExpect(jsonPath("$.processedDocuments").value(3));
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerBatch_returns202_withJobId() throws Exception {
        UUID jobId = UUID.randomUUID();
        List<UUID> docIds = List.of(UUID.randomUUID(), UUID.randomUUID());
        BatchOcrDTO dto = new BatchOcrDTO(docIds);

        when(ocrBatchService.startBatch(eq(docIds), any())).thenReturn(jobId);

        mockMvc.perform(post("/api/ocr/batch").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.jobId").value(jobId.toString()));
    }

    // ─── GET /api/ocr/training-data/export ───────────────────────────────────

    @Test
    void exportTrainingData_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/ocr/training-data/export"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void exportTrainingData_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(get("/api/ocr/training-data/export"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void exportTrainingData_returns204_whenNoEligibleBlocks() throws Exception {
        when(trainingDataExportService.queryEligibleBlocks()).thenReturn(List.of());

        mockMvc.perform(get("/api/ocr/training-data/export"))
                .andExpect(status().isNoContent());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void exportTrainingData_returns200_withZipContentType_whenBlocksExist() throws Exception {
        org.raddatz.familienarchiv.document.transcription.TranscriptionBlock block =
                org.raddatz.familienarchiv.document.transcription.TranscriptionBlock.builder()
                        .id(UUID.randomUUID()).documentId(UUID.randomUUID())
                        .annotationId(UUID.randomUUID()).text("x").sortOrder(0).build();
        when(trainingDataExportService.queryEligibleBlocks()).thenReturn(List.of(block));
        when(trainingDataExportService.exportToZip()).thenReturn(out -> {});

        mockMvc.perform(get("/api/ocr/training-data/export"))
                .andExpect(status().isOk())
                .andExpect(result ->
                    org.assertj.core.api.Assertions.assertThat(
                        result.getResponse().getContentType()).contains("application/zip"));
    }

    // ─── POST /api/ocr/train ───────────────────────────────────────────────────

    @Test
    void triggerTraining_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/ocr/train").with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void triggerTraining_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(post("/api/ocr/train").with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerTraining_returns409_whenRunAlreadyRunning() throws Exception {
        when(ocrTrainingService.triggerTraining(any()))
                .thenThrow(DomainException.conflict(ErrorCode.TRAINING_ALREADY_RUNNING, "Already running"));

        mockMvc.perform(post("/api/ocr/train").with(csrf()))
                .andExpect(status().isConflict());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerTraining_returns201_withRunInfo() throws Exception {
        UUID runId = UUID.randomUUID();
        OcrTrainingRun run = OcrTrainingRun.builder()
                .id(runId).status(TrainingStatus.DONE)
                .blockCount(10).documentCount(3).modelName("german_kurrent").build();
        when(ocrTrainingService.triggerTraining(any())).thenReturn(run);

        mockMvc.perform(post("/api/ocr/train").with(csrf()))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.status").value("DONE"))
                .andExpect(jsonPath("$.blockCount").value(10));
    }

    // ─── GET /api/ocr/training-info ───────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getTrainingInfo_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getTrainingInfo_returns200_withInfo() throws Exception {
        TrainingInfoResponse info = new TrainingInfoResponse(5, 20, 2, 3, true, null, List.of(), Map.of(), List.of());
        when(ocrTrainingService.getTrainingInfo()).thenReturn(info);

        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.availableBlocks").value(5))
                .andExpect(jsonPath("$.ocrServiceAvailable").value(true));
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getTrainingInfo_returns200_and_omits_personName_when_service_provides_empty_map() throws Exception {
        UUID personId = UUID.randomUUID();
        OcrTrainingRun runWithPerson = OcrTrainingRun.builder()
                .id(UUID.randomUUID()).status(TrainingStatus.DONE)
                .personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build();
        TrainingInfoResponse info = new TrainingInfoResponse(5, 20, 2, 3, true, null,
                List.of(runWithPerson), Map.of(), List.of());
        when(ocrTrainingService.getTrainingInfo()).thenReturn(info);

        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.personNames").isEmpty());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getTrainingInfo_includesPersonName_whenPersonIdResolves() throws Exception {
        UUID personId = UUID.randomUUID();
        OcrTrainingRun runWithPerson = OcrTrainingRun.builder()
                .id(UUID.randomUUID()).status(TrainingStatus.DONE)
                .personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build();
        TrainingInfoResponse info = new TrainingInfoResponse(5, 20, 2, 3, true, null,
                List.of(runWithPerson), Map.of(personId.toString(), "Max Mustermann"), List.of());
        when(ocrTrainingService.getTrainingInfo()).thenReturn(info);

        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.personNames." + personId).value("Max Mustermann"));
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getTrainingInfo_serializes_null_lastRun_as_json_null() throws Exception {
        TrainingInfoResponse info = new TrainingInfoResponse(0, 0, 0, 0, false, null, List.of(), Map.of(), List.of());
        when(ocrTrainingService.getTrainingInfo()).thenReturn(info);

        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.lastRun").doesNotExist());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getTrainingInfo_returns200_includingSenderModels() throws Exception {
        UUID personId = UUID.randomUUID();
        SenderModel senderModel = SenderModel.builder()
                .id(UUID.randomUUID()).personId(personId).correctedLinesAtTraining(80).build();
        TrainingInfoResponse info = new TrainingInfoResponse(
                5, 20, 2, 3, true, null, List.of(), Map.of(), List.of(senderModel));
        when(ocrTrainingService.getTrainingInfo()).thenReturn(info);

        mockMvc.perform(get("/api/ocr/training-info"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.senderModels").isArray())
                .andExpect(jsonPath("$.senderModels[0].personId").value(personId.toString()));
    }

    // ─── GET /api/ocr/training-info/global ───────────────────────────────────

    @Test
    void getGlobalTrainingHistory_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/ocr/training-info/global"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getGlobalTrainingHistory_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(get("/api/ocr/training-info/global"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getGlobalTrainingHistory_returns200_withRunsList() throws Exception {
        TrainingHistoryResponse history = new TrainingHistoryResponse(List.of(), Map.of());
        when(ocrTrainingService.getGlobalTrainingHistory()).thenReturn(history);

        mockMvc.perform(get("/api/ocr/training-info/global"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.runs").isArray());
    }

    // ─── GET /api/ocr/training-info/{personId} ───────────────────────────────

    @Test
    void getSenderTrainingHistory_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/ocr/training-info/{id}", UUID.randomUUID()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getSenderTrainingHistory_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(get("/api/ocr/training-info/{id}", UUID.randomUUID()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getSenderTrainingHistory_returns404_whenPersonNotFound() throws Exception {
        UUID unknownId = UUID.randomUUID();
        when(ocrTrainingService.getSenderTrainingHistory(unknownId))
                .thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found"));

        mockMvc.perform(get("/api/ocr/training-info/{id}", unknownId))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void getSenderTrainingHistory_returns200_withRunsList() throws Exception {
        UUID personId = UUID.randomUUID();
        TrainingHistoryResponse history = new TrainingHistoryResponse(List.of(), Map.of(personId.toString(), "Anna Müller"));
        when(ocrTrainingService.getSenderTrainingHistory(personId)).thenReturn(history);

        mockMvc.perform(get("/api/ocr/training-info/{id}", personId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.runs").isArray())
                .andExpect(jsonPath("$.personNames." + personId).value("Anna Müller"));
    }

    // ─── POST /api/ocr/train-sender ───────────────────────────────────────────

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerSenderTraining_returns400_whenPersonIdIsNull() throws Exception {
        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":null}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    void triggerSenderTraining_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + UUID.randomUUID() + "\"}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void triggerSenderTraining_returns403_whenNotAdmin() throws Exception {
        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + UUID.randomUUID() + "\"}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerSenderTraining_returns404_whenPersonNotFound() throws Exception {
        UUID unknownId = UUID.randomUUID();
        when(senderModelService.triggerManualSenderTraining(unknownId))
                .thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found"));

        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + unknownId + "\"}"))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerSenderTraining_returns202_withRunning_run() throws Exception {
        UUID personId = UUID.randomUUID();
        OcrTrainingRun run = OcrTrainingRun.builder()
                .id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
                .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build();
        when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run);

        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + personId + "\"}"))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.status").value("RUNNING"));
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerSenderTraining_returns202_withQueued_run_whenAnotherRunning() throws Exception {
        UUID personId = UUID.randomUUID();
        OcrTrainingRun run = OcrTrainingRun.builder()
                .id(UUID.randomUUID()).status(TrainingStatus.QUEUED)
                .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build();
        when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run);

        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + personId + "\"}"))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.status").value("QUEUED"));
    }

    @Test
    @WithMockUser(authorities = "ADMIN")
    void triggerSenderTraining_doesNotCallRunSenderTraining_fromController() throws Exception {
        UUID personId = UUID.randomUUID();
        OcrTrainingRun run = OcrTrainingRun.builder()
                .id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
                .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build();
        when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run);

        mockMvc.perform(post("/api/ocr/train-sender").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"personId\":\"" + personId + "\"}"))
                .andExpect(status().isAccepted());

        verify(senderModelService, never()).runSenderTraining(any());
    }

    @Test
    @WithMockUser(authorities = "READ_ALL")
    void getDocumentOcrStatus_returnsNone_whenNoOcrJobExists() throws Exception {
        UUID docId = UUID.randomUUID();
        when(ocrService.getDocumentOcrStatus(docId))
                .thenReturn(OcrStatusDTO.builder().status("NONE").build());

        mockMvc.perform(get("/api/documents/{id}/ocr-status", docId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("NONE"));
    }
}
