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

import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.rest.SimpleResponse;
import com.azure.core.util.Context;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.models.BlobStorageException;
import com.azure.storage.blob.models.BlockBlobItem;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URL;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(OutputCaptureExtension.class)
class AzureBlobExamSprintReportStorageTest {

    private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-01-03T08:00:00Z"), ZoneOffset.UTC);

    @Test
    void springSelectsTheAutowiredConstructor() throws Exception {
        AutowiredAnnotationBeanPostProcessor beanPostProcessor = new AutowiredAnnotationBeanPostProcessor();

        Constructor<?>[] candidateConstructors = beanPostProcessor.determineCandidateConstructors(
                AzureBlobExamSprintReportStorage.class,
                "azureBlobExamSprintReportStorage");

        Constructor<AzureBlobExamSprintReportStorage> expectedConstructor =
                AzureBlobExamSprintReportStorage.class.getConstructor(
                        String.class,
                        String.class,
                        String.class,
                        String.class,
                        String.class,
                        String.class,
                        Clock.class);

        assertThat(candidateConstructors).containsExactly(expectedConstructor);
    }

    @Test
    void generateDownloadUrlReturnsConfiguredBlobFileUrl() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        URI downloadUrl = storage.generateDownloadUrl(
                "exam-sprint-outlook-report-report-123.pdf",
                Duration.ofMinutes(15));

        assertThat(downloadUrl).isEqualTo(URI.create(
                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-123.pdf"));
        verifyNoInteractions(containerClient);
    }

    @Test
    void generateDownloadUrlNormalizesExtraSlashes() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "/reports/",
                "https://dcjxbtest.blob.core.chinacloudapi.cn/",
                FIXED_CLOCK);

        URI downloadUrl = storage.generateDownloadUrl(
                "/file.pdf/",
                Duration.ofMinutes(15));

        assertThat(downloadUrl).isEqualTo(URI.create(
                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/file.pdf"));
    }

    @Test
    void generateDownloadUrlEncodesFileNameWithoutReportScopedPath() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        URI downloadUrl = storage.generateDownloadUrl(
                "John Doe-报告 #1?.pdf",
                Duration.ofMinutes(15));

        assertThat(downloadUrl.toString()).isEqualTo(
                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/John%20Doe-报告%20%231%3F.pdf");
        assertThat(downloadUrl.getRawPath()).isEqualTo(
                "/reports/John%20Doe-报告%20%231%3F.pdf");
        assertThat(downloadUrl.getRawQuery()).isNull();
        assertThat(downloadUrl.getRawFragment()).isNull();
    }

    @Test
    void generateDownloadUrlRejectsBlankStorageObjectKey() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        assertThatThrownBy(() -> storage.generateDownloadUrl("   ", Duration.ofMinutes(15)))
                .isInstanceOf(IllegalStateException.class)
                .hasMessage("Azure storage storageObjectKey is incomplete");
    }

    @Test
    void generateDownloadUrlRejectsAllSlashStorageObjectKey() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        assertThatThrownBy(() -> storage.generateDownloadUrl("///", Duration.ofMinutes(15)))
                .isInstanceOf(IllegalStateException.class)
                .hasMessage("Azure storage storageObjectKey is incomplete");
    }

    @Test
    void constructorRejectsAllSlashDownloadUrlPrefix() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);

        assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "///",
                FIXED_CLOCK))
                .isInstanceOf(IllegalStateException.class)
                .hasMessage("Azure storage download-url-prefix is incomplete");
    }

    @Test
    void constructorRejectsBlankAfterTrimDownloadUrlPrefix() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);

        assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                " / ",
                FIXED_CLOCK))
                .isInstanceOf(IllegalStateException.class)
                .hasMessage("Azure storage download-url-prefix is incomplete");
    }

    @Test
    void uploadUsesFileNameAsBlobNameAndSendsHeadersAndMetadataInUploadRequest() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        BlobClient blobClient = mock(BlobClient.class);
        when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        ExamSprintReportStorage.StoredExamSprintReportFile storedFile = storage.upload(
                "report-123",
                ReportType.OUTLOOK,
                "file.pdf",
                new byte[]{1, 2, 3},
                Instant.parse("2026-01-10T00:00:00Z"));

        assertThat(storedFile.storageObjectKey()).isEqualTo("file.pdf");
        assertThat(storedFile.fileName()).isEqualTo("file.pdf");
        verify(containerClient).getBlobClient("file.pdf");
        verify(blobClient).uploadWithResponse(argThat(options ->
                        options.getLength() == 3
                                && options.getHeaders() != null
                                && "application/pdf".equals(options.getHeaders().getContentType())
                                && "report-123".equals(options.getMetadata().get("reportId"))
                                && "OUTLOOK".equals(options.getMetadata().get("reportType"))
                                && "2026-01-10T00:00:00Z".equals(options.getMetadata().get("expiresAt"))
                                && FIXED_CLOCK.instant().toString().equals(options.getMetadata().get("uploadedAt"))),
                eq(Context.NONE));
    }

    @Test
    void uploadLogsClientResolutionUploadRequestCompletionAndTotalDuration(CapturedOutput output) {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        BlobClient blobClient = mock(BlobClient.class);
        when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
                .thenReturn(successfulUploadResponse());
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        storage.upload(
                "report-123",
                ReportType.OUTLOOK,
                "file.pdf",
                new byte[]{1, 2, 3},
                Instant.parse("2026-01-10T00:00:00Z"));

        assertThat(output.getAll())
                .contains("exam_sprint_report_azure_storage_upload_stage_completed")
                .contains("stage=client_resolved")
                .contains("stage=upload_request_completed")
                .contains("exam_sprint_report_azure_storage_upload_completed")
                .contains("reportId=report-123")
                .contains("reportType=OUTLOOK")
                .contains("storageObjectKey=file.pdf")
                .contains("blobName=file.pdf")
                .contains("fileName=file.pdf")
                .contains("pdfByteLength=3")
                .contains("durationMs=")
                .contains("azureStatusCode=201")
                .contains("azureRequestId=azure-request-123")
                .contains("azureClientRequestId=client-request-456")
                .contains("azureETag=etag-789")
                .contains("azureLastModified=2026-01-03T08:01Z")
                .doesNotContain("connection-string")
                .doesNotContain("accountKey")
                .doesNotContain("account-key");
    }

    @Test
    void uploadLogsAzureDiagnosticsAndRethrowsWhenUploadRequestFails(CapturedOutput output) throws Exception {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        BlobClient blobClient = mock(BlobClient.class);
        when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
                .thenThrow(blobStorageException());
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        assertThatThrownBy(() -> storage.upload(
                "report-123",
                ReportType.OUTLOOK,
                "file.pdf",
                new byte[]{1, 2, 3},
                Instant.parse("2026-01-10T00:00:00Z")))
                .isInstanceOf(BlobStorageException.class);

        assertThat(output.getAll())
                .contains("exam_sprint_report_azure_storage_upload_failed")
                .contains("reportId=report-123")
                .contains("reportType=OUTLOOK")
                .contains("storageObjectKey=file.pdf")
                .contains("blobName=file.pdf")
                .contains("fileName=file.pdf")
                .contains("pdfByteLength=3")
                .contains("durationMs=")
                .contains("exceptionType=BlobStorageException")
                .contains("azureStatusCode=503")
                .contains("azureRequestId=azure-request-failed")
                .contains("azureClientRequestId=client-request-failed")
                .contains("azureErrorCode=ServerBusy")
                .doesNotContain("connection-string")
                .doesNotContain("accountKey")
                .doesNotContain("account-key");
    }

    @Test
    void uploadUsesDisplayFileNameAsTheStorageObjectKey() {
        BlobContainerClient containerClient = mock(BlobContainerClient.class);
        BlobClient blobClient = mock(BlobClient.class);
        when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                containerClient,
                "reports",
                "https://dcjxbtest.blob.core.chinacloudapi.cn",
                FIXED_CLOCK);

        ExamSprintReportStorage.StoredExamSprintReportFile firstStoredFile = storage.upload(
                "report-123",
                ReportType.OUTLOOK,
                "same-display-name.pdf",
                new byte[]{1},
                Instant.parse("2026-01-10T00:00:00Z"));
        ExamSprintReportStorage.StoredExamSprintReportFile secondStoredFile = storage.upload(
                "report-456",
                ReportType.OUTLOOK,
                "same-display-name.pdf",
                new byte[]{2},
                Instant.parse("2026-01-10T00:00:00Z"));

        assertThat(firstStoredFile.storageObjectKey()).isEqualTo("same-display-name.pdf");
        assertThat(secondStoredFile.storageObjectKey()).isEqualTo("same-display-name.pdf");
        assertThat(firstStoredFile.fileName()).isEqualTo("same-display-name.pdf");
        assertThat(secondStoredFile.fileName()).isEqualTo("same-display-name.pdf");
    }

    private SimpleResponse<BlockBlobItem> successfulUploadResponse() {
        HttpHeaders headers = new HttpHeaders()
                .set(HttpHeaderName.X_MS_REQUEST_ID, "azure-request-123")
                .set(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-456");
        BlockBlobItem blockBlobItem = new BlockBlobItem(
                "etag-789",
                OffsetDateTime.parse("2026-01-03T08:01:00Z"),
                null,
                true,
                null);
        return new SimpleResponse<>(
                new HttpRequest(HttpMethod.PUT, "https://example.test/reports/file.pdf"),
                201,
                headers,
                blockBlobItem);
    }

    private BlobStorageException blobStorageException() throws Exception {
        HttpResponse response = mock(HttpResponse.class);
        when(response.getStatusCode()).thenReturn(503);
        when(response.getHeaders()).thenReturn(new HttpHeaders()
                .set(HttpHeaderName.X_MS_REQUEST_ID, "azure-request-failed")
                .set(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-failed")
                .set("x-ms-error-code", "ServerBusy"));
        when(response.getHeaderValue(HttpHeaderName.X_MS_REQUEST_ID)).thenReturn("azure-request-failed");
        when(response.getHeaderValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID)).thenReturn("client-request-failed");
        when(response.getRequest()).thenReturn(new HttpRequest(HttpMethod.PUT, new URL("https://example.test/reports/file.pdf")));
        return new BlobStorageException("ServerBusy", response, null);
    }
}
