package org.raddatz.familienarchiv.importing;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.document.DatePrecision;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.person.relationship.RelationType;
import org.raddatz.familienarchiv.person.relationship.RelationshipService;
import org.raddatz.familienarchiv.person.relationship.dto.NetworkDTO;
import org.raddatz.familienarchiv.person.relationship.dto.PersonNodeDTO;
import org.raddatz.familienarchiv.person.relationship.dto.RelationshipDTO;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
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.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CanonicalImportOrchestratorTest {

    @Mock TagTreeImporter tagTreeImporter;
    @Mock PersonRegisterImporter personRegisterImporter;
    @Mock PersonTreeImporter personTreeImporter;
    @Mock DocumentImporter documentImporter;
    @Mock RelationshipService relationshipService;

    private CanonicalImportOrchestrator orchestrator(Path dir) {
        CanonicalImportOrchestrator o = new CanonicalImportOrchestrator(
                tagTreeImporter, personRegisterImporter, personTreeImporter, documentImporter,
                relationshipService);
        ReflectionTestUtils.setField(o, "canonicalDir", dir.toString());
        return o;
    }

    private void writeAllArtifacts(Path dir) throws Exception {
        Files.writeString(dir.resolve("canonical-tag-tree.xlsx"), "x");
        Files.writeString(dir.resolve("canonical-persons.xlsx"), "x");
        Files.writeString(dir.resolve("canonical-persons-tree.json"), "x");
        Files.writeString(dir.resolve("canonical-documents.xlsx"), "x");
    }

    @Test
    void getStatus_isIdleByDefault(@TempDir Path dir) {
        assertThat(orchestrator(dir).getStatus().state()).isEqualTo(ImportStatus.State.IDLE);
    }

    @Test
    void runImport_loadsTagsAndPersonsBeforeDocuments(@TempDir Path dir) throws Exception {
        writeAllArtifacts(dir);
        when(documentImporter.load(any())).thenReturn(new DocumentImporter.LoadResult(0, List.of()));
        when(relationshipService.getFamilyNetwork()).thenReturn(new NetworkDTO(List.of(), List.of()));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        InOrder order = inOrder(tagTreeImporter, personRegisterImporter, personTreeImporter, documentImporter);
        order.verify(tagTreeImporter).load(any());
        order.verify(personRegisterImporter).load(any());
        order.verify(personTreeImporter).load(any());
        order.verify(documentImporter).load(any());
    }

    @Test
    void runImport_setsStatusDone_onSuccess(@TempDir Path dir) throws Exception {
        writeAllArtifacts(dir);
        when(documentImporter.load(any())).thenReturn(new DocumentImporter.LoadResult(3, List.of()));
        when(relationshipService.getFamilyNetwork()).thenReturn(new NetworkDTO(List.of(), List.of()));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        assertThat(o.getStatus().state()).isEqualTo(ImportStatus.State.DONE);
        assertThat(o.getStatus().processed()).isEqualTo(3);
    }

    @Test
    void runImport_failsClosed_whenAnArtifactIsMissing(@TempDir Path dir) throws Exception {
        Files.writeString(dir.resolve("canonical-tag-tree.xlsx"), "x");
        // the other three artifacts are absent
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        assertThat(o.getStatus().state()).isEqualTo(ImportStatus.State.FAILED);
        verify(tagTreeImporter, never()).load(any());
        verify(documentImporter, never()).load(any());
    }

    @Test
    void runImport_setsStatusFailed_whenLoaderThrows(@TempDir Path dir) throws Exception {
        writeAllArtifacts(dir);
        when(tagTreeImporter.load(any())).thenThrow(DomainException.badRequest(
                org.raddatz.familienarchiv.exception.ErrorCode.IMPORT_ARTIFACT_INVALID, "bad"));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        assertThat(o.getStatus().state()).isEqualTo(ImportStatus.State.FAILED);
        verify(documentImporter, never()).load(any());
    }

    @Test
    void runImportAsync_throwsConflict_whenAlreadyRunning(@TempDir Path dir) {
        CanonicalImportOrchestrator o = orchestrator(dir);
        ReflectionTestUtils.setField(o, "currentStatus", new ImportStatus(
                ImportStatus.State.RUNNING, "IMPORT_RUNNING", "running", 0, List.of(), null));

        assertThatThrownBy(o::runImportAsync)
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("already in progress");
    }

    @Test
    void runImport_aggregatesDocumentSkips(@TempDir Path dir) throws Exception {
        writeAllArtifacts(dir);
        when(documentImporter.load(any())).thenReturn(new DocumentImporter.LoadResult(1,
                List.of(new ImportStatus.SkippedFile("fake.pdf", ImportStatus.SkipReason.INVALID_PDF_SIGNATURE))));
        when(relationshipService.getFamilyNetwork()).thenReturn(new NetworkDTO(List.of(), List.of()));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        assertThat(o.getStatus().skipped()).isEqualTo(1);
        assertThat(o.getStatus().skippedFiles())
                .extracting(ImportStatus.SkippedFile::filename)
                .containsExactly("fake.pdf");
    }

    // ─── generation monotonicity soft-check (#689) ─────────────────────────────

    @Test
    void runImport_invokesGetFamilyNetwork_afterPersonLoaders_beforeDocuments(@TempDir Path dir) throws Exception {
        writeAllArtifacts(dir);
        when(documentImporter.load(any())).thenReturn(new DocumentImporter.LoadResult(0, List.of()));
        when(relationshipService.getFamilyNetwork()).thenReturn(new NetworkDTO(List.of(), List.of()));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        InOrder order = inOrder(personRegisterImporter, personTreeImporter, relationshipService, documentImporter);
        order.verify(personRegisterImporter).load(any());
        order.verify(personTreeImporter).load(any());
        order.verify(relationshipService).getFamilyNetwork();
        order.verify(documentImporter).load(any());
    }

    @Test
    void runImport_completes_evenWhenMonotonicityViolatingEdgePresent(@TempDir Path dir) throws Exception {
        // child.generation (2) <= parent.generation (3) — monotonicity violation.
        // The orchestrator must WARN and continue; it must not abort or fail-closed.
        writeAllArtifacts(dir);
        UUID parentId = UUID.randomUUID();
        UUID childId = UUID.randomUUID();
        PersonNodeDTO parent = new PersonNodeDTO(parentId, "Parent", null, null, 3, true);
        PersonNodeDTO child = new PersonNodeDTO(childId, "Child", null, null, 2, true);
        RelationshipDTO edge = new RelationshipDTO(
                UUID.randomUUID(), parentId, childId,
                "Parent", null, null, "Child", null, null,
                RelationType.PARENT_OF, null, DatePrecision.UNKNOWN, null, DatePrecision.UNKNOWN, null);
        when(relationshipService.getFamilyNetwork())
                .thenReturn(new NetworkDTO(List.of(parent, child), List.of(edge)));
        when(documentImporter.load(any())).thenReturn(new DocumentImporter.LoadResult(0, List.of()));
        CanonicalImportOrchestrator o = orchestrator(dir);

        o.runImport();

        assertThat(o.getStatus().state()).isEqualTo(ImportStatus.State.DONE);
        verify(documentImporter).load(any());
    }
}
