package com.mosquito.project.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mosquito.project.persistence.entity.ShortLinkEntity;
import com.mosquito.project.persistence.entity.UserInviteEntity;
import com.mosquito.project.persistence.repository.UserInviteRepository;
import com.mosquito.project.service.PosterRenderService;
import com.mosquito.project.service.ShareConfigService;
import com.mosquito.project.service.ShortLinkService;
import com.mosquito.project.support.TestAuthSupport;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Map;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(UserExperienceController.class)
@Import(com.mosquito.project.config.ControllerTestConfig.class)
@TestPropertySource(properties = {
    "spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration," +
    "org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration," +
    "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"
})
class UserExperienceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private ShortLinkService shortLinkService;

    @MockBean
    private UserInviteRepository userInviteRepository;

    @MockBean
    private PosterRenderService posterRenderService;

    @MockBean
    private ShareConfigService shareConfigService;

    @MockBean
    private com.mosquito.project.persistence.repository.UserRewardRepository userRewardRepository;

    @Test
    void shouldReturnInvitationInfo_withShortLink() throws Exception {
        ShortLinkEntity e = new ShortLinkEntity();
        e.setCode("inv12345");
        e.setOriginalUrl("https://example.com/landing?activityId=1&inviter=2");
        when(shortLinkService.create(anyString())).thenReturn(e);
        when(shareConfigService.buildShareUrl(anyLong(), anyLong(), anyString(), any())).thenReturn("https://example.com/landing?activityId=1&inviter=2");

        mockMvc.perform(get("/api/v1/me/invitation-info")
                .param("activityId", "1")
                .param("userId", "2")
                .accept(MediaType.APPLICATION_JSON)
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.code").value("inv12345"))
            .andExpect(jsonPath("$.data.path").value("/r/inv12345"));
    }

    @Test
    void shouldReturnInvitedFriends_withPagination() throws Exception {
        UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked");
        UserInviteEntity b = new UserInviteEntity(); b.setInviteeUserId(11L); b.setStatus("registered");
        UserInviteEntity c = new UserInviteEntity(); c.setInviteeUserId(12L); c.setStatus("ordered");
        when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a,b,c));

        mockMvc.perform(get("/api/v1/me/invited-friends")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "1")
                .param("size", "1")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data[0].status").value("registered"));
    }

    @Test
    void shouldReturnPosterImage() throws Exception {
        when(posterRenderService.renderPoster(anyLong(), anyLong(), anyString())).thenReturn("placeholder".getBytes());

        mockMvc.perform(get("/api/v1/me/poster/image")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(header().string("Content-Type", "image/png"));
    }

    @Test
    void posterImage_shouldReturn500_whenRenderFails() throws Exception {
        when(posterRenderService.renderPoster(anyLong(), anyLong(), anyString()))
            .thenThrow(new RuntimeException("render failed"));

        mockMvc.perform(get("/api/v1/me/poster/image")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isInternalServerError());
    }

    @Test
    void shouldReturnPosterConfig() throws Exception {
        mockMvc.perform(get("/api/v1/me/poster/config")
                .param("template", "default")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.template").value("default"));
    }

    @Test
    void shouldReturnPosterHtml() throws Exception {
        when(posterRenderService.renderPosterHtml(anyLong(), anyLong(), anyString())).thenReturn("<html></html>");

        mockMvc.perform(get("/api/v1/me/poster/html")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.TEXT_HTML));
    }

    @Test
    void posterHtml_shouldReturn500_whenRenderFails() throws Exception {
        when(posterRenderService.renderPosterHtml(anyLong(), anyLong(), anyString()))
            .thenThrow(new RuntimeException("render failed"));

        mockMvc.perform(get("/api/v1/me/poster/html")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isInternalServerError());
    }

    @Test
    void shouldReturnRewards_withPagination() throws Exception {
        var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now());
        var r2 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r2.setType("coupon"); r2.setPoints(0); r2.setCreatedAt(java.time.OffsetDateTime.now().minusDays(1));
        when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1, r2));

        mockMvc.perform(get("/api/v1/me/rewards")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "0")
                .param("size", "1")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data[0].type").value("points"));
    }

    @Test
    void shouldReturnEmptyList_whenInvitedFriendsPaginationExceedsSize() throws Exception {
        UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked");
        when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a));

        mockMvc.perform(get("/api/v1/me/invited-friends")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "10")
                .param("size", "20")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data").isEmpty());
    }

    @Test
    void shouldReturnEmptyList_whenRewardsPaginationExceedsSize() throws Exception {
        var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now());
        when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1));

        mockMvc.perform(get("/api/v1/me/rewards")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "10")
                .param("size", "20")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data").isEmpty());
    }

    @Test
    void shouldReturnShareMeta() throws Exception {
        Map<String, Object> meta = Map.of("title", "活动标题", "description", "活动描述");
        when(shareConfigService.getShareMeta(anyLong(), anyLong(), anyString())).thenReturn(meta);

        mockMvc.perform(get("/api/v1/me/share-meta")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.title").value("活动标题"))
            .andExpect(jsonPath("$.data.description").value("活动描述"));
    }

    @Test
    void shouldReturnShareMeta_withCustomTemplate() throws Exception {
        Map<String, Object> meta = Map.of("title", "自定义模板");
        when(shareConfigService.getShareMeta(anyLong(), anyLong(), eq("custom"))).thenReturn(meta);

        mockMvc.perform(get("/api/v1/me/share-meta")
                .param("activityId", "1")
                .param("userId", "2")
                .param("template", "custom")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.title").value("自定义模板"));
    }

    @Test
    void shouldHandleZeroOrNegativeSize_inInvitedFriends() throws Exception {
        UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked");
        UserInviteEntity b = new UserInviteEntity(); b.setInviteeUserId(11L); b.setStatus("registered");
        when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a, b));

        mockMvc.perform(get("/api/v1/me/invited-friends")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "0")
                .param("size", "0")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data").isEmpty());
    }

    @Test
    void shouldHandleZeroOrNegativeSize_inRewards() throws Exception {
        var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now());
        var r2 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r2.setType("coupon"); r2.setPoints(0); r2.setCreatedAt(java.time.OffsetDateTime.now().minusDays(1));
        when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1, r2));

        mockMvc.perform(get("/api/v1/me/rewards")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "0")
                .param("size", "0")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data").isEmpty());
    }

    @Test
    void shouldHandleNegativePage_inInvitedFriends() throws Exception {
        UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked");
        when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a));

        mockMvc.perform(get("/api/v1/me/invited-friends")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "-1")
                .param("size", "20")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data[0].status").value("clicked"));
    }

    @Test
    void shouldHandleNegativePage_inRewards() throws Exception {
        var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now());
        when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1));

        mockMvc.perform(get("/api/v1/me/rewards")
                .param("activityId", "1")
                .param("userId", "2")
                .param("page", "-1")
                .param("size", "20")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data[0].type").value("points"));
    }

    @Test
    void shouldMaskPhoneNumber_inInvitedFriends() throws Exception {
        UserInviteEntity a = new UserInviteEntity();
        a.setInviteeUserId(123L);
        a.setStatus("registered");
        when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a));

        mockMvc.perform(get("/api/v1/me/invited-friends")
                .param("activityId", "1")
                .param("userId", "2")
                .header("X-API-Key", TestAuthSupport.RAW_API_KEY)
                .header("Authorization", "Bearer test-token"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data[0].maskedPhone").value("138****0123"));
    }
}
