package org.raddatz.familienarchiv.geschichte.journeyitem;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentRepository;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.geschichte.Geschichte;
import org.raddatz.familienarchiv.geschichte.GeschichteRepository;
import org.raddatz.familienarchiv.geschichte.GeschichteStatus;
import org.raddatz.familienarchiv.geschichte.GeschichteType;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.AppUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import software.amazon.awssdk.services.s3.S3Client;

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.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class JourneyItemDocumentDeleteTest {

    @MockitoBean
    S3Client s3Client;

    @MockitoBean
    AuditService auditService;

    @MockitoSpyBean
    DocumentRepository documentRepository;

    @PersistenceContext
    EntityManager em;

    @Autowired DocumentService documentService;
    @Autowired JourneyItemRepository journeyItemRepository;
    @Autowired GeschichteRepository geschichteRepository;
    @Autowired DocumentRepository docRepo;
    @Autowired AppUserRepository appUserRepository;
    @Autowired JdbcTemplate jdbcTemplate;

    Geschichte journey;
    Document doc;
    AppUser writer;

    @BeforeEach
    void seed() {
        writer = appUserRepository.save(AppUser.builder()
                .email("delete-test-writer@test")
                .password("hash")
                .build());
        doc = docRepo.save(Document.builder()
                .title("Testbrief")
                .originalFilename("testbrief.pdf")
                .status(DocumentStatus.UPLOADED)
                .build());
        journey = geschichteRepository.save(Geschichte.builder()
                .title("Eine Lesereise")
                .status(GeschichteStatus.DRAFT)
                .type(GeschichteType.JOURNEY)
                .build());
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(writer.getEmail(), null,
                        List.of(new SimpleGrantedAuthority("BLOG_WRITE"))));
    }

    @AfterEach
    void cleanup() {
        SecurityContextHolder.clearContext();
        reset(documentRepository);
        // Deletion order is FK-load-bearing: journey_items reference both documents
        // and geschichten, so children must be removed before their parents.
        journeyItemRepository.deleteAll();
        docRepo.deleteAll();
        geschichteRepository.deleteAll();
        appUserRepository.deleteAll();
    }

    // ─── AC-1: headline ───────────────────────────────────────────────────────

    @Test
    void deleting_document_linked_via_note_less_item_deletes_item_not_500() {
        JourneyItem item = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).document(doc).build());
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());

        assertThat(journeyItemRepository.findById(item.getId())).isEmpty();
        assertThat(docRepo.findById(doc.getId())).isEmpty();
    }

    // ─── AC-2: note-carrying item survives as placeholder ─────────────────────

    @Test
    void deleting_document_preserves_note_carrying_item_as_placeholder() {
        JourneyItem item = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).document(doc).note("curator context").build());
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());
        em.clear();

        JourneyItem surviving = journeyItemRepository.findById(item.getId()).orElseThrow();
        assertThat(surviving.getDocumentId()).isNull();
        assertThat(surviving.getNote()).isEqualTo("curator context");
    }

    // ─── AC-3: note-only item untouched ───────────────────────────────────────

    @Test
    void deleting_document_does_not_affect_note_only_item() {
        JourneyItem noteOnly = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).note("Einleitung").build());
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());
        em.clear();

        JourneyItem reloaded = journeyItemRepository.findById(noteOnly.getId()).orElseThrow();
        assertThat(reloaded.getDocumentId()).isNull();
        assertThat(reloaded.getNote()).isEqualTo("Einleitung");
    }

    // ─── AC-4: asymmetric multi-journey ───────────────────────────────────────

    @Test
    void deleting_document_applies_independently_per_referencing_item() {
        Geschichte journey2 = geschichteRepository.save(Geschichte.builder()
                .title("Zweite Reise")
                .status(GeschichteStatus.DRAFT)
                .type(GeschichteType.JOURNEY)
                .build());

        JourneyItem noteLess = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).document(doc).build());
        JourneyItem noteCarrying = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey2).position(10).document(doc).note("Begleittext").build());
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());
        em.clear();

        assertThat(journeyItemRepository.findById(noteLess.getId())).isEmpty();
        JourneyItem surviving = journeyItemRepository.findById(noteCarrying.getId()).orElseThrow();
        assertThat(surviving.getDocumentId()).isNull();
        assertThat(surviving.getNote()).isEqualTo("Begleittext");
    }

    // ─── AC-5: rollback guard ─────────────────────────────────────────────────

    @Test
    void listener_deletes_roll_back_when_document_delete_fails() {
        JourneyItem item = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).document(doc).build());
        em.clear();

        doThrow(new RuntimeException("simulated failure"))
                .when(documentRepository).deleteById(any());

        assertThatThrownBy(() -> documentService.deleteDocument(doc.getId(), writer.getId()))
                .isInstanceOf(RuntimeException.class);

        em.clear();
        assertThat(journeyItemRepository.findById(item.getId())).isPresent();
    }

    // ─── AC-6: empty-string note boundary ────────────────────────────────────

    @Test
    void empty_string_note_item_is_cascaded_whitespace_only_note_is_preserved() {
        // uq_journey_items_geschichte_document prevents two items with the same
        // (geschichte_id, document_id) in one journey — use two separate journeys.
        Geschichte journey2 = geschichteRepository.save(Geschichte.builder()
                .title("Zweite Reise für AC-6")
                .status(GeschichteStatus.DRAFT)
                .type(GeschichteType.JOURNEY)
                .build());

        UUID emptyNoteItemId = UUID.randomUUID();
        UUID whitespaceNoteItemId = UUID.randomUUID();

        jdbcTemplate.update(
                "INSERT INTO journey_items (id, geschichte_id, position, document_id, note) VALUES (?,?,?,?,?)",
                emptyNoteItemId, journey.getId(), 10, doc.getId(), "");
        jdbcTemplate.update(
                "INSERT INTO journey_items (id, geschichte_id, position, document_id, note) VALUES (?,?,?,?,?)",
                whitespaceNoteItemId, journey2.getId(), 20, doc.getId(), "   ");
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());
        em.clear();

        assertThat(journeyItemRepository.findById(emptyNoteItemId)).isEmpty();
        JourneyItem whitespaceItem = journeyItemRepository.findById(whitespaceNoteItemId).orElseThrow();
        assertThat(whitespaceItem.getDocumentId()).isNull();
        assertThat(whitespaceItem.getNote()).isEqualTo("   ");
    }

    // ─── Idempotency / no-collateral ──────────────────────────────────────────

    @Test
    void deleting_document_in_zero_journeys_returns_no_collateral() {
        Document unlinked = docRepo.save(Document.builder()
                .title("Unverknüpfter Brief")
                .originalFilename("other.pdf")
                .status(DocumentStatus.UPLOADED)
                .build());
        JourneyItem unrelated = journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).note("unrelated note").build());
        em.clear();

        documentService.deleteDocument(unlinked.getId(), writer.getId());
        em.clear();

        assertThat(docRepo.findById(unlinked.getId())).isEmpty();
        assertThat(journeyItemRepository.findById(unrelated.getId())).isPresent();
        assertThat(journeyItemRepository.count()).isEqualTo(1);
    }

    // ─── AC-7: audit — DOCUMENT_DELETED emitted, JOURNEY_ITEM_REMOVED absent ─

    @Test
    void deleting_document_emits_document_audit_but_no_journey_item_audit() {
        journeyItemRepository.save(
                JourneyItem.builder().geschichte(journey).position(10).document(doc).build());
        em.clear();

        documentService.deleteDocument(doc.getId(), writer.getId());

        verify(auditService).logAfterCommit(eq(AuditKind.DOCUMENT_DELETED), eq(writer.getId()), eq(doc.getId()), any());
        verify(auditService, never()).logAfterCommit(eq(AuditKind.JOURNEY_ITEM_REMOVED), any(), any(), any());
    }
}
