package cn.yunzhixue.ability.center.examsprint.adapter.http;

import cn.yunzhixue.ability.center.AbilityCenterRuntimeApplication;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StreamUtils;

import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;
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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(classes = AbilityCenterRuntimeApplication.class)
@AutoConfigureMockMvc
class ExamSprintReportControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ExamSprintReportRepository reportRepository;

    @Test
    void createReportReturnsAcceptedResponse() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequestJson()))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.data.reportId").isNotEmpty())
                .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                .andExpect(jsonPath("$.data.generationStatus").value("PENDING"));
    }

    @Test
    void createReportWithInvalidPayloadReturnsValidationError() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidRequestJson()))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    void createAchievementReportDownloadAndPreviewHtml() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validAchievementRequestJson()))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.data.reportId").isNotEmpty())
                .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                .andExpect(jsonPath("$.data.generationStatus").value("PENDING"))
                .andReturn();

        String reportId = readJson(createResult).at("/data/reportId").asText();
        JsonNode queryBody = waitForSuccessfulReport(reportId);
        URI downloadUri = URI.create(queryBody.at("/data/downloadUrl").asText());
        URI previewHtmlUri = URI.create(queryBody.at("/data/previewHtmlUrl").asText());

        mockMvc.perform(get(downloadUri))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PDF));

        MvcResult previewResult = mockMvc.perform(get(previewHtmlUri))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
                .andReturn();

        assertThat(previewResult.getResponse().getContentAsString(StandardCharsets.UTF_8))
                .contains("高考英语临考突击学习成果报告")
                .contains("模块一：词汇量对比")
                .contains("模块二：试卷熟词量对比")
                .contains("模块三：实考生词命中状况")
                .contains("achievement-bar-chart")
                .doesNotContain("echarts");
    }

    @Test
    void createAchievementReportWithInvalidPayloadReturnsValidationError() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidAchievementRequestJson()))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    void getCreatedReportDownloadUrlReturnsPdfContent() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequestJson()))
                .andExpect(status().isAccepted())
                .andReturn();

        String reportId = readJson(createResult).at("/data/reportId").asText();
        JsonNode queryBody = waitForSuccessfulReport(reportId);
        URI downloadUri = URI.create(queryBody.at("/data/downloadUrl").asText());

        mockMvc.perform(get(downloadUri))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PDF));
    }

    @Test
    void expiredReportDownloadIsRejected() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequestJson()))
                .andExpect(status().isAccepted())
                .andReturn();

        String reportId = readJson(createResult).at("/data/reportId").asText();
        JsonNode queryBody = waitForSuccessfulReport(reportId);
        URI downloadUri = URI.create(queryBody.at("/data/downloadUrl").asText());
        ExamSprintReport report = reportRepository.findById(reportId).orElseThrow();
        reportRepository.save(new ExamSprintReport(
                report.reportId(),
                report.reportType(),
                report.payload(),
                report.generationStatus(),
                report.createdAt(),
                report.updatedAt(),
                report.createdAt().minusSeconds(1),
                report.storageObjectKey(),
                report.fileName(),
                report.failureReason()));

        mockMvc.perform(get(downloadUri))
                .andExpect(status().isInternalServerError())
                .andExpect(jsonPath("$.code").value("EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE"));

        assertThat(reportRepository.findById(reportId).orElseThrow().generationStatus())
                .isEqualTo(ExamSprintReportGenerationStatus.EXPIRED);
    }

    private String validRequestJson() {
        return readRequestFromClasspath("requests/exam-sprint-outlook-report-request.json");
    }

    private String invalidRequestJson() {
        try {
            com.fasterxml.jackson.databind.node.ObjectNode request =
                    (com.fasterxml.jackson.databind.node.ObjectNode) objectMapper.readTree(validRequestJson());
            ((com.fasterxml.jackson.databind.node.ObjectNode) request.path("payload").path("reportMetadata"))
                    .remove("learnerName");
            return objectMapper.writeValueAsString(request);
        } catch (Exception exception) {
            throw new IllegalStateException("Failed to build invalid request json", exception);
        }
    }

    private String validAchievementRequestJson() {
        return readRequestFromClasspath("requests/exam-sprint-achievement-report-request.json");
    }

    private String invalidAchievementRequestJson() {
        return readRequestFromClasspath("requests/exam-sprint-achievement-report-invalid-request.json");
    }

    private String readRequestFromClasspath(String resourcePath) {
        try (InputStream inputStream = new ClassPathResource(resourcePath).getInputStream()) {
            return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        } catch (Exception exception) {
            throw new IllegalStateException("Failed to read request fixture from classpath: " + resourcePath, exception);
        }
    }

    private JsonNode readJson(MvcResult result) throws Exception {
        return objectMapper.readTree(result.getResponse().getContentAsString());
    }

    private JsonNode waitForSuccessfulReport(String reportId) throws Exception {
        JsonNode queryBody = null;
        for (int attempt = 0; attempt < 30; attempt++) {
            MvcResult queryResult = mockMvc.perform(get("/api/exam-sprint/reports/{reportId}", reportId))
                    .andExpect(status().isOk())
                    .andReturn();
            queryBody = readJson(queryResult);
            if ("SUCCESS".equals(queryBody.at("/data/generationStatus").asText())) {
                return queryBody;
            }
            Thread.sleep(100L);
        }
        return queryBody;
    }
}
