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

import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

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

class ExamSprintReportGenerationWorkerTest {

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

    @Test
    void processMarksReportSuccessAfterUpload() {
        TestRepository repository = new TestRepository();
        repository.save(ExamSprintReport.pending(
                "report-success",
                ExamSprintReportType.OUTLOOK,
                OBJECT_MAPPER.createObjectNode(),
                FIXED_CLOCK.instant(),
                FIXED_CLOCK.instant().plusSeconds(86400)));
        TestStorage storage = new TestStorage();
        ExamSprintReportGenerationWorker worker = new ExamSprintReportGenerationWorker(
                repository,
                List.of(new TestRenderer()),
                html -> html.getBytes(StandardCharsets.UTF_8),
                storage,
                FIXED_CLOCK);

        worker.process("report-success");

        ExamSprintReport report = repository.findById("report-success").orElseThrow();
        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
        assertThat(report.storageObjectKey()).isEqualTo("exam-sprint-reports/outlook/report-success/exam-sprint-outlook-report-report-success.pdf");
        assertThat(report.fileName()).isEqualTo("exam-sprint-outlook-report-report-success.pdf");
    }

    @Test
    void processCreatesAchievementFileNameAndStorageKeyAfterUpload() {
        TestRepository repository = new TestRepository();
        repository.save(ExamSprintReport.pending(
                "report-achievement",
                ExamSprintReportType.ACHIEVEMENT,
                OBJECT_MAPPER.createObjectNode(),
                FIXED_CLOCK.instant(),
                FIXED_CLOCK.instant().plusSeconds(86400)));
        TestStorage storage = new TestStorage();
        ExamSprintReportGenerationWorker worker = new ExamSprintReportGenerationWorker(
                repository,
                List.of(new TestRenderer(ExamSprintReportType.ACHIEVEMENT)),
                html -> html.getBytes(StandardCharsets.UTF_8),
                storage,
                FIXED_CLOCK);

        worker.process("report-achievement");

        ExamSprintReport report = repository.findById("report-achievement").orElseThrow();
        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
        assertThat(report.fileName()).isEqualTo("exam-sprint-achievement-report-report-achievement.pdf");
        assertThat(report.storageObjectKey()).isEqualTo(
                "exam-sprint-reports/achievement/report-achievement/exam-sprint-achievement-report-report-achievement.pdf");
    }

    @Test
    void processMarksReportFailedWhenGenerationPipelineThrows() {
        TestRepository repository = new TestRepository();
        repository.save(ExamSprintReport.pending(
                "report-failed",
                ExamSprintReportType.OUTLOOK,
                OBJECT_MAPPER.createObjectNode(),
                FIXED_CLOCK.instant(),
                FIXED_CLOCK.instant().plusSeconds(86400)));

        ExamSprintReportGenerationWorker worker = new ExamSprintReportGenerationWorker(
                repository,
                List.of(new FailingRenderer()),
                html -> html.getBytes(StandardCharsets.UTF_8),
                new TestStorage(),
                FIXED_CLOCK);

        worker.process("report-failed");

        ExamSprintReport report = repository.findById("report-failed").orElseThrow();
        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.FAILED);
        assertThat(report.failureReason()).isEqualTo("renderer exploded");
    }

    private static class TestRenderer implements ExamSprintReportRenderer {
        private final ExamSprintReportType supportedReportType;

        TestRenderer() {
            this(ExamSprintReportType.OUTLOOK);
        }

        TestRenderer(ExamSprintReportType supportedReportType) {
            this.supportedReportType = supportedReportType;
        }

        @Override
        public boolean supports(ExamSprintReportType reportType) {
            return reportType == supportedReportType;
        }

        @Override
        public String render(com.fasterxml.jackson.databind.JsonNode payload, Instant generatedAt) {
            return "<html><body>ok</body></html>";
        }
    }

    private static class FailingRenderer implements ExamSprintReportRenderer {
        @Override
        public boolean supports(ExamSprintReportType reportType) {
            return reportType == ExamSprintReportType.OUTLOOK;
        }

        @Override
        public String render(com.fasterxml.jackson.databind.JsonNode payload, Instant generatedAt) {
            throw new IllegalStateException("renderer exploded");
        }
    }

    private static class TestRepository implements ExamSprintReportRepository {
        private final ConcurrentMap<String, ExamSprintReport> storage = new ConcurrentHashMap<>();

        @Override
        public ExamSprintReport save(ExamSprintReport report) {
            storage.put(report.reportId(), report);
            return report;
        }

        @Override
        public Optional<ExamSprintReport> findById(String reportId) {
            return Optional.ofNullable(storage.get(reportId));
        }

        @Override
        public List<ExamSprintReport> findExpiredAtOrBefore(Instant instant) {
            return storage.values().stream().filter(report -> report.isExpiredAt(instant)).toList();
        }
    }

    private static class TestStorage implements ExamSprintReportStorage {
        @Override
        public StoredExamSprintReportFile upload(
                String reportId,
                ExamSprintReportType reportType,
                String fileName,
                byte[] pdfBytes,
                Instant expiresAt) {
            String typeSegment = reportType.name().toLowerCase();
            return new StoredExamSprintReportFile("exam-sprint-reports/" + typeSegment + "/" + reportId + "/" + fileName, fileName);
        }

        @Override
        public URI generateDownloadUrl(String storageObjectKey, Duration ttl) {
            return URI.create("https://download.example.local/" + storageObjectKey + "?sig=test");
        }

        @Override
        public Optional<StoredExamSprintReportContent> download(String storageObjectKey) {
            return Optional.empty();
        }

        @Override
        public void delete(String storageObjectKey) {
        }
    }
}
