package cn.yunzhixue.ability.center.examsprint.application.report;

import cn.yunzhixue.ability.center.examsprint.domain.report.AchievementReportContent;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportPdfGenerator;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportContent;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
import cn.yunzhixue.ability.center.examsprint.domain.report.UnmodeledReportContent;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

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

@ExtendWith(OutputCaptureExtension.class)
class ExamSprintReportWarmupRunnerTest {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-07T08:00:00Z"), ZoneOffset.UTC);

    /** 覆盖启动事件异步提交场景，当应用 ready 后，应只提交后台任务而不在线程内同步执行预热。 */
    @Test
    void onApplicationReadySubmitsWarmupJobWithoutRunningInline() {
        List<String> events = new ArrayList<>();
        RecordingExecutor executor = new RecordingExecutor();
        RecordingRenderer outlookRenderer = new RecordingRenderer(ReportType.OUTLOOK, "outlook-html", events);
        RecordingRenderer achievementRenderer = new RecordingRenderer(ReportType.ACHIEVEMENT, "achievement-html", events);
        RecordingPdfGenerator pdfGenerator = new RecordingPdfGenerator(events);
        ExamSprintReportWarmupRunner runner = runner(
                List.of(outlookRenderer, achievementRenderer),
                pdfGenerator,
                executor);

        runner.onApplicationReady();

        assertThat(executor.commands).hasSize(1);
        assertThat(events).isEmpty();

        executor.runOnlyCommand();

        assertThat(events).containsExactly(
                "render:OUTLOOK",
                "pdf:outlook-html",
                "render:ACHIEVEMENT",
                "pdf:achievement-html");
    }

    /** 覆盖 warmup 异步提交失败场景，当 executor 拒绝任务时，应记录安全日志且不向 ApplicationReadyEvent 抛出异常。 */
    @Test
    void onApplicationReadyLogsSafeFailureWhenExecutorRejectsTask(CapturedOutput output) {
        List<String> events = new ArrayList<>();
        Executor rejectingExecutor = command -> {
            throw new java.util.concurrent.RejectedExecutionException("SENSITIVE_EXECUTOR_REJECTION_DO_NOT_LOG");
        };
        ExamSprintReportWarmupRunner runner = runner(
                List.of(
                        new RecordingRenderer(ReportType.OUTLOOK, "outlook-html", events),
                        new RecordingRenderer(ReportType.ACHIEVEMENT, "achievement-html", events)),
                new RecordingPdfGenerator(events),
                rejectingExecutor);

        runner.onApplicationReady();

        assertThat(events).isEmpty();
        assertThat(output.getAll())
                .contains("exam_sprint_report_warmup_failed")
                .contains("reportType=ALL")
                .contains("stage=async_submit")
                .contains("exceptionType=RejectedExecutionException")
                .contains("failureReason=RejectedExecutionException")
                .doesNotContain("SENSITIVE_EXECUTOR_REJECTION_DO_NOT_LOG")
                .doesNotContain("StudentWordsLatest")
                .doesNotContain("TestPaperImprovedWords");
    }

    /** 覆盖串行预热主流程，当两个报告资源有效时，应按 OUTLOOK 到 ACHIEVEMENT 顺序渲染并生成 PDF。 */
    @Test
    void warmUpReportsRendersOutlookBeforeAchievementAndGeneratesPdf(CapturedOutput output) {
        List<String> events = new ArrayList<>();
        RecordingRenderer outlookRenderer = new RecordingRenderer(ReportType.OUTLOOK, "outlook-html", events);
        RecordingRenderer achievementRenderer = new RecordingRenderer(ReportType.ACHIEVEMENT, "achievement-html", events);
        RecordingPdfGenerator pdfGenerator = new RecordingPdfGenerator(events);
        ExamSprintReportWarmupRunner runner = runner(
                List.of(outlookRenderer, achievementRenderer),
                pdfGenerator,
                Runnable::run);

        runner.warmUpReports();

        assertThat(events).containsExactly(
                "render:OUTLOOK",
                "pdf:outlook-html",
                "render:ACHIEVEMENT",
                "pdf:achievement-html");
        assertThat(outlookRenderer.generatedAt).containsExactly(FIXED_CLOCK.instant());
        assertThat(achievementRenderer.generatedAt).containsExactly(FIXED_CLOCK.instant());

        assertThat(outlookRenderer.renderedContents).singleElement().isInstanceOf(UnmodeledReportContent.class);
        UnmodeledReportContent outlookContent = (UnmodeledReportContent) outlookRenderer.renderedContents.get(0);
        assertThat(outlookContent.reportType()).isEqualTo(ReportType.OUTLOOK);
        assertThat(((JsonNode) outlookContent.source()).path("StudentName").asText()).isEqualTo("20260318测试");

        assertThat(achievementRenderer.renderedContents).singleElement().isInstanceOf(AchievementReportContent.class);
        AchievementReportContent achievementContent = (AchievementReportContent) achievementRenderer.renderedContents.get(0);
        assertThat(achievementContent.studentName()).isEqualTo("20260318测试");
        assertThat(achievementContent.reportTitle()).isEqualTo("初中英语临考突击学习成果报告");

        assertThat(pdfGenerator.htmlInputs).containsExactly("outlook-html", "achievement-html");
        assertThat(output.getAll())
                .contains("exam_sprint_report_warmup_started")
                .contains("exam_sprint_report_warmup_succeeded")
                .contains("reportType=OUTLOOK")
                .contains("reportType=ACHIEVEMENT")
                .contains("durationMs=")
                .contains("renderDurationMs=")
                .contains("pdfDurationMs=")
                .contains("htmlLength=")
                .contains("pdfByteLength=");
    }

    /** 覆盖单报告失败隔离场景，当 OUTLOOK 渲染失败时，应记录安全失败日志且继续预热 ACHIEVEMENT。 */
    @Test
    void warmUpReportsLogsFailureWithoutPayloadOrSensitiveMessageAndContinues(CapturedOutput output) {
        List<String> events = new ArrayList<>();
        FailingRenderer failingOutlookRenderer = new FailingRenderer(ReportType.OUTLOOK, events);
        RecordingRenderer achievementRenderer = new RecordingRenderer(ReportType.ACHIEVEMENT, "achievement-html", events);
        RecordingPdfGenerator pdfGenerator = new RecordingPdfGenerator(events);
        ExamSprintReportWarmupRunner runner = runner(
                List.of(failingOutlookRenderer, achievementRenderer),
                pdfGenerator,
                Runnable::run);

        runner.warmUpReports();

        assertThat(events).containsExactly(
                "render:OUTLOOK",
                "render:ACHIEVEMENT",
                "pdf:achievement-html");
        assertThat(achievementRenderer.renderedContents).singleElement().isInstanceOf(AchievementReportContent.class);
        assertThat(pdfGenerator.htmlInputs).containsExactly("achievement-html");
        assertThat(output.getAll())
                .contains("exam_sprint_report_warmup_failed")
                .contains("reportType=OUTLOOK")
                .contains("stage=render_html")
                .contains("exceptionType=IllegalStateException")
                .contains("failureReason=IllegalStateException")
                .contains("exam_sprint_report_warmup_succeeded")
                .contains("reportType=ACHIEVEMENT")
                .doesNotContain("SENSITIVE_WARMUP_FAILURE_DO_NOT_LOG")
                .doesNotContain("StudentWordsLatest")
                .doesNotContain("TestPaperImprovedWords");
    }

    private ExamSprintReportWarmupRunner runner(
            List<ExamSprintReportRenderer> renderers,
            ExamSprintReportPdfGenerator pdfGenerator,
            Executor executor) {
        return new ExamSprintReportWarmupRunner(renderers, pdfGenerator, OBJECT_MAPPER, FIXED_CLOCK, executor);
    }

    private static final class RecordingExecutor implements Executor {
        private final List<Runnable> commands = new ArrayList<>();

        @Override
        public void execute(Runnable command) {
            commands.add(command);
        }

        private void runOnlyCommand() {
            assertThat(commands).hasSize(1);
            commands.get(0).run();
        }
    }

    private static final class RecordingRenderer implements ExamSprintReportRenderer {
        private final ReportType reportType;
        private final String html;
        private final List<String> events;
        private final List<ReportContent> renderedContents = new ArrayList<>();
        private final List<Instant> generatedAt = new ArrayList<>();

        private RecordingRenderer(ReportType reportType, String html, List<String> events) {
            this.reportType = reportType;
            this.html = html;
            this.events = events;
        }

        @Override
        public boolean supports(ReportType reportType) {
            return this.reportType == reportType;
        }

        @Override
        public String render(ReportContent content, Instant generatedAt) {
            events.add("render:" + reportType);
            renderedContents.add(content);
            this.generatedAt.add(generatedAt);
            return html;
        }
    }

    private static final class FailingRenderer implements ExamSprintReportRenderer {
        private final ReportType reportType;
        private final List<String> events;

        private FailingRenderer(ReportType reportType, List<String> events) {
            this.reportType = reportType;
            this.events = events;
        }

        @Override
        public boolean supports(ReportType reportType) {
            return this.reportType == reportType;
        }

        @Override
        public String render(ReportContent content, Instant generatedAt) {
            events.add("render:" + reportType);
            throw new IllegalStateException("SENSITIVE_WARMUP_FAILURE_DO_NOT_LOG");
        }
    }

    private static final class RecordingPdfGenerator implements ExamSprintReportPdfGenerator {
        private final List<String> events;
        private final List<String> htmlInputs = new ArrayList<>();

        private RecordingPdfGenerator(List<String> events) {
            this.events = events;
        }

        @Override
        public byte[] generate(String htmlContent) {
            events.add("pdf:" + htmlContent);
            htmlInputs.add(htmlContent);
            return ("pdf:" + htmlContent).getBytes(StandardCharsets.UTF_8);
        }
    }
}
