package org.raddatz.familienarchiv.filestorage;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.mock.web.MockMultipartFile;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class FileServiceTest {

    private S3Client s3Client;
    private FileService fileService;

    @BeforeEach
    void setUp() {
        s3Client = mock(S3Client.class);
        fileService = new FileService(s3Client, null, "test-bucket");
    }

    @Test
    void uploadFile_returnsS3Key() throws IOException {
        MockMultipartFile file = new MockMultipartFile(
                "file", "test.pdf", "application/pdf", new byte[]{1, 2, 3});

        FileService.UploadResult result = fileService.uploadFile(file, "test.pdf");

        assertThat(result.s3Key()).startsWith("documents/");
        assertThat(result.s3Key()).endsWith("_test.pdf");
        verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
    }

    @Test
    void uploadFile_returnsCorrectSha256FileHash() throws IOException, NoSuchAlgorithmException {
        byte[] content = "hello pdf content".getBytes();
        MockMultipartFile file = new MockMultipartFile(
                "file", "doc.pdf", "application/pdf", content);

        FileService.UploadResult result = fileService.uploadFile(file, "doc.pdf");

        // Compute expected hash independently
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(content);
        StringBuilder expected = new StringBuilder();
        for (byte b : hashBytes) {
            expected.append(String.format("%02x", b));
        }

        assertThat(result.fileHash()).isEqualTo(expected.toString());
    }

    @Test
    void uploadFile_differentContents_produceDifferentHashes() throws IOException {
        MockMultipartFile file1 = new MockMultipartFile(
                "f", "a.pdf", "application/pdf", new byte[]{1, 2, 3});
        MockMultipartFile file2 = new MockMultipartFile(
                "f", "b.pdf", "application/pdf", new byte[]{4, 5, 6});

        FileService.UploadResult r1 = fileService.uploadFile(file1, "a.pdf");
        FileService.UploadResult r2 = fileService.uploadFile(file2, "b.pdf");

        assertThat(r1.fileHash()).isNotEqualTo(r2.fileHash());
    }

    @Test
    void uploadFile_sameContents_produceSameHash() throws IOException {
        byte[] content = new byte[]{10, 20, 30};
        MockMultipartFile file1 = new MockMultipartFile("f", "x.pdf", "application/pdf", content);
        MockMultipartFile file2 = new MockMultipartFile("f", "y.pdf", "application/pdf", content);

        FileService.UploadResult r1 = fileService.uploadFile(file1, "x.pdf");
        FileService.UploadResult r2 = fileService.uploadFile(file2, "y.pdf");

        assertThat(r1.fileHash()).isEqualTo(r2.fileHash());
    }

    @Test
    void uploadFile_throwsIOException_whenS3Throws() {
        MockMultipartFile file = new MockMultipartFile("f", "fail.pdf", "application/pdf", new byte[]{1});
        S3Exception s3ex = (S3Exception) S3Exception.builder().message("bucket error").statusCode(500).build();
        when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(s3ex);

        assertThatThrownBy(() -> fileService.uploadFile(file, "fail.pdf"))
                .isInstanceOf(IOException.class)
                .hasMessageContaining("Failed to upload");
    }

    // ─── downloadFile ─────────────────────────────────────────────────────────

    @Test
    void downloadFile_returnsResourceWithContentType() {
        byte[] content = "pdf content".getBytes();
        GetObjectResponse response = GetObjectResponse.builder().contentType("application/pdf").build();
        ResponseInputStream<GetObjectResponse> stream = new ResponseInputStream<>(
                response, AbortableInputStream.create(new ByteArrayInputStream(content)));
        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(stream);

        FileService.S3FileDownload result = fileService.downloadFile("documents/test.pdf");

        assertThat(result.contentType()).isEqualTo("application/pdf");
        assertThat(result.resource()).isNotNull();
    }

    @Test
    void downloadFile_fallsBackToOctetStream_whenContentTypeIsBlank() {
        byte[] content = "data".getBytes();
        GetObjectResponse response = GetObjectResponse.builder().contentType("   ").build();
        ResponseInputStream<GetObjectResponse> stream = new ResponseInputStream<>(
                response, AbortableInputStream.create(new ByteArrayInputStream(content)));
        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(stream);

        FileService.S3FileDownload result = fileService.downloadFile("documents/file");

        assertThat(result.contentType()).isEqualTo("application/octet-stream");
    }

    @Test
    void downloadFile_fallsBackToOctetStream_whenContentTypeIsNull() {
        byte[] content = "data".getBytes();
        GetObjectResponse response = GetObjectResponse.builder().build(); // no contentType
        ResponseInputStream<GetObjectResponse> stream = new ResponseInputStream<>(
                response, AbortableInputStream.create(new ByteArrayInputStream(content)));
        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(stream);

        FileService.S3FileDownload result = fileService.downloadFile("documents/file");

        assertThat(result.contentType()).isEqualTo("application/octet-stream");
    }

    @Test
    void downloadFile_throwsStorageFileNotFoundException_whenNoSuchKey() {
        NoSuchKeyException ex = NoSuchKeyException.builder().message("not found").statusCode(404).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFile("missing/key.pdf"))
                .isInstanceOf(FileService.StorageFileNotFoundException.class)
                .hasMessageContaining("missing/key.pdf");
    }

    @Test
    void downloadFile_throwsRuntimeException_whenS3Exception() {
        S3Exception ex = (S3Exception) S3Exception.builder().message("storage error").statusCode(503).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFile("documents/file.pdf"))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Storage Error");
    }

    // ─── downloadFileBytes ────────────────────────────────────────────────────

    @Test
    void downloadFileBytes_returnsRawBytes() throws IOException {
        byte[] content = "raw bytes".getBytes();
        GetObjectResponse response = GetObjectResponse.builder().build();
        ResponseInputStream<GetObjectResponse> stream = new ResponseInputStream<>(
                response, AbortableInputStream.create(new ByteArrayInputStream(content)));
        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(stream);

        byte[] result = fileService.downloadFileBytes("documents/file.pdf");

        assertThat(result).isEqualTo(content);
    }

    @Test
    void downloadFileBytes_throwsStorageFileNotFoundException_whenNoSuchKey() {
        NoSuchKeyException ex = NoSuchKeyException.builder().message("not found").statusCode(404).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFileBytes("missing/key.pdf"))
                .isInstanceOf(FileService.StorageFileNotFoundException.class);
    }

    @Test
    void downloadFileBytes_throwsIOException_whenS3Exception() {
        S3Exception ex = (S3Exception) S3Exception.builder().message("storage error").statusCode(503).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFileBytes("documents/file.pdf"))
                .isInstanceOf(IOException.class)
                .hasMessageContaining("Failed to download");
    }

    // ─── downloadFileStream ────────────────────────────────────────────────────

    @Test
    void downloadFileStream_returnsStreamableContent() throws IOException {
        byte[] content = "streamed bytes".getBytes();
        GetObjectResponse response = GetObjectResponse.builder().contentType("application/pdf").build();
        ResponseInputStream<GetObjectResponse> stream = new ResponseInputStream<>(
                response, AbortableInputStream.create(new ByteArrayInputStream(content)));
        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(stream);

        try (java.io.InputStream result = fileService.downloadFileStream("documents/file.pdf")) {
            assertThat(result.readAllBytes()).isEqualTo(content);
        }
    }

    @Test
    void downloadFileStream_throwsStorageFileNotFoundException_whenNoSuchKey() {
        NoSuchKeyException ex = NoSuchKeyException.builder().message("not found").statusCode(404).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFileStream("missing/key.pdf"))
                .isInstanceOf(FileService.StorageFileNotFoundException.class)
                .hasMessageContaining("missing/key.pdf");
    }

    @Test
    void downloadFileStream_throwsIOException_whenS3Exception() {
        S3Exception ex = (S3Exception) S3Exception.builder().message("storage error").statusCode(503).build();
        when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex);

        assertThatThrownBy(() -> fileService.downloadFileStream("documents/file.pdf"))
                .isInstanceOf(IOException.class)
                .hasMessageContaining("Failed to open stream");
    }
}
