package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.outlook;

import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

class ClasspathOutlookExamSprintReportRendererTest {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String TEMPLATE_PATH = "templates/outlook-exam-sprint-report-template.html";

    @Test
    void renderBuildsOutlookHtmlAlignedWithDesignDraftDynamicStructure() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        String html = renderer.render(samplePayload(), Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(html)
                .contains("class=\"analysis-table\"")
                .contains("class=\"analysis-row\"")
                .contains("class=\"analysis-cell analysis-cell-left\"")
                .contains("class=\"analysis-cell analysis-cell-right\"")
                .contains("class='card")
                .contains("class='chart-box'")
                .contains("class='data-text'")
                .contains("<table class='frequency-table' role='presentation'>")
                .contains("<tr class='frequency-row'>")
                .contains("<td class='frequency-cell frequency-cell-1'>")
                .contains("<td class='frequency-cell frequency-cell-2'>")
                .contains("<td class='frequency-cell frequency-cell-3'>")
                .contains("<td class='frequency-cell frequency-cell-4'>")
                .contains("class='freq-card card-3 active'")
                .contains("class='freq-header'>3套/周 <span class='badge'>推荐</span>")
                .contains("class='freq-header'>5套/周 <span class='crown'>★</span>")
                .contains("class='chart-track' cx='110' cy='110' r='76' fill='none' stroke='#e8eef7' stroke-width='18'")
                .doesNotContain("class='freq-title'>")
                .doesNotContain("1套/周 <span class='badge'>")
                .doesNotContain("2套/周 <span class='badge'>")
                .doesNotContain("5套/周 <span class='badge'>")
                .doesNotContain("④")
                .contains("胜率 <strong>72%</strong>")
                .contains("class='suggest-box'")
                .contains("考前半个月·核心突击期")
                .contains("考前半小时·临阵巩固期")
                .contains("class='student-case'")
                .contains("<table class='student-case-table' role='presentation'>")
                .contains("<tr class='student-case-row'>")
                .contains("<td class='case-chart-cell'>")
                .contains("<td class='case-info-cell'>")
                .contains("class='case-chart'")
                .contains("class='case-info'")
                .contains("王雷宇")
                .contains("705词 | 高考命中：237词 | 命中率：33.8%")
                .contains("+19分")
                .contains("考纲总量：<span class='highlight'>4200词</span>")
                .contains("已掌握：<span class='highlight'>2701词（64.31%）</span>")
                .contains("未掌握：<span class='highlight'>1499词（35.69%）</span>")
                .contains("真题总词：961词 | 生词量：847词（88.14%）")
                .contains("生词占比降至74.51%")
                .contains("基础必会词：掌握率62%")
                .contains("高分拉分词：掌握率41%")
                .contains("高频词：188.6（优先学习）")
                .contains("中频词：154.5（重点突破）")
                .contains("低频词：70.4（酌情学习）");
    }

    @Test
    void supportsOnlyOutlookReportType() {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        assertThat(renderer.supports(ExamSprintReportType.OUTLOOK)).isTrue();
        assertThat(renderer.supports(ExamSprintReportType.ACHIEVEMENT)).isFalse();
    }

    @Test
    void renderUsesInjectedObjectMapperForPayloadDeserialization() throws Exception {
        TrackingObjectMapper objectMapper = new TrackingObjectMapper();
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(objectMapper);

        renderer.render(samplePayload(), Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(objectMapper.treeToValueCalled).isTrue();
    }

    @Test
    void renderClosesTemplateInputStreamAfterLoadingClasspathTemplate() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
        TrackingTemplateClassLoader trackingClassLoader = new TrackingTemplateClassLoader(originalClassLoader);

        try {
            Thread.currentThread().setContextClassLoader(trackingClassLoader);

            renderer.render(samplePayload(), Instant.parse("2026-01-03T08:00:00Z"));
        } finally {
            Thread.currentThread().setContextClassLoader(originalClassLoader);
        }

        assertThat(trackingClassLoader.inputStream.closed).isTrue();
    }

    @Test
    void renderEscapesHtmlSensitiveCharactersAndScriptPayloads() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        JsonNode payload = payloadWithEscapingSamples();
        String html = renderer.render(payload, Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(html)
                .contains("李&lt;同学&gt;")
                .contains("春季&amp;高考英语")
                .contains("Ability &quot;Bot&quot;")
                .contains("O&#39;Brien")
                .contains("&lt;script&gt;alert(1)&lt;/script&gt;")
                .doesNotContain("<script>alert(1)</script>")
                .doesNotContain("<script>");
    }

    @Test
    void renderFallsBackToDefaultThemeColorWhenPayloadColorIsInvalid() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        JsonNode payload = payloadWithInvalidThemeColors();
        String html = renderer.render(payload, Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(html)
                .containsPattern("class='chart-column high-band-column'[^>]*fill='#448aff'")
                .containsPattern("class='chart-column mid-band-column'[^>]*fill='#448aff'")
                .containsPattern("class='chart-column low-band-column'[^>]*fill='#448aff'")
                .doesNotContain("onload=")
                .doesNotContain("javascript:alert(1)");
    }

    @Test
    void renderUsesCircleForSyllabusAndCaseProgressWhenPercentReachesHundred() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        JsonNode payload = payloadWithProgressPercent(100, 100.0);
        String html = renderer.render(payload, Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(html)
                .contains("class='donut-mastered-full-circle'")
                .contains("class='case-progress-full-circle'")
                .doesNotContain("class='donut-mastered-arc'")
                .doesNotContain("class='case-progress-arc'");
    }

    @Test
    void renderSkipsProgressArcWhenSyllabusAndCasePercentAreZeroOrBelow() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

        JsonNode payload = payloadWithProgressPercent(0, 0.0);
        String html = renderer.render(payload, Instant.parse("2026-01-03T08:00:00Z"));

        assertThat(html)
                .doesNotContain("class='donut-mastered-arc'")
                .doesNotContain("class='case-progress-arc'")
                .contains("class='donut-unmastered-arc'");
    }

    private static final class TrackingObjectMapper extends ObjectMapper {
        private boolean treeToValueCalled;

        @Override
        public <T> T treeToValue(com.fasterxml.jackson.core.TreeNode node, Class<T> valueType)
                throws com.fasterxml.jackson.core.JsonProcessingException {
            treeToValueCalled = true;
            return super.treeToValue(node, valueType);
        }
    }

    private static final class TrackingTemplateClassLoader extends ClassLoader {
        private final TrackingInputStream inputStream = new TrackingInputStream("template".getBytes(StandardCharsets.UTF_8));

        private TrackingTemplateClassLoader(ClassLoader parent) {
            super(parent);
        }

        @Override
        public java.io.InputStream getResourceAsStream(String name) {
            if (TEMPLATE_PATH.equals(name)) {
                return inputStream;
            }
            return super.getResourceAsStream(name);
        }
    }

    private static final class TrackingInputStream extends ByteArrayInputStream {
        private boolean closed;

        private TrackingInputStream(byte[] buf) {
            super(buf);
        }

        @Override
        public void close() throws IOException {
            closed = true;
            super.close();
        }
    }

    private JsonNode samplePayload() throws Exception {
        return OBJECT_MAPPER.readTree("""
                {
                  "reportMetadata": {
                    "reportVersionLabel": "2026 词汇展望报告",
                    "learnerName": "李同学",
                    "targetExamName": "春季高考英语",
                    "sprintPeriodLabel": "30 天考前冲刺",
                    "authorName": "Ability Bot"
                  },
                  "readinessOverview": {
                    "summary": "基础较稳，具备短期冲刺提分空间。",
                    "currentStage": "冲刺提升期",
                    "keyInsight": "高频与常考词群是提分关键。",
                    "readinessScore": 72
                  },
                  "syllabusMasteryChart": {
                    "totalWordCount": 4200,
                    "masteredWordCount": 2701,
                    "unmasteredWordCount": 1499,
                    "masteryPercent": 64,
                    "summaryLabel": "考纲词汇掌握概览",
                    "recommendation": "优先补齐高考核心场景词。"
                  },
                  "pastPaperVocabularyChart": {
                    "totalWordCount": 961,
                    "unknownWordCountBeforeSprint": 847,
                    "unknownWordCountAfterSprint": 716,
                    "projectedScoreGainLabel": "预计提分5-15分",
                    "recommendation": "先压降真题生词占比。"
                  },
                  "highFrequencyVocabularyChart": {
                    "basicCorePercent": 62,
                    "highScorePercent": 41,
                    "highlightLabel": "拉分词是提分核心突破项"
                  },
                  "vocabularyFrequencyBandChart": {
                    "bars": [
                      {"bandLabel": "高频词", "currentValue": 188.6, "priorityLabel": "优先学习", "themeColor": "#448aff"},
                      {"bandLabel": "中频词", "currentValue": 154.5, "priorityLabel": "重点突破", "themeColor": "#4caf50"},
                      {"bandLabel": "低频词", "currentValue": 70.4, "priorityLabel": "酌情学习", "themeColor": "#ff9800"}
                    ]
                  },
                  "frequencyPlan": {
                    "cards": [
                      {
                        "cadencePerWeek": 1,
                        "scoreGainLabel": "+5分",
                        "winRatePercent": 38,
                        "recommended": false,
                        "badgeLabel": "稳健",
                        "emphasisIcon": "①"
                      },
                      {
                        "cadencePerWeek": 2,
                        "scoreGainLabel": "+10分",
                        "winRatePercent": 55,
                        "recommended": false,
                        "badgeLabel": "均衡",
                        "emphasisIcon": "②"
                      },
                      {
                        "cadencePerWeek": 3,
                        "scoreGainLabel": "+15分",
                        "winRatePercent": 72,
                        "recommended": true,
                        "badgeLabel": "推荐",
                        "emphasisIcon": "③"
                      },
                      {
                        "cadencePerWeek": 5,
                        "scoreGainLabel": "+20分",
                        "winRatePercent": 88,
                        "recommended": false,
                        "badgeLabel": "冲刺",
                        "emphasisIcon": "④"
                      }
                    ],
                    "recommendationTitle": "💡建议策略",
                    "recommendationSummary": "7 天提分冲刺是首选节奏，按词频优先级记忆，不浪费时间；只攻克高频/中频核心词，2周15小时速记500-800必考词，快速缩小生词缺口。",
                    "phaseSuggestions": [
                      {"title": "考前半个月·核心突击期", "description": "围绕高频词建立记忆闭环。"},
                      {"title": "考前半小时·临阵巩固期", "description": "结合真题词做循环巩固。"}
                    ]
                  },
                  "scoreImprovementCaseStudy": {
                    "headline": "真实提分 · 效果可复制",
                    "learnerName": "王雷宇",
                    "studyPeriodLabel": "考前半个月·核心突击期",
                    "memorizedWordCount": 705,
                    "examHitWordCount": 237,
                    "hitRatePercent": 33.8,
                    "baselineScoreLabel": "70分以下",
                    "finalScore": 89,
                    "scoreGain": 19
                  }
                }
                """);
    }

    private JsonNode payloadWithEscapingSamples() throws Exception {
        ObjectNode payload = samplePayload().deepCopy();

        ObjectNode frequencyPlan = (ObjectNode) payload.get("frequencyPlan");
        ObjectNode firstPhaseSuggestion = (ObjectNode) frequencyPlan.withArray("phaseSuggestions").get(0);
        firstPhaseSuggestion.put("title", "春季&高考英语");
        frequencyPlan.put("recommendationSummary", "<script>alert(1)</script>");

        ObjectNode scoreImprovementCaseStudy = (ObjectNode) payload.get("scoreImprovementCaseStudy");
        scoreImprovementCaseStudy.put("learnerName", "李<同学>");
        scoreImprovementCaseStudy.put("headline", "Ability \"Bot\"");
        scoreImprovementCaseStudy.put("baselineScoreLabel", "O'Brien");

        return payload;
    }

    private JsonNode payloadWithInvalidThemeColors() throws Exception {
        ObjectNode payload = samplePayload().deepCopy();
        payload.with("vocabularyFrequencyBandChart").withArray("bars").forEach(node -> {
            ObjectNode bar = (ObjectNode) node;
            if ("高频词".equals(bar.path("bandLabel").asText())) {
                bar.put("themeColor", "\" onload=\"alert(1)");
                return;
            }
            if ("中频词".equals(bar.path("bandLabel").asText())) {
                bar.put("themeColor", "url(javascript:alert(1))");
                return;
            }
            bar.put("themeColor", "#12");
        });
        return payload;
    }

    private JsonNode payloadWithProgressPercent(int masteryPercent, double hitRatePercent) throws Exception {
        ObjectNode payload = samplePayload().deepCopy();
        ObjectNode syllabusMasteryChart = (ObjectNode) payload.get("syllabusMasteryChart");
        syllabusMasteryChart.put("totalWordCount", 100);
        syllabusMasteryChart.put("masteredWordCount", Math.max(0, Math.min(100, masteryPercent)));
        syllabusMasteryChart.put("unmasteredWordCount", 100 - Math.max(0, Math.min(100, masteryPercent)));
        syllabusMasteryChart.put("masteryPercent", masteryPercent);
        ((ObjectNode) payload.get("scoreImprovementCaseStudy")).put("hitRatePercent", hitRatePercent);
        return payload;
    }
}
