package cn.yunzhixue.ability.center.examsprint.infrastructure.report.pdf;

import cn.yunzhixue.ability.center.examsprint.domain.report.AchievementReportContent;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
import cn.yunzhixue.ability.center.examsprint.domain.report.UnmodeledReportContent;
import cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.achievement.ClasspathAchievementExamSprintReportRenderer;
import cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.outlook.ClasspathOutlookExamSprintReportRenderer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.Test;

import java.awt.Font;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
import java.time.Instant;
import java.util.List;

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

class OpenHtmlToPdfExamSprintReportPdfGeneratorTest {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    /**
     * 覆盖官方上游词汇 payload 渲染后的 Outlook HTML 生成 PDF 时，应产出可解析 PDF 并包含关键静态与计算文本。
     */
    @Test
    void generateCreatesPdfSmokeWithExtractableOutlookKeyText() throws Exception {
        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator();

        String html = renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
        byte[] pdfBytes = pdfGenerator.generate(html);

        assertThat(pdfBytes).isNotEmpty();
        assertThat(new String(pdfBytes, 0, 4, StandardCharsets.ISO_8859_1)).isEqualTo("%PDF");

        Path previewPdfPath = Path.of(System.getProperty("user.dir"), "target", "outlook-report-demo.pdf");
        Files.createDirectories(previewPdfPath.getParent());
        Files.write(previewPdfPath, pdfBytes);
        assertThat(previewPdfPath).exists().isRegularFile();

        try (PDDocument document = PDDocument.load(pdfBytes)) {
            assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(1);
            String normalizedText = new PDFTextStripper().getText(document).replaceAll("\\s+", "");
            assertThat(normalizedText)
                    .contains("高考英语临考词汇突击潜力展望报告")
                    .contains("模块一：个人学情分析")
                    .contains("模块二：科学备考建议")
                    .contains("模块三：上届学员提分案例")
                    .contains("词频区间掌握度")
                    .containsAnyOf("练习学案频率与提分规划", "建议策略")
                    .contains("王雷宇")
                    .contains("705词")
                    .contains("237词")
                    .contains("+19分")
                    .containsAnyOf("高频词：56.7%（优先学习）", "高频词56.7%优先学习")
                    .containsAnyOf("中频词：47.5%（重点突破）", "中频词47.5%重点突破")
                    .containsAnyOf("低频词：30.0%（酌情学习）", "低频词30.0%酌情学习");
        }
    }

    /**
     * 覆盖学习成果报告 HTML 生成 PDF 时，应产出可解析 PDF 并包含成果报告关键文本。
     */
    @Test
    void generateCreatesPdfSmokeWithExtractableAchievementKeyText() throws Exception {
        ClasspathAchievementExamSprintReportRenderer renderer = new ClasspathAchievementExamSprintReportRenderer();
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator();

        String html = renderer.render(sampleAchievementContent(), Instant.parse("2026-04-25T08:00:00Z"));
        byte[] pdfBytes = pdfGenerator.generate(html);

        assertThat(pdfBytes).isNotEmpty();
        assertThat(new String(pdfBytes, 0, 4, StandardCharsets.ISO_8859_1)).isEqualTo("%PDF");

        Path previewPdfPath = Path.of(System.getProperty("user.dir"), "target", "achievement-report-demo.pdf");
        Files.createDirectories(previewPdfPath.getParent());
        Files.write(previewPdfPath, pdfBytes);
        assertThat(previewPdfPath).exists().isRegularFile();

        try (PDDocument document = PDDocument.load(pdfBytes)) {
            assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(1);
            String normalizedText = normalizePdfText(new PDFTextStripper().getText(document));
            assertThat(normalizedText)
                    .contains("高考英语临考突击学习成果报告")
                    .contains("模块一:词汇量对比")
                    .contains("模块二:试卷熟词量对比")
                    .contains("模块三:实考生词命中状况")
                    .contains("词汇量提升(个)")
                    .contains("真题生词命中率")
                    .contains("1.93%")
                    .contains("0.48倍")
                    .contains("高考词汇量")
                    .contains("3500词")
                    .contains("掌握率")
                    .contains("66.51%")
                    .contains("67.06%")
                    .contains("试卷标题")
                    .contains("2024真题")
                    .contains("861词")
                    .contains("75.49%")
                    .contains("75.96%")
                    .contains("207个")
                    .contains("203个")
                    .contains("4个")
                    .containsAnyOf("number", "bear", "popular", "importance");
        }
    }

    /**
     * 覆盖系统字体候选为空时，PDF 生成器应使用随包 MiSans 字体输出可抽取中文文本。
     */
    @Test
    void generateUsesBundledMiSansWhenSystemFontCandidatesAreEmpty() throws Exception {
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator(
                BundledOutlookReportFonts::load,
                List.of());

        byte[] pdfBytes = pdfGenerator.generate("""
                <html>
                <head>
                    <meta charset=\"UTF-8\"/>
                    <style>
                        body { font-family: MiSans; }
                    </style>
                </head>
                <body>
                    <p>MiSans 测试：临考冲刺 +19分</p>
                </body>
                </html>
                """);

        assertThat(pdfBytes).isNotEmpty();
        assertThat(new String(pdfBytes, 0, 4, StandardCharsets.ISO_8859_1)).isEqualTo("%PDF");

        try (PDDocument document = PDDocument.load(pdfBytes)) {
            assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(1);
            assertThat(normalizePdfText(new PDFTextStripper().getText(document)))
                    .contains("MiSans测试:临考冲刺+19分");
        }
    }

    /**
     * 覆盖系统字体候选为空且 HTML 包含内联 SVG 中文时，AWT 注册的随包 MiSans 应支持 SVG 文本渲染。
     */
    @Test
    void generateRendersInlineSvgChineseTextWithAwtRegisteredMiSansWhenSystemFontCandidatesAreEmpty() throws Exception {
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator(
                BundledOutlookReportFonts::load,
                List.of());

        BundledOutlookReportFonts.load();
        assertThat(new Font("MiSans VF", Font.PLAIN, 24).canDisplayUpTo("掌握率")).isEqualTo(-1);

        byte[] pdfBytes = pdfGenerator.generate("""
                <html>
                <head>
                    <meta charset=\"UTF-8\"/>
                    <style>
                        body { font-family: sans-serif; }
                    </style>
                </head>
                <body>
                    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 80' width='220' height='80'
                         font-family=\"'MiSans VF', MiSans, ReportFont, sans-serif\">
                        <text x='20' y='48' font-size='24'>掌握率</text>
                    </svg>
                </body>
                </html>
                """);

        assertThat(pdfBytes).isNotEmpty();
        try (PDDocument document = PDDocument.load(pdfBytes)) {
            assertThat(document.getNumberOfPages()).isEqualTo(1);
        }
    }

    /**
     * 覆盖随包字体 supplier 抛出非预期异常时，PDF 生成器应保留根因并向调用方报告生成失败。
     */
    @Test
    void generateDoesNotHideUnexpectedBundledFontSupplierFailures() {
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator(
                () -> { throw new IllegalArgumentException("broken supplier contract"); },
                List.of("/System/Library/Fonts/Supplemental/Arial Unicode.ttf"));

        assertThatThrownBy(() -> pdfGenerator.generate("<html><body>测试</body></html>"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("Failed to generate PDF")
                .hasRootCauseInstanceOf(IllegalArgumentException.class)
                .hasRootCauseMessage("broken supplier contract");
    }

    /**
     * 覆盖随包字体资源缺失时，PDF 生成器应回退系统字体并仍能生成 ASCII PDF。
     */
    @Test
    void generateFallsBackToSystemFontsWhenBundledFontResourceIsMissing() {
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator(
                () -> { throw new BundledOutlookReportFonts.BundledFontMissingException("missing bundled font"); },
                List.of());

        byte[] pdfBytes = pdfGenerator.generate("<html><body>ASCII fallback smoke</body></html>");

        assertThat(pdfBytes).isNotEmpty();
        assertThat(new String(pdfBytes, 0, 4, StandardCharsets.ISO_8859_1)).isEqualTo("%PDF");
    }

    /**
     * 覆盖随包字体注册失败时，PDF 生成器不应静默回退而应报告生成失败。
     */
    @Test
    void generateDoesNotFallbackWhenBundledFontRegistrationFails() {
        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator(
                () -> { throw new BundledOutlookReportFonts.BundledFontUnavailableException("invalid bundled font"); },
                List.of());

        assertThatThrownBy(() -> pdfGenerator.generate("<html><body>测试</body></html>"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("Failed to generate PDF")
                .hasRootCauseInstanceOf(BundledOutlookReportFonts.BundledFontUnavailableException.class)
                .hasRootCauseMessage("invalid bundled font");
    }

    private String normalizePdfText(String text) {
        return Normalizer.normalize(text, Normalizer.Form.NFKC).replaceAll("\\s+", "");
    }

    private UnmodeledReportContent unmodeledOutlookContent(JsonNode payload) {
        return new UnmodeledReportContent(ReportType.OUTLOOK, payload);
    }

    private AchievementReportContent sampleAchievementContent() {
        return new AchievementReportContent(
                "吴泓妤",
                "高考英语临考突击学习成果报告",
                "2024真题 · 两周专项训练 · 真实提分效果",
                "恭喜完成两周考前突击专项训练",
                "基于2024英语真题试卷 · 真实学习效果分析",
                new AchievementReportContent.SummaryMetrics(
                        "+19",
                        "+4",
                        "0.0193",
                        "0.48"),
                new AchievementReportContent.Comparison(2328.0, 2347.0, "2328", "2347", "+19"),
                new AchievementReportContent.Comparison(650.0, 654.0, "650", "654", "+4"),
                new AchievementReportContent.StageVocabularySummary("高考", "3500", "66.51", "67.06", "+0.55"),
                new AchievementReportContent.TestPaperVocabularySummary("2024真题", "861", "207", "203", "75.49", "75.96", "+0.62"),
                new AchievementReportContent.ExamUnknownWordsHitStatus(
                        "0.0193",
                        "0.48",
                        "207",
                        "203",
                        "4",
                        List.of("number", "bear", "popular", "importance")));
    }

    private JsonNode samplePayload() throws Exception {
        return OBJECT_MAPPER.readTree("""
                {
                  "StudentName": "20260318测试",
                  "StudentStage": 2,
                  "StageName": "初中",
                  "StageVocabulary": 10,
                  "StageExaminName": "中考",
                  "StageImportant": 3,
                  "StudentWordsLatest": [
                    {"WordId": 1, "MeanId": 1, "WordSpell": "w1", "WordFrequency": 1, "Mastery": 1.0, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 2, "MeanId": 1, "WordSpell": "w2", "WordFrequency": 2, "Mastery": 0.5, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 3, "MeanId": 1, "WordSpell": "w3", "WordFrequency": 3, "Mastery": 0.2, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 4, "MeanId": 1, "WordSpell": "w4", "WordFrequency": 4, "Mastery": 0.4, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 5, "MeanId": 1, "WordSpell": "w5", "WordFrequency": 5, "Mastery": 0.6, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 6, "MeanId": 1, "WordSpell": "w6", "WordFrequency": 6, "Mastery": 0.8, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 7, "MeanId": 1, "WordSpell": "w7", "WordFrequency": 7, "Mastery": 0.1, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 8, "MeanId": 1, "WordSpell": "w8", "WordFrequency": 8, "Mastery": 0.2, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 9, "MeanId": 1, "WordSpell": "w9", "WordFrequency": 9, "Mastery": 0.3, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
                    {"WordId": 10, "MeanId": 1, "WordSpell": "w10", "WordFrequency": 10, "Mastery": 0.4, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"}
                  ],
                  "MastedWordCount": 4,
                  "UnMastedWordCount": 6,
                  "ExamineStrangeWordCount": 3,
                  "TestPaperWordIdArray": [1, 2, 3, 4, 5],
                  "TestPaperTitle": "文章2.jpg",
                  "TestPaperUnMasterWords": ["lot", "father", "catch"],
                  "TestPaperMastedWords": ["a", "the"],
                  "TestPaperMastedWordCount": 2,
                  "TestPaperWordCount": 5,
                  "Complex": false
                }
                """);
    }

}
