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

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageMedia;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Test;
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.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(
        classes = ThHotelApplication.class,
        properties = "source-message.original-read.access-key=test-original-read-key")
@AutoConfigureMockMvc
@ActiveProfiles("test")
class SourceMessageControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private SourceMessageCaptureService captureService;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void shouldListAndReadSummaryWithoutOriginalContentOrMediaUrls() throws Exception {
        SourceMessageCaptureResult result = captureService.capture(command(
                "mail-api-001",
                "conversation-api-001",
                "Please update guest name. Private phone 13800138000.",
                "<html><body>Private HTML</body></html>",
                "https://media.example.test/private.pdf?token=secret"
        ));

        mockMvc.perform(get("/api/source-messages")
                        .param("hotelId", "HOTEL-TEST")
                        .param("externalConversationId", "conversation-api-001")
                        .param("pageNum", "1")
                        .param("pageSize", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].id").value(result.inboxId().toString()))
                .andExpect(jsonPath("$.items[0].externalMessageId").value("mail-api-001"))
                .andExpect(jsonPath("$.items[0].externalConversationId").value("conversation-api-001"))
                .andExpect(jsonPath("$.items[0].captureStatus").value("RECEIVED"))
                .andExpect(jsonPath("$.items[0].safeSnippet").value(containsString("Please update guest name")))
                .andExpect(content().string(not(containsString("13800138000"))))
                .andExpect(content().string(not(containsString("Private HTML"))))
                .andExpect(content().string(not(containsString("media.example.test"))))
                .andExpect(content().string(not(containsString("token=secret"))))
                .andExpect(content().string(not(containsString("payloadJson"))));

        mockMvc.perform(get("/api/source-messages/{id}", result.inboxId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(result.inboxId().toString()))
                .andExpect(jsonPath("$.externalMessageId").value("mail-api-001"))
                .andExpect(jsonPath("$.safeSnippet").value(containsString("Please update guest name")))
                .andExpect(content().string(not(containsString("Private HTML"))))
                .andExpect(content().string(not(containsString("media.example.test"))))
                .andExpect(content().string(not(containsString("payloadJson"))));
    }

    @Test
    void shouldRejectOriginalReadWithoutAccessKey() throws Exception {
        SourceMessageCaptureResult result = captureService.capture(command(
                "mail-original-denied-001",
                "conversation-original-denied-001",
                "Private original text",
                "<html><body>Private original HTML</body></html>",
                "https://media.example.test/denied.pdf?token=secret"
        ));

        mockMvc.perform(get("/api/source-messages/{id}/original", result.inboxId())
                        .header("X-TH-Hotel-Actor", "operator-001")
                        .header("X-TH-Hotel-Access-Scene", "reservation-detail"))
                .andExpect(status().isForbidden());
    }

    @Test
    void shouldReadOriginalContentWithAccessKeyAndRecordAudit() throws Exception {
        SourceMessageCaptureResult result = captureService.capture(command(
                "mail-original-001",
                "conversation-original-001",
                "Original text body with private context.",
                "<html><body>Original HTML body</body></html>",
                "https://media.example.test/original.pdf"
        ));

        mockMvc.perform(get("/api/source-messages/{id}/original", result.inboxId())
                        .header("X-TH-Hotel-Source-Original-Read-Key", "test-original-read-key")
                        .header("X-TH-Hotel-Actor", "operator-001")
                        .header("X-TH-Hotel-Access-Scene", "reservation-detail"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(result.inboxId().toString()))
                .andExpect(jsonPath("$.textBody").value(containsString("Original text body")))
                .andExpect(jsonPath("$.htmlBody").value(containsString("Original HTML body")))
                .andExpect(jsonPath("$.htmlSanitizeRequired").value(true))
                .andExpect(jsonPath("$.attachments[0].fileName").value("private.pdf"))
                .andExpect(jsonPath("$.attachments[0].externalUrl")
                        .value("https://media.example.test/original.pdf"));

        Long auditCount = jdbcTemplate.queryForObject("""
                SELECT COUNT(*)
                FROM platform_source_message_original_access_audit
                WHERE inbox_id = ?
                  AND actor_id = 'operator-001'
                  AND access_scene = 'reservation-detail'
                """, Long.class, result.inboxId());
        assert auditCount != null;
        org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(1L);
    }

    @Test
    void shouldReadConversationOriginalContentAndRelatedReservationSummaryWithoutFrontendKey() throws Exception {
        SourceMessageCaptureResult first = captureService.capture(conversationCommand(
                "mail-conversation-p0-001",
                "conversation-p0-001",
                "First full text body with booking context.",
                "<html><body><p>First HTML body</p><img src=\"cid:image-001\" onerror=\"alert(1)\" />"
                        + "<a href=\"javascript:alert(2)\">unsafe</a><script>alert(3)</script></body></html>",
                List.of(
                        media("INLINE_IMAGE", "image-001.png", "image/png", 100L,
                                "https://media.example.test/image-001.png?token=secret", "image-001"),
                        media("ATTACHMENT", "first.pdf", "application/pdf", 1000L,
                                "https://media.example.test/first.pdf?token=secret", "attachment-001")
                )));
        SourceMessageCaptureResult second = captureService.capture(conversationCommand(
                "mail-conversation-p0-002",
                "conversation-p0-001",
                "Second full text body.",
                "<html><body><p>Second HTML body</p></body></html>",
                List.of()));
        Long orderId = 940000000000000101L;
        Long transitionId = 940000000000000201L;
        Long taskId = 940000000000000301L;
        insertRelatedOrder(orderId, first.inboxId(), "GRP-CONVERSATION-P0-001");
        insertRelatedTransition(transitionId, first.inboxId(), "GRP-CONVERSATION-P0-001");
        insertRelatedTask(taskId, orderId, first.inboxId(), transitionId);

        mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", first.inboxId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.conversation.external_conversation_id").value("conversation-p0-001"))
                .andExpect(jsonPath("$.conversation.message_count").value(2))
                .andExpect(jsonPath("$.messages.length()").value(2))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].text_body")
                        .value(contains(containsString("First full text body"))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body")
                        .value(contains(containsString("First HTML body"))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body")
                        .value(contains(containsString("<script>alert(3)</script>"))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
                        .value(contains(containsString("First HTML body"))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
                        .value(contains(not(containsString("<script")))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
                        .value(contains(not(containsString("onerror")))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
                        .value(contains(not(containsString("javascript:")))))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_sanitize_required")
                        .value(contains(true)))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_render_mode")
                        .value(contains("SANITIZED_HTML")))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].inline_images[0].externalUrl")
                        .value(contains("https://media.example.test/image-001.png?token=secret")))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].attachments[0].externalUrl")
                        .value(contains("https://media.example.test/first.pdf?token=secret")))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].related_orders[0].order_id")
                        .value(contains(orderId.toString())))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].related_tasks[0].task_id")
                        .value(contains(taskId.toString())))
                .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-002')].text_body")
                        .value(contains(containsString("Second full text body"))));

        Long auditCount = jdbcTemplate.queryForObject("""
                SELECT COUNT(*)
                FROM platform_source_message_original_access_audit
                WHERE inbox_id IN (?, ?)
                  AND actor_id = 'system:source-message-conversation'
                  AND access_scene = 'source-message-conversation'
                """, Long.class, first.inboxId(), second.inboxId());
        org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(2L);
    }

    @Test
    void shouldHideSystemOrderFromConversationRelatedOrders() throws Exception {
        SourceMessageCaptureResult sourceMessage = captureService.capture(conversationCommand(
                "mail-conversation-hidden-order-001",
                "conversation-hidden-order-001",
                "Information only message.",
                "<html><body><p>Information only message.</p></body></html>",
                List.of()));
        Long orderId = 940000000000000401L;
        Long transitionId = 940000000000000501L;
        Long taskId = 940000000000000601L;
        insertHiddenRelatedOrder(orderId, sourceMessage.inboxId());
        insertRelatedTransition(transitionId, sourceMessage.inboxId(), "TMP-HIDDEN-CONVERSATION-001");
        insertRelatedTask(taskId, orderId, sourceMessage.inboxId(), transitionId);

        mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", sourceMessage.inboxId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.messages[0].related_orders.length()").value(0))
                .andExpect(jsonPath("$.messages[0].related_tasks[0].task_id").value(taskId.toString()))
                .andExpect(content().string(not(containsString("TMP-HIDDEN-CONVERSATION-001"))));
    }

    private CaptureSourceMessageCommand command(
            String externalMessageId,
            String externalConversationId,
            String textBody,
            String htmlBody,
            String mediaUrl) {
        return new CaptureSourceMessageCommand(
                "HOTEL-TEST",
                "AGENTBUS",
                "EMAIL",
                externalMessageId,
                externalConversationId,
                "frame-" + externalMessageId,
                "session-api",
                Instant.parse("2026-07-06T10:00:00Z"),
                "guest@example.test",
                "Reservation update",
                textBody,
                htmlBody,
                "{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
                "agentbus-outlook-v1",
                List.of(new CaptureSourceMessageMedia(
                        "ATTACHMENT",
                        "private.pdf",
                        "application/pdf",
                        1000L,
                        mediaUrl,
                        "attachment-api-001"))
        );
    }

    private CaptureSourceMessageCommand conversationCommand(
            String externalMessageId,
            String externalConversationId,
            String textBody,
            String htmlBody,
            List<CaptureSourceMessageMedia> mediaItems) {
        return new CaptureSourceMessageCommand(
                "HOTEL-TEST",
                "AGENTBUS",
                "EMAIL",
                externalMessageId,
                externalConversationId,
                "frame-" + externalMessageId,
                "session-api",
                Instant.parse("2026-07-06T10:00:00Z"),
                "guest@example.test",
                "Reservation update",
                textBody,
                htmlBody,
                "{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
                "agentbus-outlook-v1",
                mediaItems
        );
    }

    private CaptureSourceMessageMedia media(
            String mediaType,
            String fileName,
            String contentType,
            Long sizeBytes,
            String externalUrl,
            String externalMediaId) {
        return new CaptureSourceMessageMedia(mediaType, fileName, contentType, sizeBytes, externalUrl, externalMediaId);
    }

    private void insertRelatedOrder(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, created_at, updated_at
                )
                VALUES (?, 'HOTEL-TEST', 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'AI_CANDIDATE', ?,
                    ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, orderId, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
    }

    private void insertHiddenRelatedOrder(Long orderId, Long sourceMessageId) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_order (
                    id, hotel_id, order_key_type, order_business_key, active_business_key,
                    temporary_order_code, order_status, order_visibility, business_key_source, display_name,
                    source_message_id, version, created_at, updated_at
                )
                VALUES (?, 'HOTEL-TEST', 'TEMPORARY', NULL, NULL, ?, 'TEMPORARY',
                    'HIDDEN_SYSTEM', 'AI_CANDIDATE', ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, orderId, "TMP-HIDDEN-CONVERSATION-001", "TMP-HIDDEN-CONVERSATION-001", sourceMessageId);
    }

    private void insertRelatedTransition(Long transitionId, Long sourceMessageId, String groupCode) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_ai_transition (
                    id, hotel_id, batch_id, source_message_id, source_event_index, array_index,
                    execution_order, catalog_code, skill_id, result_type, ai_task_type,
                    system_task_type, task_card_type, task_subtype, current_or_history,
                    group_code, item_payload_sha256, item_idempotency_key, blocked_until_parent_completed,
                    ai_payload_json, case_keys_json, extracted_fields_json, created_at, updated_at
                )
                VALUES (?, 'HOTEL-TEST', ?, ?, 1, 1, 1, 'S02', 'conversation_skill',
                    'normal_task', 'New Booking', 'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block',
                    'current', ?, ?, ?, 0, '{}', '{}', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, transitionId, transitionId - 1, sourceMessageId, groupCode, "1".repeat(64),
                transitionId.toString());
    }

    private void insertRelatedTask(Long taskId, Long orderId, Long sourceMessageId, Long transitionId) {
        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', 'New Booking',
                    'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block', 'PENDING_CONFIRM',
                    1, 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, taskId, orderId, sourceMessageId, transitionId);
    }
}
