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

import cn.yunzhixue.ability.center.AbilityCenterRuntimeApplication;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
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;

    // WHY: The runtime fixture is the curl-ready upstream payload contract, so async creation must accept it without a wrapper.
    @Test
    void createReportReturnsAcceptedResponse() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/outlook-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"));
    }

    // WHY: The synchronous path must render the same root-level upstream payload clients submit in production.
    @Test
    void createOutlookReportSyncReturnsDownloadUrlAndPdfIsDownloadable() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/outlook-reports/sync")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validRequestJson()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.reportId").isNotEmpty())
                .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                .andExpect(jsonPath("$.data.downloadUrl").isNotEmpty())
                .andReturn();

        URI downloadUri = URI.create(readJson(createResult).at("/data/downloadUrl").asText());

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

    // WHY: Missing required upstream vocabulary fields should fail validation before report generation starts.
    @Test
    void createReportWithInvalidPayloadReturnsValidationError() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/outlook-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidRequestJson()))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    // WHY: Achievement async creation should use the same curl-ready fixture style as independent report endpoints.
    @Test
    void createAchievementReportDownloadUrlReturnsPdfContent() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/achievement-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());

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

    // WHY: The achievement sync route shares runtime wiring with outlook and guards against breaking cross-report PDF downloads.
    @Test
    void createAchievementReportSyncReturnsDownloadUrlAndPdfIsDownloadable() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/achievement-reports/sync")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(validAchievementRequestJson()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                .andExpect(jsonPath("$.data.downloadUrl").isNotEmpty())
                .andReturn();

        URI downloadUri = URI.create(readJson(createResult).at("/data/downloadUrl").asText());

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

    // WHY: Achievement fixtures document the upstream caller contract and must not drift back to legacy renderer DTO shape.
    @Test
    void achievementSyncFixtureUsesCallerPascalCasePayload() throws Exception {
        JsonNode payload = objectMapper.readTree(validAchievementRequestJson());

        assertThat(payload.path("StudentName").asText()).isEqualTo("吴泓妤");
        assertThat(payload.path("StageName").asText()).isEqualTo("高考");
        assertThat(payload.path("TestPaperTitle").asText()).isEqualTo("2024真题");
        assertThat(payload.path("StudentImproveWordCount").asInt()).isEqualTo(19);
        assertThat(payload.path("TestPaperImprovedWordCount").asInt()).isEqualTo(4);
        assertThat(payload.path("PaperMasteryHitRate").asDouble()).isEqualTo(1.93);
        assertThat(payload.path("ImproveStudyEfficiency").asDouble()).isEqualTo(0.48);
        assertThat(payload.path("TestPaperImprovedWords").get(0).asText()).isEqualTo(" number ");
        assertThat(payload.has("summaryMetrics")).isFalse();
        assertThat(payload.has("vocabularyComparison")).isFalse();
        assertThat(payload.has("paperKnownWordsComparison")).isFalse();
        assertThat(payload.has("examUnknownWordsHitStatus")).isFalse();
    }

    // WHY: The outlook fixture documents the client contract and must not drift back to legacy wrapper or text-field shapes.
    @Test
    void outlookSyncFixtureUsesUpstreamVocabularyPayloadWithoutUnitStrippingTextFields() throws Exception {
        JsonNode payload = objectMapper.readTree(validRequestJson());

        assertThat(payload.path("StudentName").asText()).isEqualTo("20260318测试");
        assertThat(payload.path("StageExaminName").asText()).isEqualTo("中考");
        assertThat(payload.has("payload")).isFalse();
        assertThat(payload.findValuesAsText("learningEfficiencyText")).isEmpty();
        assertThat(payload.findValuesAsText("unknownWordHitRateText")).isEmpty();
        assertThat(payload.findValuesAsText("beforeText")).isEmpty();
        assertThat(payload.findValuesAsText("afterText")).isEmpty();
        assertThat(payload.findValuesAsText("growthText")).isEmpty();
    }

    // WHY: Achievement validation failures must keep the same public error envelope as outlook validation failures.
    @Test
    void createAchievementReportWithInvalidPayloadReturnsValidationError() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/achievement-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidAchievementRequestJson()))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    // WHY: Async outlook reports should progress from the root-level upstream payload to downloadable PDF content.
    @Test
    void getCreatedReportDownloadUrlReturnsPdfContent() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/outlook-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));
    }

    // WHY: Expiry handling must reject stale download URLs even when reports are created from the upstream payload contract.
    @Test
    void expiredReportDownloadIsRejected() throws Exception {
        MvcResult createResult = mockMvc.perform(post("/api/exam-sprint/outlook-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.content(),
                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(ReportGenerationStatus.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());
            request.remove("StudentName");
            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;
    }
}
