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

import cn.yunzhixue.ability.center.examsprint.application.report.ExamSprintReportApplicationService;
import cn.yunzhixue.ability.center.examsprint.application.report.ExamSprintReportApplicationService.ReportDownloadContent;
import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportWithUrlResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportDetailResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
import cn.yunzhixue.ability.center.kernel.BusinessException;
import cn.yunzhixue.ability.center.kernel.ErrorCode;
import cn.yunzhixue.ability.center.GlobalExceptionHandler;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

import org.mockito.ArgumentCaptor;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
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;

@WebMvcTest(ExamSprintReportController.class)
@Import({ExamSprintReportController.class, GlobalExceptionHandler.class})
class ExamSprintReportControllerWebMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ExamSprintReportApplicationService applicationService;

    // WHY: Independent outlook endpoints must forward the root-level upstream payload unchanged to the application layer.
    @Test
    void createOutlookReportUsesIndependentEndpoint() throws Exception {
        given(applicationService.createOutlookReport(any())).willReturn(new CreateExamSprintReportResponse(
                "report-001",
                ExamSprintReportType.OUTLOOK,
                ExamSprintReportGenerationStatus.PENDING,
                Instant.parse("2026-01-01T00:00:00Z"),
                Instant.parse("2026-01-02T00:00:00Z")));

        String requestJson = requestJson("requests/exam-sprint-outlook-report-request.json");

        mockMvc.perform(post("/api/exam-sprint/outlook-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestJson))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.data.reportId").value("report-001"))
                .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                .andExpect(jsonPath("$.data.generationStatus").value("PENDING"));

        ArgumentCaptor<JsonNode> payloadCaptor = ArgumentCaptor.forClass(JsonNode.class);
        verify(applicationService).createOutlookReport(payloadCaptor.capture());
        JsonNode payload = payloadCaptor.getValue();
        Assertions.assertEquals("20260318测试", payload.path("StudentName").asText());
        Assertions.assertEquals("中考", payload.path("StageExaminName").asText());
    }

    // WHY: Achievement endpoint tests protect the route split using the same curl-ready fixture style as outlook.
    @Test
    void createAchievementReportUsesIndependentEndpoint() throws Exception {
        given(applicationService.createAchievementReport(any())).willReturn(new CreateExamSprintReportResponse(
                "report-002",
                ExamSprintReportType.ACHIEVEMENT,
                ExamSprintReportGenerationStatus.PENDING,
                Instant.parse("2026-01-01T00:00:00Z"),
                Instant.parse("2026-01-02T00:00:00Z")));

        String requestJson = requestJson("requests/exam-sprint-achievement-report-request.json");

        mockMvc.perform(post("/api/exam-sprint/achievement-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestJson))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.data.reportId").value("report-002"))
                .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                .andExpect(jsonPath("$.data.generationStatus").value("PENDING"));

        ArgumentCaptor<JsonNode> payloadCaptor = ArgumentCaptor.forClass(JsonNode.class);
        verify(applicationService).createAchievementReport(payloadCaptor.capture());
        JsonNode payload = payloadCaptor.getValue();
        Assertions.assertEquals("吴泓妤", payload.path("StudentName").asText());
        Assertions.assertEquals("高考", payload.path("StageName").asText());
        Assertions.assertEquals("2024真题", payload.path("TestPaperTitle").asText());
        Assertions.assertEquals(19, payload.path("StudentImproveWordCount").asInt());
        Assertions.assertEquals(4, payload.path("TestPaperImprovedWordCount").asInt());
        Assertions.assertEquals(1.93, payload.path("PaperMasteryHitRate").asDouble());
        Assertions.assertEquals(0.48, payload.path("ImproveStudyEfficiency").asDouble());
        Assertions.assertEquals(" number ", payload.path("TestPaperImprovedWords").get(0).asText());
        Assertions.assertFalse(payload.has("summaryMetrics"));
        Assertions.assertFalse(payload.has("vocabularyComparison"));
        Assertions.assertFalse(payload.has("paperKnownWordsComparison"));
        Assertions.assertFalse(payload.has("examUnknownWordsHitStatus"));
    }

    // WHY: Sync outlook requests should use the same root-level upstream payload as async requests.
    @Test
    void createOutlookReportSyncReturnsDownloadUrl() throws Exception {
        given(applicationService.createOutlookReportSync(any())).willReturn(new CreateExamSprintReportWithUrlResponse(
                "report-sync-001",
                ExamSprintReportType.OUTLOOK,
                ExamSprintReportGenerationStatus.SUCCESS,
                Instant.parse("2026-01-01T00:00:00Z"),
                Instant.parse("2026-01-01T00:01:00Z"),
                Instant.parse("2026-01-02T00:00:00Z"),
                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-outlook-report-report-sync-001.pdf"));

        String requestJson = requestJson("requests/exam-sprint-outlook-report-request.json");

        mockMvc.perform(post("/api/exam-sprint/outlook-reports/sync")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestJson))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-outlook-report-report-sync-001.pdf"));

        verify(applicationService).createOutlookReportSync(any());
    }

    // WHY: Achievement sync routing must stay independent from outlook while returning the same response envelope shape.
    @Test
    void createAchievementReportSyncReturnsDownloadUrl() throws Exception {
        given(applicationService.createAchievementReportSync(any())).willReturn(new CreateExamSprintReportWithUrlResponse(
                "report-sync-002",
                ExamSprintReportType.ACHIEVEMENT,
                ExamSprintReportGenerationStatus.SUCCESS,
                Instant.parse("2026-01-01T00:00:00Z"),
                Instant.parse("2026-01-01T00:01:00Z"),
                Instant.parse("2026-01-02T00:00:00Z"),
                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-achievement-report-report-sync-002.pdf"));

        String requestJson = requestJson("requests/exam-sprint-achievement-report-request.json");

        mockMvc.perform(post("/api/exam-sprint/achievement-reports/sync")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestJson))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-achievement-report-report-sync-002.pdf"));

        verify(applicationService).createAchievementReportSync(any());
    }

    // WHY: Removed generic report endpoints must not reappear while independent report endpoints are being maintained.
    @Test
    void removedReportEndpointsAreNotExposed() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"reportType\":\"OUTLOOK\",\"payload\":{}}"))
                .andExpect(status().isNotFound());

        mockMvc.perform(get("/api/exam-sprint/reports/{reportId}" + "/preview" + "/html", "report-001"))
                .andExpect(status().isNotFound());

        verifyNoInteractions(applicationService);
    }

    // WHY: Malformed JSON should be rejected at the web boundary without invoking application services.
    @Test
    void createOutlookReportReturnsValidationErrorWhenJsonIsMalformed() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/outlook-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"StudentName\":]"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
                .andExpect(jsonPath("$.message").value(containsString("请求体不是合法 JSON")));

        verifyNoInteractions(applicationService);
    }

    // WHY: Achievement malformed JSON is also rejected by Spring before application service validation can add report-specific details.
    @Test
    void createAchievementReportReturnsValidationErrorWhenJsonIsMalformed() throws Exception {
        mockMvc.perform(post("/api/exam-sprint/achievement-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"reportTitle\":]"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
                .andExpect(jsonPath("$.message").value(containsString("请求体不是合法 JSON")));

        verifyNoInteractions(applicationService);
    }

    // WHY: Download routes expose report ids publicly and must not require callers to know storage object keys.
    @Test
    void downloadReportUsesReportIdInsteadOfStorageKey() throws Exception {
        given(applicationService.getReport(eq("report-001"))).willReturn(new ExamSprintReportDetailResponse(
                "report-001",
                ExamSprintReportType.OUTLOOK,
                ExamSprintReportGenerationStatus.SUCCESS,
                Instant.parse("2026-01-01T00:00:00Z"),
                Instant.parse("2026-01-01T00:05:00Z"),
                Instant.parse("2026-01-02T00:00:00Z"),
                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-outlook-report-report-001.pdf",
                null));
        given(applicationService.downloadReport("report-001")).willReturn(new ReportDownloadContent(
                "exam-sprint-outlook-report-report-001.pdf",
                "%PDF".getBytes(StandardCharsets.US_ASCII),
                "application/pdf"));

        mockMvc.perform(get("/api/exam-sprint/reports/{reportId}", "report-001"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.reportId").value("report-001"))
                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/exam-sprint-outlook-report-report-001.pdf"));

        mockMvc.perform(get("/api/exam-sprint/reports/{reportId}/download", "report-001"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PDF));
    }

    // WHY: Kernel business errors must keep stable HTTP status and code mapping at the controller boundary.
    @Test
    void getReportMapsNewKernelBusinessExceptionToBusinessStatusCode() throws Exception {
        given(applicationService.getReport(eq("missing-report")))
                .willThrow(new BusinessException(ErrorCode.REPORT_NOT_FOUND));

        mockMvc.perform(get("/api/exam-sprint/reports/{reportId}", "missing-report"))
                .andExpect(status().isNotFound())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.code").value("REPORT_NOT_FOUND"));
    }

    // WHY: Controller advice must expose a BusinessException custom Chinese message instead of the ErrorCode default English text.
    @Test
    void createOutlookReportUsesBusinessExceptionCustomMessageInResponse() throws Exception {
        given(applicationService.createOutlookReport(any()))
                .willThrow(new BusinessException(
                        ErrorCode.VALIDATION_ERROR,
                        "展望报告参数校验失败：StudentName 必须为非空字符串"));

        mockMvc.perform(post("/api/exam-sprint/outlook-reports")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"StudentName\":123}"))
                .andExpect(status().isBadRequest())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
                .andExpect(jsonPath("$.message").value("展望报告参数校验失败：StudentName 必须为非空字符串"));
    }

    private String requestJson(String resourcePath) throws Exception {
        try (InputStream inputStream = ExamSprintReportControllerWebMvcTest.class.getClassLoader().getResourceAsStream(resourcePath)) {
            if (inputStream == null) {
                throw new IllegalArgumentException("Missing test resource: " + resourcePath);
            }
            return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
        }
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    static class TestApplication {
    }
}
