package kr.itn.itnhub.project;

import kr.itn.itnhub.AbstractDbTest;
import kr.itn.itnhub.mattermost.MattermostClient;
import kr.itn.itnhub.org.Organization;
import kr.itn.itnhub.org.OrganizationMapper;
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.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureMockMvc
@WithMockUser(username = "admin", roles = "ADMIN")
class ProjectControllerTest extends AbstractDbTest {

    @Autowired
    MockMvc mvc;

    @Autowired
    OrganizationMapper orgMapper;

    @Autowired
    JdbcTemplate jdbc;

    @MockBean
    MattermostClient mattermost;

    private Long orgId;

    @BeforeEach
    void setUp() {
        jdbc.update("delete from organization");

        Organization org = new Organization();
        org.setOrgNo("001");
        org.setOrgName("국제방송교류재단");
        org.setChannelSlug("001");
        orgMapper.upsertBySeed(org);
        orgId = orgMapper.findByOrgNoAndOrgName("001", "국제방송교류재단").getId();
    }

    @Test
    void 목록은_기관현황에_연락이력_보고서_건수를_붙여_내려준다() throws Exception {
        mvc.perform(get("/api/projects"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(1))
                .andExpect(jsonPath("$[0].org.orgNo").value("001"))
                .andExpect(jsonPath("$[0].contactCount").value(0))
                .andExpect(jsonPath("$[0].reportCount").value(0))
                .andExpect(jsonPath("$[0].lastContactedOn").doesNotExist());
    }

    @Test
    void 연락이력을_등록하면_최근순으로_돌려주고_작성자는_로그인_사용자다() throws Exception {
        mvc.perform(post("/api/orgs/{id}/contact-logs", orgId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-20", "method": "전화", "summary": "신청 확인"}
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(1))
                .andExpect(jsonPath("$[0].author").value("admin"))
                .andExpect(jsonPath("$[0].contactedOn").value("2026-07-20"));

        mvc.perform(post("/api/orgs/{id}/contact-logs", orgId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-25", "method": "메일", "summary": "자료 요청"}
                                """))
                .andExpect(status().isOk())
                // 최근 연락일이 위로 온다
                .andExpect(jsonPath("$[0].contactedOn").value("2026-07-25"))
                .andExpect(jsonPath("$[1].contactedOn").value("2026-07-20"));

        mvc.perform(get("/api/projects"))
                .andExpect(jsonPath("$[0].contactCount").value(2))
                .andExpect(jsonPath("$[0].lastContactedOn").value("2026-07-25"));
    }

    @Test
    void 연락이력_수정은_내용만_바꾸고_작성자는_유지한다() throws Exception {
        String created = mvc.perform(post("/api/orgs/{id}/contact-logs", orgId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-20", "method": "전화", "summary": "처음"}
                                """))
                .andReturn().getResponse().getContentAsString();
        int logId = com.jayway.jsonpath.JsonPath.parse(created).read("$[0].id", Integer.class);

        mvc.perform(put("/api/orgs/{id}/contact-logs/{logId}", orgId, logId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-21", "method": "방문", "summary": "고침"}
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].summary").value("고침"))
                .andExpect(jsonPath("$[0].method").value("방문"))
                .andExpect(jsonPath("$[0].author").value("admin"));
    }

    @Test
    void 연락이력을_삭제하면_목록에서_빠진다() throws Exception {
        String created = mvc.perform(post("/api/orgs/{id}/contact-logs", orgId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-20", "method": "전화", "summary": "지울 것"}
                                """))
                .andReturn().getResponse().getContentAsString();
        int logId = com.jayway.jsonpath.JsonPath.parse(created).read("$[0].id", Integer.class);

        mvc.perform(delete("/api/orgs/{id}/contact-logs/{logId}", orgId, logId).with(csrf()))
                .andExpect(status().isNoContent());

        mvc.perform(get("/api/orgs/{id}/contact-logs", orgId))
                .andExpect(jsonPath("$.length()").value(0));
    }

    @Test
    void 내용이_비면_400이다() throws Exception {
        mvc.perform(post("/api/orgs/{id}/contact-logs", orgId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"contactedOn": "2026-07-20", "method": "전화", "summary": ""}
                                """))
                .andExpect(status().isBadRequest());
    }

    @Test
    void 보고서를_올리면_목록에_쌓이고_다운로드된다() throws Exception {
        MockMultipartFile file = new MockMultipartFile("file", "최종보고서.pdf",
                "application/pdf", "보고서 내용".getBytes(java.nio.charset.StandardCharsets.UTF_8));

        String uploaded = mvc.perform(multipart("/api/orgs/{id}/reports", orgId).file(file).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(1))
                .andExpect(jsonPath("$[0].fileName").value("최종보고서.pdf"))
                .andExpect(jsonPath("$[0].uploadedBy").value("admin"))
                .andReturn().getResponse().getContentAsString();
        int reportId = com.jayway.jsonpath.JsonPath.parse(uploaded).read("$[0].id", Integer.class);

        // 한글 파일명은 RFC 5987로 인코딩돼 내려간다
        mvc.perform(get("/api/orgs/{id}/reports/{reportId}", orgId, reportId))
                .andExpect(status().isOk())
                .andExpect(header().string("Content-Disposition",
                        org.hamcrest.Matchers.containsString("filename*=UTF-8''")));

        mvc.perform(get("/api/projects"))
                .andExpect(jsonPath("$[0].reportCount").value(1));
    }

    @Test
    void 같은_기관에_여러_번_올리면_최신순으로_쌓인다() throws Exception {
        mvc.perform(multipart("/api/orgs/{id}/reports", orgId)
                        .file(new MockMultipartFile("file", "1차.pdf", "application/pdf", "a".getBytes()))
                        .with(csrf()))
                .andExpect(status().isOk());
        mvc.perform(multipart("/api/orgs/{id}/reports", orgId)
                        .file(new MockMultipartFile("file", "2차.pdf", "application/pdf", "b".getBytes()))
                        .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].fileName").value("2차.pdf"));
    }

    @Test
    void 빈_파일은_400이다() throws Exception {
        mvc.perform(multipart("/api/orgs/{id}/reports", orgId)
                        .file(new MockMultipartFile("file", "empty.pdf", "application/pdf", new byte[0]))
                        .with(csrf()))
                .andExpect(status().isBadRequest());
    }

    @Test
    void 보고서를_삭제하면_목록에서_빠진다() throws Exception {
        String uploaded = mvc.perform(multipart("/api/orgs/{id}/reports", orgId)
                        .file(new MockMultipartFile("file", "지울보고서.pdf", "application/pdf", "x".getBytes()))
                        .with(csrf()))
                .andReturn().getResponse().getContentAsString();
        int reportId = com.jayway.jsonpath.JsonPath.parse(uploaded).read("$[0].id", Integer.class);

        mvc.perform(delete("/api/orgs/{id}/reports/{reportId}", orgId, reportId).with(csrf()))
                .andExpect(status().isNoContent());

        mvc.perform(get("/api/orgs/{id}/reports", orgId))
                .andExpect(jsonPath("$.length()").value(0));
        assertThat(jdbc.queryForObject("select count(*) from org_report", Integer.class)).isZero();
    }

    @Test
    void 없는_기관이면_404다() throws Exception {
        mvc.perform(get("/api/orgs/{id}/contact-logs", orgId + 99999L))
                .andExpect(status().isNotFound());
    }
}
