package org.raddatz.familienarchiv.importing;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.document.ThumbnailAsyncRunner;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagService;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.test.util.ReflectionTestUtils;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

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.*;

@ExtendWith(MockitoExtension.class)
class MassImportServiceTest {

    @Mock DocumentService documentService;
    @Mock PersonService personService;
    @Mock TagService tagService;
    @Mock S3Client s3Client;
    @Mock ThumbnailAsyncRunner thumbnailAsyncRunner;

    MassImportService service;

    @BeforeEach
    void setUp() {
        service = new MassImportService(documentService, personService, tagService, s3Client, thumbnailAsyncRunner);
        ReflectionTestUtils.setField(service, "bucketName", "test-bucket");
        ReflectionTestUtils.setField(service, "colIndex", 0);
        ReflectionTestUtils.setField(service, "colBox", 1);
        ReflectionTestUtils.setField(service, "colFolder", 2);
        ReflectionTestUtils.setField(service, "colSender", 3);
        ReflectionTestUtils.setField(service, "colReceivers", 5);
        ReflectionTestUtils.setField(service, "colDate", 7);
        ReflectionTestUtils.setField(service, "colLocation", 9);
        ReflectionTestUtils.setField(service, "colTags", 10);
        ReflectionTestUtils.setField(service, "colSummary", 11);
        ReflectionTestUtils.setField(service, "colTranscription", 13);
    }

    // ─── getStatus ────────────────────────────────────────────────────────────

    @Test
    void getStatus_returnsIdleByDefault() {
        assertThat(service.getStatus().state()).isEqualTo(MassImportService.State.IDLE);
    }

    // ─── runImportAsync ───────────────────────────────────────────────────────

    @Test
    void runImportAsync_setsFailedStatus_whenImportDirectoryDoesNotExist() {
        // /import directory doesn't exist in test environment → findSpreadsheetFile throws
        service.runImportAsync();

        assertThat(service.getStatus().state()).isEqualTo(MassImportService.State.FAILED);
    }

    @Test
    void runImportAsync_throwsConflict_whenAlreadyRunning() {
        MassImportService.ImportStatus running = new MassImportService.ImportStatus(
                MassImportService.State.RUNNING, "Running...", 0, LocalDateTime.now());
        ReflectionTestUtils.setField(service, "currentStatus", running);

        assertThatThrownBy(() -> service.runImportAsync())
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("already in progress");
    }

    // ─── importSingleDocument — skip already uploaded ─────────────────────────

    @Test
    void importSingleDocument_skips_whenDocumentAlreadyUploadedNotPlaceholder() {
        Document existing = Document.builder()
                .id(UUID.randomUUID())
                .originalFilename("doc001.pdf")
                .status(DocumentStatus.UPLOADED)
                .build();
        when(documentService.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.of(existing));

        service.importSingleDocument(minimalCells("doc001.pdf"), Optional.empty(), "doc001.pdf", "doc001");

        verify(documentService, never()).save(any());
    }

    // ─── importSingleDocument — create new document (metadata only) ───────────

    @Test
    void importSingleDocument_createsNewDocument_whenNotExists() {
        when(documentService.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        service.importSingleDocument(minimalCells("doc002.pdf"), Optional.empty(), "doc002.pdf", "doc002");

        verify(documentService).save(argThat(d ->
                d.getOriginalFilename().equals("doc002.pdf")
                        && d.getStatus() == DocumentStatus.PLACEHOLDER));
    }

    // ─── importSingleDocument — update existing placeholder ──────────────────

    @Test
    void importSingleDocument_updatesExistingPlaceholder() {
        Document placeholder = Document.builder()
                .id(UUID.randomUUID())
                .originalFilename("existing.pdf")
                .status(DocumentStatus.PLACEHOLDER)
                .build();
        when(documentService.findByOriginalFilename("existing.pdf")).thenReturn(Optional.of(placeholder));
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        service.importSingleDocument(minimalCells("existing.pdf"), Optional.empty(), "existing.pdf", "existing");

        verify(documentService).save(same(placeholder));
    }

    // ─── importSingleDocument — with file (S3 upload) ─────────────────────────

    @Test
    void importSingleDocument_uploadsFileToS3_andSetsStatusUploaded(@TempDir Path tempDir) throws Exception {
        Path tempFile = tempDir.resolve("doc003.pdf");
        Files.write(tempFile, "PDF content".getBytes());

        when(documentService.findByOriginalFilename("doc003.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        service.importSingleDocument(
                minimalCells("doc003.pdf"), Optional.of(tempFile.toFile()), "doc003.pdf", "doc003");

        verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
        verify(documentService).save(argThat(d -> d.getStatus() == DocumentStatus.UPLOADED));
    }

    @Test
    void importSingleDocument_returnsEarly_whenS3UploadFails(@TempDir Path tempDir) throws Exception {
        Path tempFile = tempDir.resolve("fail.pdf");
        Files.write(tempFile, "data".getBytes());

        when(documentService.findByOriginalFilename("fail.pdf")).thenReturn(Optional.empty());
        doThrow(new RuntimeException("S3 error"))
                .when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));

        service.importSingleDocument(
                minimalCells("fail.pdf"), Optional.of(tempFile.toFile()), "fail.pdf", "fail");

        verify(documentService, never()).save(any());
    }

    // ─── importSingleDocument — sender handling ───────────────────────────────

    @Test
    void importSingleDocument_setsNullSender_whenSenderCellIsBlank() {
        when(documentService.findByOriginalFilename("nosender.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<String> cells = buildCells("nosender.pdf", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "nosender.pdf", "nosender");

        verify(documentService).save(argThat(d -> d.getSender() == null));
        verify(personService, never()).findOrCreateByAlias(any());
    }

    @Test
    void importSingleDocument_createsSender_whenSenderCellIsNonBlank() {
        Person sender = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Müller").build();
        when(documentService.findByOriginalFilename("withsender.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(personService.findOrCreateByAlias("Walter Müller")).thenReturn(sender);

        List<String> cells = buildCells("withsender.pdf", "Walter Müller", "", "");
        service.importSingleDocument(cells, Optional.empty(), "withsender.pdf", "withsender");

        verify(personService).findOrCreateByAlias("Walter Müller");
        verify(documentService).save(argThat(d -> d.getSender() == sender));
    }

    // ─── importSingleDocument — tag handling ─────────────────────────────────

    @Test
    void importSingleDocument_createsTag_whenTagCellIsNonBlank() {
        Tag tag = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
        when(documentService.findByOriginalFilename("tagged.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(tagService.findOrCreate("Familie")).thenReturn(tag);

        List<String> cells = buildCells("tagged.pdf", "", "", "Familie");
        service.importSingleDocument(cells, Optional.empty(), "tagged.pdf", "tagged");

        verify(tagService).findOrCreate("Familie");
    }

    @Test
    void importSingleDocument_doesNotCreateTag_whenTagCellIsBlank() {
        when(documentService.findByOriginalFilename("notag.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<String> cells = buildCells("notag.pdf", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "notag.pdf", "notag");

        verify(tagService, never()).findOrCreate(any());
    }

    // ─── importSingleDocument — metadataComplete heuristic ───────────────────

    @Test
    void importSingleDocument_metadataComplete_whenSenderPresent() {
        Person sender = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
        when(documentService.findByOriginalFilename("meta.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(personService.findOrCreateByAlias("A B")).thenReturn(sender);

        List<String> cells = buildCells("meta.pdf", "A B", "", "");
        service.importSingleDocument(cells, Optional.empty(), "meta.pdf", "meta");

        verify(documentService).save(argThat(Document::isMetadataComplete));
    }

    @Test
    void importSingleDocument_metadataIncomplete_whenNoKeyFieldsPresent() {
        when(documentService.findByOriginalFilename("nometa.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<String> cells = buildCells("nometa.pdf", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "nometa.pdf", "nometa");

        verify(documentService).save(argThat(d -> !d.isMetadataComplete()));
    }

    // ─── importSingleDocument — blank fields set to null ─────────────────────

    @Test
    void importSingleDocument_setsBlankFieldsToNull() {
        when(documentService.findByOriginalFilename("blank.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<String> cells = buildCells("blank.pdf", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "blank.pdf", "blank");

        verify(documentService).save(argThat(d ->
                d.getLocation() == null &&
                d.getSummary() == null &&
                d.getTranscription() == null &&
                d.getArchiveBox() == null &&
                d.getArchiveFolder() == null));
    }

    // ─── processRows — via ReflectionTestUtils ────────────────────────────────

    @Test
    void processRows_returnsZero_whenOnlyHeaderRow() {
        List<List<String>> rows = List.of(List.of("header", "col1"));
        Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);
        assertThat(result).isEqualTo(0);
    }

    @Test
    void processRows_skipsRowWithBlankIndex() {
        List<List<String>> rows = List.of(
                List.of("header"),
                minimalCells("")  // blank index
        );
        Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);
        assertThat(result).isEqualTo(0);
        verify(documentService, never()).findByOriginalFilename(any());
    }

    @Test
    void processRows_addsExtension_whenIndexHasNoDot() {
        when(documentService.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<List<String>> rows = List.of(
                List.of("header"),
                minimalCells("doc001")  // no dot → appends ".pdf"
        );
        Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);

        assertThat(result).isEqualTo(1);
        verify(documentService).findByOriginalFilename("doc001.pdf");
    }

    @Test
    void processRows_usesFilenameAsIs_whenIndexHasDot() {
        when(documentService.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<List<String>> rows = List.of(
                List.of("header"),
                minimalCells("doc002.pdf")  // has dot → used as-is
        );
        Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);

        assertThat(result).isEqualTo(1);
        verify(documentService).findByOriginalFilename("doc002.pdf");
    }

    // ─── importSingleDocument — non-blank optional fields ────────────────────

    @Test
    void importSingleDocument_setsNonNullOptionalFields_whenPresent() {
        when(documentService.findByOriginalFilename("rich.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // box=1, folder=2, location=9, summary=11, transcription=13
        List<String> cells = List.of(
                "rich.pdf",      // 0: index
                "Box A",         // 1: box
                "Folder B",      // 2: folder
                "",              // 3: sender
                "",              // 4: unused
                "",              // 5: receivers
                "",              // 6: unused
                "",              // 7: date
                "",              // 8: unused
                "Hamburg",       // 9: location
                "",              // 10: tags
                "A summary",     // 11: summary
                "",              // 12: unused
                "A transcript"   // 13: transcription
        );

        service.importSingleDocument(cells, Optional.empty(), "rich.pdf", "rich");

        verify(documentService).save(argThat(d ->
                "Box A".equals(d.getArchiveBox()) &&
                "Folder B".equals(d.getArchiveFolder()) &&
                "Hamburg".equals(d.getLocation()) &&
                "A summary".equals(d.getSummary()) &&
                "A transcript".equals(d.getTranscription())));
    }

    @Test
    void importSingleDocument_setsMetadataComplete_whenReceiversArePresent() {
        Person receiver = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Müller").build();
        when(documentService.findByOriginalFilename("rcv.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(personService.findOrCreateByAlias("Walter Müller")).thenReturn(receiver);

        List<String> cells = List.of(
                "rcv.pdf", "", "", "", "", "Walter Müller", "", "", "", "", "", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "rcv.pdf", "rcv");

        verify(documentService).save(argThat(Document::isMetadataComplete));
    }

    @Test
    void importSingleDocument_setsMetadataComplete_whenDateIsPresent() {
        when(documentService.findByOriginalFilename("dated.pdf")).thenReturn(Optional.empty());
        when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));

        List<String> cells = List.of(
                "dated.pdf", "", "", "", "", "", "", "2024-03-15", "", "", "", "", "", "");
        service.importSingleDocument(cells, Optional.empty(), "dated.pdf", "dated");

        verify(documentService).save(argThat(Document::isMetadataComplete));
    }

    // ─── buildTitle — null location ───────────────────────────────────────────

    @Test
    void buildTitle_withNullLocation_skipsLocationPart() {
        String result = ReflectionTestUtils.invokeMethod(service, "buildTitle",
                "doc005", LocalDate.of(1940, 5, 1), (String) null);
        assertThat(result).contains("doc005").contains("1940");
        assertThat(result).doesNotContain("Berlin");
    }

    // ─── parseDate — via ReflectionTestUtils ─────────────────────────────────

    @Test
    void parseDate_returnsNull_whenValueIsNull() {
        LocalDate result = ReflectionTestUtils.invokeMethod(service, "parseDate", (String) null);
        assertThat(result).isNull();
    }

    @Test
    void parseDate_returnsNull_whenValueIsBlank() {
        LocalDate result = ReflectionTestUtils.invokeMethod(service, "parseDate", "   ");
        assertThat(result).isNull();
    }

    @Test
    void parseDate_returnsDate_whenValidIsoFormat() {
        LocalDate result = ReflectionTestUtils.invokeMethod(service, "parseDate", "2024-03-15");
        assertThat(result).isEqualTo(LocalDate.of(2024, 3, 15));
    }

    @Test
    void parseDate_returnsNull_whenInvalidDateString() {
        LocalDate result = ReflectionTestUtils.invokeMethod(service, "parseDate", "15.03.2024");
        assertThat(result).isNull();
    }

    // ─── buildTitle — via ReflectionTestUtils ────────────────────────────────

    @Test
    void buildTitle_withDateAndLocation() {
        String result = ReflectionTestUtils.invokeMethod(service, "buildTitle",
                "doc001", LocalDate.of(1940, 5, 1), "Berlin");
        assertThat(result).contains("doc001").contains("Berlin").contains("1940");
    }

    @Test
    void buildTitle_withDateOnly() {
        String result = ReflectionTestUtils.invokeMethod(service, "buildTitle",
                "doc002", LocalDate.of(1960, 8, 15), "");
        assertThat(result).contains("doc002").contains("1960");
        assertThat(result).doesNotContain("Berlin");
    }

    @Test
    void buildTitle_withIndexOnly_whenDateAndLocationAreNull() {
        String result = ReflectionTestUtils.invokeMethod(service, "buildTitle",
                "doc003", null, "");
        assertThat(result).isEqualTo("doc003");
    }

    @Test
    void buildTitle_withLocationOnly_whenDateIsNull() {
        // date=null, location present → date part skipped, location appended
        String result = ReflectionTestUtils.invokeMethod(service, "buildTitle",
                "doc004", null, "Berlin");
        assertThat(result).contains("doc004").contains("Berlin");
        assertThat(result).doesNotContain("("); // no date part
    }

    // ─── getCell — via ReflectionTestUtils ───────────────────────────────────

    @Test
    void getCell_returnsEmptyString_whenColBeyondListSize() {
        List<String> cells = List.of("a", "b");
        String result = ReflectionTestUtils.invokeMethod(service, "getCell", cells, 5);
        assertThat(result).isEmpty();
    }

    @Test
    void getCell_returnsEmptyString_whenValueIsNull() {
        List<String> cells = new ArrayList<>();
        cells.add(null);
        cells.add("b");
        String result = ReflectionTestUtils.invokeMethod(service, "getCell", cells, 0);
        assertThat(result).isEmpty();
    }

    @Test
    void getCell_returnsTrimmedValue() {
        List<String> cells = List.of("  hello  ", "world");
        String result = ReflectionTestUtils.invokeMethod(service, "getCell", cells, 0);
        assertThat(result).isEqualTo("hello");
    }

    // ─── helpers ──────────────────────────────────────────────────────────────

    /**
     * Builds a minimal 14-element cell row with the given filename at index 0
     * and blanks for all optional fields.
     */
    private List<String> minimalCells(String filename) {
        return buildCells(filename, "", "", "");
    }

    /**
     * Builds a cell row with sender, receiver, and tag controls.
     * Layout matches the default column indices set in setUp().
     */
    private List<String> buildCells(String filename, String sender, String receivers, String tag) {
        // 14 elements: index=0,box=1,folder=2,sender=3,[4],receivers=5,[6],date=7,[8],location=9,tag=10,summary=11,[12],transcription=13
        return List.of(
                filename,  // 0: index
                "",        // 1: box
                "",        // 2: folder
                sender,    // 3: sender
                "",        // 4: (unused)
                receivers, // 5: receivers
                "",        // 6: (unused)
                "",        // 7: date
                "",        // 8: (unused)
                "",        // 9: location
                tag,       // 10: tags
                "",        // 11: summary
                "",        // 12: (unused)
                ""         // 13: transcription
        );
    }
}
