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.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 cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleStatus;
import cn.nianxx.thhotel.platform.access.domain.PlatformPermissionEntity;
import cn.nianxx.thhotel.platform.access.domain.PlatformRoleEntity;
import cn.nianxx.thhotel.platform.access.repository.PlatformAccessRepository;
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 cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
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.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:frontend_read_auth_cp1;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
                "auth.bootstrap.admin.username=cp1-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 FrontendReadAuthorizationControllerTest {

    private static final String HOTEL_ID = "HOTEL-TEST";
    private static final String OTHER_HOTEL_ID = "HOTEL-OTHER";
    private static final String ORIGINAL_ONLY_USERNAME = "cp2-original-only";
    private static final String ORIGINAL_ONLY_PASSWORD = "OriginalOnly@123456";
    private static final String ORIGINAL_ONLY_ROLE_CODE = "CP2_ORIGINAL_ONLY";

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private SourceMessageCaptureService captureService;
    @Autowired
    private PlatformIdentityRepository identityRepository;
    @Autowired
    private PlatformAccessRepository accessRepository;
    @Autowired
    private PlatformHotelRepository hotelRepository;
    @Autowired
    private AuthPasswordService passwordService;

    @BeforeEach
    void ensureNoPermissionUser() {
        PlatformUserEntity user = identityRepository.findUserByUsername("cp1-no-permission")
                .orElseGet(() -> {
                    LocalDateTime now = LocalDateTime.now();
                    PlatformUserEntity created = new PlatformUserEntity();
                    created.setUsername("cp1-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_ID, true);
        ensureOriginalOnlyUser();
    }

    @Test
    void shouldRejectReservationReadWhenTokenMissing() throws Exception {
        mockMvc.perform(get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
    }

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

        performAuthorized(mockMvc, token, get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
    }

    @Test
    void shouldRejectSourceMessageReadWhenTokenMissing() throws Exception {
        mockMvc.perform(get("/api/source-messages")
                        .param("hotel_id", HOTEL_ID))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
    }

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

        performAuthorized(mockMvc, token, get("/api/source-messages")
                        .param("hotel_id", HOTEL_ID))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
    }

    @Test
    void shouldRejectSourceMessageOriginalAndConversationWhenTokenMissing() throws Exception {
        mockMvc.perform(get("/api/source-messages/{id}/original", 970000000000000901L))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));

        mockMvc.perform(get("/api/source-messages/{id}/conversation", 970000000000000901L))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
    }

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

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", 970000000000000902L))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", 970000000000000902L))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
    }

    @Test
    void shouldRejectSourceMessageOriginalAndConversationWhenSummaryPermissionMissing() throws Exception {
        String token = loginToken(mockMvc, ORIGINAL_ONLY_USERNAME, ORIGINAL_ONLY_PASSWORD);
        SourceMessageCaptureResult source = captureHotelSourceMessage();

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", source.inboxId()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", source.inboxId()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
    }

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

        performAuthorized(mockMvc, token, get("/api/source-messages")
                        .param("hotel_id", OTHER_HOTEL_ID))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
    }

    @Test
    void shouldRejectReservationAndSourceMessageDetailAcrossHotels() throws Exception {
        String token = loginToken(mockMvc, "cp1-admin", "Admin@123456");
        SourceMessageCaptureResult source = captureOtherHotelSourceMessage();
        Long orderId = 970000000000000101L;
        Long taskId = 970000000000000301L;
        insertOtherHotelOrderTask(orderId, taskId, source.inboxId());

        performAuthorized(mockMvc, token, get("/api/reservation/orders/{orderId}", orderId))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));

        performAuthorized(mockMvc, token, get("/api/reservation/tasks/{taskId}", taskId))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));

        performAuthorized(mockMvc, token, get("/api/reservation/tasks/{taskId}/audits", taskId))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}", source.inboxId()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
    }

    @Test
    void shouldRejectSourceMessageOriginalAndConversationAcrossHotels() throws Exception {
        String token = loginToken(mockMvc, "cp1-admin", "Admin@123456");
        SourceMessageCaptureResult source = captureOtherHotelSourceMessage();

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", source.inboxId()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));

        performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", source.inboxId()))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
    }

    @Test
    void shouldKeepSuperAgentTaskResultEndpointOutsideFrontendLoginInterceptor() throws Exception {
        mockMvc.perform(post("/api/integrations/superagent/task-results")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error_code").value("AUTH_HEADER_MISSING"));
    }

    @Test
    void shouldKeepAiQueryEndpointOutsideFrontendLoginInterceptor() throws Exception {
        mockMvc.perform(post("/api/ai-query/v1/case-context")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error.code").value("AUTH_HEADER_MISSING"));
    }

    @Test
    void shouldKeepMcpEndpointOutsideFrontendLoginInterceptor() throws Exception {
        mockMvc.perform(post("/mcp")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "jsonrpc": "2.0",
                                  "id": "cp1-mcp-auth-001",
                                  "method": "tools/list",
                                  "params": {}
                                }
                                """))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.error.data.code").value("MCP_AUTH_INVALID"));
    }

    private SourceMessageCaptureResult captureOtherHotelSourceMessage() {
        return captureService.capture(new CaptureSourceMessageCommand(
                OTHER_HOTEL_ID,
                "AGENTBUS",
                "EMAIL",
                "cp1-other-hotel-mail-001",
                "cp1-other-hotel-thread-001",
                "frame-cp1-other",
                "session-cp1-other",
                Instant.parse("2026-07-13T01:00:00Z"),
                "guest@example.test",
                "Other hotel message",
                "Other hotel body",
                "<html><body>Other hotel body</body></html>",
                "{\"source\":{\"external_message_id\":\"cp1-other-hotel-mail-001\"}}",
                "agentbus-outlook-v1",
                List.of()));
    }

    private SourceMessageCaptureResult captureHotelSourceMessage() {
        return captureService.capture(new CaptureSourceMessageCommand(
                HOTEL_ID,
                "AGENTBUS",
                "EMAIL",
                "cp2-hotel-mail-001",
                "cp2-hotel-thread-001",
                "frame-cp2-hotel",
                "session-cp2-hotel",
                Instant.parse("2026-07-13T02:00:00Z"),
                "guest@example.test",
                "Current hotel message",
                "Current hotel body",
                "<html><body>Current hotel body</body></html>",
                "{\"source\":{\"external_message_id\":\"cp2-hotel-mail-001\"}}",
                "agentbus-outlook-v1",
                List.of()));
    }

    private void ensureOriginalOnlyUser() {
        PlatformUserEntity user = identityRepository.findUserByUsername(ORIGINAL_ONLY_USERNAME)
                .orElseGet(() -> {
                    LocalDateTime now = LocalDateTime.now();
                    PlatformUserEntity created = new PlatformUserEntity();
                    created.setUsername(ORIGINAL_ONLY_USERNAME);
                    created.setPasswordHash(passwordService.hash(ORIGINAL_ONLY_PASSWORD));
                    created.setDisplayName("仅原文权限用户");
                    created.setUserStatus(PlatformUserStatus.ACTIVE.name());
                    created.setSuperAdmin(false);
                    created.setPasswordChangedAt(now);
                    created.setCreatedAt(now);
                    created.setUpdatedAt(now);
                    identityRepository.insertUser(created);
                    return created;
                });
        PlatformRoleEntity role = accessRepository.findRoleByCode(ORIGINAL_ONLY_ROLE_CODE)
                .orElseGet(() -> {
                    LocalDateTime now = LocalDateTime.now();
                    PlatformRoleEntity created = new PlatformRoleEntity();
                    created.setRoleCode(ORIGINAL_ONLY_ROLE_CODE);
                    created.setRoleName("CP2 仅原文权限测试角色");
                    created.setRoleStatus(PlatformRoleStatus.ACTIVE.name());
                    created.setSystemBuiltin(false);
                    created.setCreatedAt(now);
                    created.setUpdatedAt(now);
                    accessRepository.insertRole(created);
                    return created;
                });
        PlatformPermissionEntity originalPermission = accessRepository
                .findPermissionByCode(PlatformPermissionCode.SOURCE_MESSAGE_ORIGINAL_READ.name())
                .orElseThrow();
        accessRepository.replaceRolePermissions(role.getId(), List.of(originalPermission.getId()));
        accessRepository.ensureUserRole(user.getId(), role.getId());
        hotelRepository.ensureUserHotel(user.getId(), HOTEL_ID, true);
    }

    private void insertOtherHotelOrderTask(Long orderId, Long taskId, Long sourceMessageId) {
        Long transitionId = taskId - 1;
        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, latest_activity_at
                )
                VALUES (?, ?, 'GROUP_CODE', 'GRP-CP1-OTHER', 'GRP-CP1-OTHER',
                    'TMP-CP1-OTHER', 'ACTIVE', 'VISIBLE', 'AI_CANDIDATE', 'GRP-CP1-OTHER',
                    ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, orderId, OTHER_HOTEL_ID, sourceMessageId);
        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 (?, ?, ?, ?, 1, 1, 1, 'S02', 'cp1_skill',
                    'normal_task', 'New Booking', 'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block',
                    'current', 'GRP-CP1-OTHER', ?, ?, 0, '{}', '{}', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, transitionId, OTHER_HOTEL_ID, transitionId - 1, sourceMessageId, "1".repeat(64),
                transitionId.toString());
        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 (?, ?, ?, ?, ?, 'normal_task', 'New Booking',
                    'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block', 'PENDING_CONFIRM',
                    1, 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, taskId, OTHER_HOTEL_ID, orderId, sourceMessageId, transitionId);
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_task_card (
                    id, hotel_id, task_id, task_card_type, field_contract_version,
                    ai_payload_json, created_at, updated_at
                )
                VALUES (?, ?, ?, 'NEW_BOOKING', 'test', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, taskId + 1, OTHER_HOTEL_ID, taskId);
    }
}
