package cn.nianxx.thhotel.workflows.reservation.control;

import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
import static org.assertj.core.api.Assertions.assertThat;
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.times;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
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.DocumentConversionException;
import cn.nianxx.thhotel.platform.documentconversion.service.ExcelToPdfConverter;
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
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.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(
        classes = ThHotelApplication.class,
        properties = {
                "spring.datasource.url=jdbc:h2:mem:reservation_invoice_generation;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
                "auth.bootstrap.admin.username=invoice-admin",
                "auth.bootstrap.admin.password=Admin@123456",
                "auth.bootstrap.admin.display-name=系统管理员",
                "auth.bootstrap.default-hotel-id=HOTEL-TEST",
                "auth.bootstrap.default-hotel-name=测试酒店",
                "auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
                "superagent.task-result.hmac-secret=test-superagent-secret",
                "mcp.enabled=true",
                "mcp.auth-token=test-mcp-token"
        })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class ReservationInvoiceGenerationControllerTest {

    private static final String ENDPOINT = "/api/reservation/invoices/manual-generations";

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private PlatformIdentityRepository identityRepository;
    @Autowired
    private PlatformHotelRepository hotelRepository;
    @Autowired
    private AuthPasswordService passwordService;

    @MockBean
    private ExcelToPdfConverter excelToPdfConverter;
    @MockBean
    private ObjectStorageService objectStorageService;

    @BeforeEach
    void setUpNoPermissionUser() {
        jdbcTemplate.update("delete from workflow_reservation_invoice_generation");
        PlatformUserEntity user = identityRepository.findUserByUsername("invoice-no-permission")
                .orElseGet(() -> {
                    LocalDateTime now = LocalDateTime.now();
                    PlatformUserEntity created = new PlatformUserEntity();
                    created.setUsername("invoice-no-permission");
                    created.setPasswordHash(passwordService.hash("NoPerm@123456"));
                    created.setDisplayName("无权限用户");
                    created.setUserStatus(PlatformUserStatus.ACTIVE.name());
                    created.setSuperAdmin(false);
                    created.setPasswordChangedAt(now);
                    created.setCreatedAt(now);
                    created.setUpdatedAt(now);
                    identityRepository.insertUser(created);
                    return created;
                });
        hotelRepository.ensureUserHotel(user.getId(), "HOTEL-TEST", true);
    }

    @Test
    void shouldGenerateManualInvoicePdfAndPersistGenerationRecord() throws Exception {
        byte[] pdfBytes = "%PDF-1.7\nmanual-invoice".getBytes(StandardCharsets.UTF_8);
        when(excelToPdfConverter.convert(any())).thenReturn(new ExcelToPdfConvertedDocument(
                "proforma-invoice.pdf",
                MediaType.APPLICATION_PDF_VALUE,
                (long) pdfBytes.length,
                pdfBytes,
                88L));
        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());
        });
        String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");

        performAuthorized(mockMvc, token, post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequest()))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.generation_status").value("SUCCEEDED"))
                .andExpect(jsonPath("$.source_type").value("MANUAL"))
                .andExpect(jsonPath("$.hotel_id").value("HOTEL-TEST"))
                .andExpect(jsonPath("$.template_code").value("PROFORMA_INVOICE_V1"))
                .andExpect(jsonPath("$.pdf_url", containsString("https://oss.example.test/")))
                .andExpect(jsonPath("$.pdf_object_key", containsString("reservation-invoices/HOTEL-TEST/")))
                .andExpect(jsonPath("$.generated_excel_object_key", containsString("reservation-invoices/HOTEL-TEST/")))
                .andExpect(jsonPath("$.totals.subtotal").value(16822.43))
                .andExpect(jsonPath("$.totals.vat").value(1177.57))
                .andExpect(jsonPath("$.totals.total").value(18000.00))
                .andExpect(jsonPath("$.totals.currency").value("THB"))
                .andExpect(jsonPath("$.created_at", containsString("Z")))
                .andExpect(content().string(not(containsString("op.liantaitravel@gmail.com"))));

        ArgumentCaptor<ExcelToPdfConversionInput> converterInputCaptor =
                ArgumentCaptor.forClass(ExcelToPdfConversionInput.class);
        verify(excelToPdfConverter).convert(converterInputCaptor.capture());
        assertThat(converterInputCaptor.getValue().fileName()).endsWith(".xlsx");
        assertThat(converterInputCaptor.getValue().content()).startsWith(new byte[]{0x50, 0x4B});

        ArgumentCaptor<ObjectStoragePutRequest> storageRequestCaptor =
                ArgumentCaptor.forClass(ObjectStoragePutRequest.class);
        verify(objectStorageService, times(2)).putObject(storageRequestCaptor.capture());
        assertThat(storageRequestCaptor.getAllValues())
                .extracting(ObjectStoragePutRequest::objectKey)
                .anySatisfy(objectKey -> assertThat(objectKey).endsWith(".xlsx"))
                .anySatisfy(objectKey -> assertThat(objectKey).endsWith(".pdf"));

        Integer generatedRows = jdbcTemplate.queryForObject(
                "select count(*) from workflow_reservation_invoice_generation where hotel_id = 'HOTEL-TEST'",
                Integer.class);
        assertThat(generatedRows).isEqualTo(1);
    }

    @Test
    void shouldRejectManualInvoiceWhenTokenMissing() throws Exception {
        mockMvc.perform(post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequest()))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectManualInvoiceWhenPermissionMissing() throws Exception {
        String token = loginToken(mockMvc, "invoice-no-permission", "NoPerm@123456");

        performAuthorized(mockMvc, token, post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequest()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectManualInvoiceWhenChargeLinesMissing() throws Exception {
        String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");

        performAuthorized(mockMvc, token, post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(emptyChargesRequest()))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("RESERVATION_INVOICE_VALIDATION_FAILED"))
                .andExpect(jsonPath("$.details[0]").value("invoice_payload.charges: 至少需要一条费用明细。"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldRejectManualInvoiceWhenTaskDoesNotBelongToOrder() throws Exception {
        insertOrder(2080010000000000001L, 2080010000000000101L, "GRP-M009-ORDER-1");
        insertOrder(2080010000000000002L, 2080010000000000102L, "GRP-M009-ORDER-2");
        insertTask(2080010000000000201L, 2080010000000000002L, 2080010000000000102L);
        String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");

        performAuthorized(mockMvc, token, post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequestWithContext(2080010000000000001L, 2080010000000000201L)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error_code").value("RESERVATION_INVOICE_CONTEXT_MISMATCH"));

        verifyNoInteractions(excelToPdfConverter, objectStorageService);
    }

    @Test
    void shouldKeepGeneratedExcelObjectKeyWhenPdfConversionFails() throws Exception {
        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());
        });
        when(excelToPdfConverter.convert(any())).thenThrow(new DocumentConversionException(
                HttpStatus.BAD_GATEWAY,
                "DOCUMENT_CONVERSION_FAILED",
                "PDF 转换失败。"));
        String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");

        performAuthorized(mockMvc, token, post(ENDPOINT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequest()))
                .andExpect(status().isBadGateway())
                .andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_FAILED"));

        String generationStatus = jdbcTemplate.queryForObject(
                "SELECT generation_status FROM workflow_reservation_invoice_generation",
                String.class);
        String generatedExcelObjectKey = jdbcTemplate.queryForObject(
                "SELECT generated_excel_object_key FROM workflow_reservation_invoice_generation",
                String.class);
        String safeErrorCode = jdbcTemplate.queryForObject(
                "SELECT safe_error_code FROM workflow_reservation_invoice_generation",
                String.class);
        assertThat(generationStatus).isEqualTo("FAILED");
        assertThat(generatedExcelObjectKey).endsWith(".xlsx");
        assertThat(safeErrorCode).isEqualTo("DOCUMENT_CONVERSION_FAILED");
        verify(objectStorageService).putObject(any());
    }

    private String validRequest() {
        return """
                {
                  "hotel_id": "HOTEL-TEST",
                  "source_type": "MANUAL",
                  "task_id": null,
                  "order_id": null,
                  "template_code": "PROFORMA_INVOICE_V1",
                  "invoice_payload": {
                    "document": {
                      "invoice_date": "2026-07-17",
                      "booking_date": "2026-07-12",
                      "due_date": "2026-07-22"
                    },
                    "recipient": {
                      "company_code": "LIAN_TAI",
                      "contact_id": "LIAN_TAI_KHUN_ANN",
                      "company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
                      "attention": "Khun Ann",
                      "address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
                      "telephone": "061-397-2675",
                      "email": "op.liantaitravel@gmail.com"
                    },
                    "booking": {
                      "group_name": "GRP-DEMO-0802",
                      "arrival_date": "2026-08-02",
                      "departure_date": "2026-08-05",
                      "room_rate_note": "includingBF",
                      "extra_bed_rate": 1200
                    },
                    "charges": [
                      {
                        "description": "GRP-DEMO-0802",
                        "room_type": "Deluxe Room",
                        "quantity": 2,
                        "rate": 3000,
                        "nights": 3
                      }
                    ]
                  }
                }
                """;
    }

    private String validRequestWithContext(Long orderId, Long taskId) {
        return """
                {
                  "hotel_id": "HOTEL-TEST",
                  "source_type": "MANUAL",
                  "task_id": %s,
                  "order_id": %s,
                  "template_code": "PROFORMA_INVOICE_V1",
                  "invoice_payload": {
                    "document": {
                      "invoice_date": "2026-07-17",
                      "booking_date": "2026-07-12",
                      "due_date": "2026-07-22"
                    },
                    "recipient": {
                      "company_code": "LIAN_TAI",
                      "contact_id": "LIAN_TAI_KHUN_ANN",
                      "company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
                      "attention": "Khun Ann",
                      "address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
                      "telephone": "061-397-2675",
                      "email": "op.liantaitravel@gmail.com"
                    },
                    "booking": {
                      "group_name": "GRP-DEMO-0802",
                      "arrival_date": "2026-08-02",
                      "departure_date": "2026-08-05",
                      "room_rate_note": "includingBF",
                      "extra_bed_rate": 1200
                    },
                    "charges": [
                      {
                        "description": "GRP-DEMO-0802",
                        "room_type": "Deluxe Room",
                        "quantity": 2,
                        "rate": 3000,
                        "nights": 3
                      }
                    ]
                  }
                }
                """.formatted(taskId, orderId);
    }

    private String emptyChargesRequest() {
        return """
                {
                  "hotel_id": "HOTEL-TEST",
                  "source_type": "MANUAL",
                  "task_id": null,
                  "order_id": null,
                  "template_code": "PROFORMA_INVOICE_V1",
                  "invoice_payload": {
                    "document": {
                      "invoice_date": "2026-07-17",
                      "booking_date": "2026-07-12",
                      "due_date": "2026-07-22"
                    },
                    "recipient": {
                      "company_code": "LIAN_TAI",
                      "contact_id": "LIAN_TAI_KHUN_ANN",
                      "company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
                      "attention": "Khun Ann",
                      "address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
                      "telephone": "061-397-2675",
                      "email": "op.liantaitravel@gmail.com"
                    },
                    "booking": {
                      "group_name": "GRP-DEMO-0802",
                      "arrival_date": "2026-08-02",
                      "departure_date": "2026-08-05",
                      "room_rate_note": "includingBF",
                      "extra_bed_rate": 1200
                    },
                    "charges": []
                  }
                }
                """;
    }

    private void insertOrder(Long orderId, Long sourceMessageId, String groupCode) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_order (
                    id, hotel_id, order_key_type, order_business_key, active_business_key,
                    temporary_order_code, order_status, business_key_source, display_name,
                    source_message_id, version, latest_activity_at, created_at, updated_at
                )
                VALUES (?, 'HOTEL-TEST', 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'USER_CONFIRMED', ?,
                    ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, orderId, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
    }

    private void insertTask(Long taskId, Long orderId, Long sourceMessageId) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_task (
                    id, hotel_id, order_id, source_message_id, ai_transition_id,
                    result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
                    task_status, queue_participation, execution_order, blocked_until_parent_completed,
                    version, created_at, updated_at
                )
                VALUES (?, 'HOTEL-TEST', ?, ?, ?, 'normal_task', 'Update Booking', 'UPDATE_BOOKING',
                    'UPDATE_BOOKING', 'manual_invoice_test', 'READY', 1, 1, 0, 0,
                    CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, taskId, orderId, sourceMessageId, taskId - 1);
    }
}
