package org.raddatz.familienarchiv.geschichte;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.geschichte.GeschichteUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.geschichte.Geschichte;
import org.raddatz.familienarchiv.geschichte.GeschichteStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.geschichte.GeschichteRepository;
import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class GeschichteServiceTest {

    @Mock
    GeschichteRepository geschichteRepository;
    @Mock
    PersonService personService;
    @Mock
    DocumentService documentService;
    @Mock
    UserService userService;

    @InjectMocks
    GeschichteService geschichteService;

    AppUser writer;
    AppUser reader;

    @BeforeEach
    void setUp() {
        SecurityContextHolder.clearContext();
        writer = AppUser.builder().id(UUID.randomUUID()).email("writer@test").build();
        reader = AppUser.builder().id(UUID.randomUUID()).email("reader@test").build();
    }

    @AfterEach
    void tearDown() {
        SecurityContextHolder.clearContext();
    }

    // ─── getById ──────────────────────────────────────────────────────────────

    @Test
    void getById_throws_NOT_FOUND_for_draft_when_user_lacks_BLOG_WRITE() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID id = UUID.randomUUID();
        Geschichte draft = draft(id);
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(draft));

        assertThatThrownBy(() -> geschichteService.getById(id))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.GESCHICHTE_NOT_FOUND);
    }

    @Test
    void getById_returns_draft_when_user_has_BLOG_WRITE() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        Geschichte draft = draft(id);
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(draft));

        Geschichte result = geschichteService.getById(id);

        assertThat(result).isSameAs(draft);
    }

    @Test
    void getById_returns_published_to_anyone_authenticated() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID id = UUID.randomUUID();
        Geschichte published = published(id);
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(published));

        Geschichte result = geschichteService.getById(id);

        assertThat(result).isSameAs(published);
    }

    @Test
    void getById_throws_NOT_FOUND_when_id_unknown() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID id = UUID.randomUUID();
        when(geschichteRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> geschichteService.getById(id))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.GESCHICHTE_NOT_FOUND);
    }

    // ─── list ─────────────────────────────────────────────────────────────────

    @Test
    void list_forces_PUBLISHED_status_for_reader_without_BLOG_WRITE() {
        authenticateAs(reader, Permission.READ_ALL);
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of(published(UUID.randomUUID())));

        geschichteService.list(/*status*/ null, /*personIds*/ List.of(), /*documentId*/ null, /*limit*/ 50);

        // Status pinning lives inside the Specification; we assert end-to-end behaviour
        // in GeschichteServiceIntegrationTest. Here we just confirm the service routes
        // through the spec-aware repository method.
        verify(geschichteRepository).findAll(any(Specification.class), any(Sort.class));
    }

    @Test
    void list_passes_null_status_through_for_BLOG_WRITER_so_drafts_are_visible() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of(draft(UUID.randomUUID()), published(UUID.randomUUID())));

        List<Geschichte> out = geschichteService.list(null, List.of(), null, 50);

        assertThat(out).hasSize(2);
        verify(geschichteRepository).findAll(any(Specification.class), any(Sort.class));
    }

    @Test
    void list_invokes_repository_findAll_when_filtering_by_single_personId() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID personId = UUID.randomUUID();
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of());

        geschichteService.list(null, List.of(personId), null, 50);

        verify(geschichteRepository).findAll(any(Specification.class), any(Sort.class));
    }

    @Test
    void list_invokes_repository_findAll_when_filtering_by_multiple_personIds() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID a = UUID.randomUUID();
        UUID b = UUID.randomUUID();
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of());

        geschichteService.list(null, List.of(a, b), null, 50);

        verify(geschichteRepository).findAll(any(Specification.class), any(Sort.class));
    }

    @Test
    void list_filters_by_documentId() {
        authenticateAs(reader, Permission.READ_ALL);
        UUID documentId = UUID.randomUUID();
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of());

        geschichteService.list(null, List.of(), documentId, 50);

        verify(geschichteRepository).findAll(any(Specification.class), any(Sort.class));
    }

    @Test
    void list_caps_limit_at_max_via_pageable_when_caller_passes_huge_value() {
        authenticateAs(reader, Permission.READ_ALL);
        when(geschichteRepository.findAll(any(Specification.class), any(Sort.class)))
                .thenReturn(List.of(published(UUID.randomUUID())));

        // 9999 should be clamped — service trims to MAX_LIMIT (200) before/after the query
        List<Geschichte> out = geschichteService.list(null, List.of(), null, 9999);

        assertThat(out).hasSizeLessThanOrEqualTo(200);
    }

    // ─── create ──────────────────────────────────────────────────────────────

    @Test
    void create_sets_status_to_DRAFT_by_default() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(userService.findByEmail(writer.getEmail())).thenReturn(writer);
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("My Story");
        dto.setBody("<p>plain text</p>");

        Geschichte saved = geschichteService.create(dto);

        assertThat(saved.getStatus()).isEqualTo(GeschichteStatus.DRAFT);
        assertThat(saved.getPublishedAt()).isNull();
        assertThat(saved.getAuthor()).isSameAs(writer);
    }

    @Test
    void create_sanitizes_body_HTML_dropping_disallowed_tags() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(userService.findByEmail(writer.getEmail())).thenReturn(writer);
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("XSS attempt");
        dto.setBody("<p>safe</p><script>alert(1)</script><img src=x onerror=alert(2)>");

        Geschichte saved = geschichteService.create(dto);

        assertThat(saved.getBody())
                .contains("<p>safe</p>")
                .doesNotContain("<script>")
                .doesNotContain("onerror")
                .doesNotContain("<img");
    }

    @Test
    void create_keeps_allowed_tags_strong_em_h2_h3_ul_ol_li() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(userService.findByEmail(writer.getEmail())).thenReturn(writer);
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("Rich");
        dto.setBody("<h2>Heading</h2><p>Some <strong>bold</strong> and <em>italic</em>.</p>"
                + "<ul><li>one</li></ul><ol><li>first</li></ol>");

        Geschichte saved = geschichteService.create(dto);

        assertThat(saved.getBody())
                .contains("<h2>Heading</h2>")
                .contains("<strong>bold</strong>")
                .contains("<em>italic</em>")
                .contains("<ul>")
                .contains("<ol>")
                .contains("<li>one</li>");
    }

    @Test
    void create_resolves_personIds_via_PersonService() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(userService.findByEmail(writer.getEmail())).thenReturn(writer);
        UUID personId = UUID.randomUUID();
        Person person = Person.builder().id(personId).build();
        when(personService.getAllById(List.of(personId))).thenReturn(List.of(person));
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("Linked");
        dto.setPersonIds(List.of(personId));

        Geschichte saved = geschichteService.create(dto);

        assertThat(saved.getPersons()).containsExactly(person);
    }

    @Test
    void create_resolves_documentIds_via_DocumentService() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        when(userService.findByEmail(writer.getEmail())).thenReturn(writer);
        UUID docId = UUID.randomUUID();
        Document doc = Document.builder().id(docId).build();
        when(documentService.getDocumentById(docId)).thenReturn(doc);
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("Linked doc");
        dto.setDocumentIds(List.of(docId));

        Geschichte saved = geschichteService.create(dto);

        assertThat(saved.getDocuments()).containsExactly(doc);
    }

    @Test
    void create_throws_BAD_REQUEST_when_title_blank() {
        authenticateAs(writer, Permission.BLOG_WRITE);

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setTitle("   ");
        dto.setBody("<p>x</p>");

        assertThatThrownBy(() -> geschichteService.create(dto))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.VALIDATION_ERROR);
    }

    // ─── update ──────────────────────────────────────────────────────────────

    @Test
    void update_sets_publishedAt_when_status_transitions_to_PUBLISHED() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        Geschichte existing = draft(id);
        existing.setPublishedAt(null);
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(existing));
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setStatus(GeschichteStatus.PUBLISHED);

        Geschichte saved = geschichteService.update(id, dto);

        assertThat(saved.getStatus()).isEqualTo(GeschichteStatus.PUBLISHED);
        assertThat(saved.getPublishedAt()).isNotNull();
    }

    @Test
    void update_clears_publishedAt_when_status_transitions_back_to_DRAFT() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        Geschichte existing = published(id);
        existing.setPublishedAt(LocalDateTime.now().minusDays(1));
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(existing));
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setStatus(GeschichteStatus.DRAFT);

        Geschichte saved = geschichteService.update(id, dto);

        assertThat(saved.getStatus()).isEqualTo(GeschichteStatus.DRAFT);
        assertThat(saved.getPublishedAt()).isNull();
    }

    @Test
    void update_sanitizes_body_on_save() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        when(geschichteRepository.findById(id)).thenReturn(Optional.of(draft(id)));
        when(geschichteRepository.save(any(Geschichte.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
        dto.setBody("<p>ok</p><script>alert(1)</script>");

        Geschichte saved = geschichteService.update(id, dto);

        assertThat(saved.getBody()).doesNotContain("<script>").contains("<p>ok</p>");
    }

    @Test
    void update_throws_NOT_FOUND_when_geschichte_unknown() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        when(geschichteRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> geschichteService.update(id, new GeschichteUpdateDTO()))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.GESCHICHTE_NOT_FOUND);
    }

    // ─── delete ──────────────────────────────────────────────────────────────

    @Test
    void delete_calls_repository_deleteById() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        when(geschichteRepository.existsById(id)).thenReturn(true);

        geschichteService.delete(id);

        verify(geschichteRepository).deleteById(id);
    }

    @Test
    void delete_throws_NOT_FOUND_when_unknown() {
        authenticateAs(writer, Permission.BLOG_WRITE);
        UUID id = UUID.randomUUID();
        when(geschichteRepository.existsById(id)).thenReturn(false);

        assertThatThrownBy(() -> geschichteService.delete(id))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.GESCHICHTE_NOT_FOUND);
        verify(geschichteRepository, never()).deleteById(any());
    }

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

    private void authenticateAs(AppUser user, Permission... permissions) {
        var authorities = List.of(permissions).stream()
                .map(p -> new SimpleGrantedAuthority(p.name()))
                .collect(Collectors.toList());
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(user.getEmail(), null, authorities));
    }

    private Geschichte draft(UUID id) {
        return Geschichte.builder()
                .id(id)
                .title("Draft")
                .body("<p>body</p>")
                .status(GeschichteStatus.DRAFT)
                .persons(new HashSet<>())
                .documents(new HashSet<>())
                .build();
    }

    private Geschichte published(UUID id) {
        return Geschichte.builder()
                .id(id)
                .title("Published")
                .body("<p>body</p>")
                .status(GeschichteStatus.PUBLISHED)
                .publishedAt(LocalDateTime.now().minusHours(1))
                .persons(new HashSet<>())
                .documents(new HashSet<>())
                .build();
    }
}
