package cn.nianxx.thhotel.platform.documentconversion.control;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConversionInput;
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConvertedDocument;
import cn.nianxx.thhotel.platform.documentconversion.service.ExcelToPdfConverter;
import cn.nianxx.thhotel.platform.documentconversion.service.DocumentConversionException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(
        classes = ThHotelApplication.class,
        properties = {
                "document-conversion.enabled=true",
                "document-conversion.access-key=test-document-key",
                "document-conversion.max-file-bytes=1024",
                "document-conversion.output-oss-prefix=document-conversions/excel-to-pdf/"
        })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class DocumentConversionControllerTest {

    private static final String ENDPOINT = "/api/system/document-conversions/excel-to-pdf";

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ExcelToPdfConverter excelToPdfConverter;

    @MockBean
    private ObjectStorageService objectStorageService;

    @Test
    void shouldConvertUploadedExcelToPdfAndUploadPdfToOss() throws Exception {
        byte[] excelBytes = xlsxBytes("xlsx-bytes");
        byte[] pdfBytes = "%PDF-1.7\nconverted".getBytes(StandardCharsets.UTF_8);
        when(excelToPdfConverter.convert(any())).thenReturn(new ExcelToPdfConvertedDocument(
                "booking-request.pdf",
                MediaType.APPLICATION_PDF_VALUE,
                (long) pdfBytes.length,
                pdfBytes,
                123L));
        when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
            ObjectStoragePutRequest request = invocation.getArgument(0);
            return new ObjectStoragePutResult(
                    request.objectKey(),
                    "https://oss.example.test/" + request.objectKey(),
                    request.contentType(),
                    request.sizeBytes());
        });

        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.xlsx", excelBytes))
                        .param("hotel_id", "HOTEL-TEST")
                        .header("X-TH-Hotel-Document-Conversion-Key", "test-document-key"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.conversion_status").value("SUCCEEDED"))
                .andExpect(jsonPath("$.source_file_name").value("booking-request.xlsx"))
                .andExpect(jsonPath("$.pdf_file_name").value("booking-request.pdf"))
                .andExpect(jsonPath("$.pdf_url", containsString("https://oss.example.test/")))
                .andExpect(jsonPath("$.object_key", containsString("document-conversions/excel-to-pdf/")))
                .andExpect(jsonPath("$.content_type").value(MediaType.APPLICATION_PDF_VALUE))
                .andExpect(jsonPath("$.pdf_size_bytes").value(pdfBytes.length))
                .andExpect(jsonPath("$.safe_error_summary").doesNotExist())
                .andExpect(content().string(not(containsString("test-document-key"))));

        ArgumentCaptor<ExcelToPdfConversionInput> converterInputCaptor =
                ArgumentCaptor.forClass(ExcelToPdfConversionInput.class);
        verify(excelToPdfConverter).convert(converterInputCaptor.capture());
        org.assertj.core.api.Assertions.assertThat(converterInputCaptor.getValue().fileName())
                .isEqualTo("booking-request.xlsx");
        org.assertj.core.api.Assertions.assertThat(converterInputCaptor.getValue().content())
                .containsExactly(excelBytes);

        ArgumentCaptor<ObjectStoragePutRequest> storageRequestCaptor =
                ArgumentCaptor.forClass(ObjectStoragePutRequest.class);
        verify(objectStorageService).putObject(storageRequestCaptor.capture());
        org.assertj.core.api.Assertions.assertThat(storageRequestCaptor.getValue().objectKey())
                .startsWith("document-conversions/excel-to-pdf/HOTEL-TEST/")
                .endsWith("/booking-request.pdf");
        org.assertj.core.api.Assertions.assertThat(storageRequestCaptor.getValue().contentType())
                .isEqualTo(MediaType.APPLICATION_PDF_VALUE);
        org.assertj.core.api.Assertions.assertThat(storageRequestCaptor.getValue().content())
                .containsExactly(pdfBytes);
    }

    @Test
    void shouldRejectUploadWhenDocumentConversionKeyMissing() throws Exception {
        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.xlsx", xlsxBytes("xlsx-bytes"))))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_KEY_INVALID"))
                .andExpect(content().string(not(containsString("test-document-key"))));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectUnsupportedExcelFileType() throws Exception {
        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.txt", "not-excel".getBytes(StandardCharsets.UTF_8)))
                        .header("X-TH-Hotel-Document-Conversion-Key", "test-document-key"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_FILE_TYPE_UNSUPPORTED"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectExcelExtensionWhenContentHeaderInvalid() throws Exception {
        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.xlsx", "not-excel".getBytes(StandardCharsets.UTF_8)))
                        .header("X-TH-Hotel-Document-Conversion-Key", "test-document-key"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_FILE_CONTENT_INVALID"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectFileLargerThanConfiguredLimit() throws Exception {
        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.xlsx", "x".repeat(2048).getBytes(StandardCharsets.UTF_8)))
                        .header("X-TH-Hotel-Document-Conversion-Key", "test-document-key"))
                .andExpect(status().isPayloadTooLarge())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_FILE_TOO_LARGE"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldReturnSafeErrorWhenLibreOfficeConversionFails() throws Exception {
        when(excelToPdfConverter.convert(any())).thenThrow(new DocumentConversionException(
                HttpStatus.GATEWAY_TIMEOUT,
                "DOCUMENT_CONVERSION_TIMEOUT",
                "Excel 转 PDF 超时。"));

        mockMvc.perform(multipart(ENDPOINT)
                        .file(excelFile("booking-request.xlsx", xlsxBytes("xlsx-bytes")))
                        .header("X-TH-Hotel-Document-Conversion-Key", "test-document-key"))
                .andExpect(status().isGatewayTimeout())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_TIMEOUT"))
                .andExpect(jsonPath("$.message").value("Excel 转 PDF 超时。"))
                .andExpect(content().string(not(containsString("xlsx-bytes"))));

        verifyNoInteractions(objectStorageService);
    }

    private MockMultipartFile excelFile(String fileName, byte[] content) {
        return new MockMultipartFile(
                "file",
                fileName,
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                content);
    }

    private byte[] xlsxBytes(String content) {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            outputStream.write(new byte[]{0x50, 0x4B, 0x03, 0x04});
            outputStream.write(content.getBytes(StandardCharsets.UTF_8));
            return outputStream.toByteArray();
        } catch (IOException exception) {
            throw new AssertionError("构造测试 xlsx 字节失败。", exception);
        }
    }
}
