package com.storycove.service;

import com.storycove.entity.Author;
import com.storycove.entity.Story;
import com.storycove.repository.AuthorRepository;
import com.storycove.service.exception.DuplicateResourceException;
import com.storycove.service.exception.ResourceNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.times;

@ExtendWith(MockitoExtension.class)
@DisplayName("Author Service Unit Tests")
class AuthorServiceTest {

    @Mock
    private AuthorRepository authorRepository;

    private AuthorService authorService;

    private Author testAuthor;
    private UUID testId;

    @BeforeEach
    void setUp() {
        testId = UUID.randomUUID();
        testAuthor = new Author("Test Author");
        testAuthor.setId(testId);
        testAuthor.setNotes("Test notes");
        
        // Initialize service with mock SearchServiceAdapter
        SearchServiceAdapter mockSearchServiceAdapter = mock(SearchServiceAdapter.class);
        authorService = new AuthorService(authorRepository, mockSearchServiceAdapter);
    }

    @Test
    @DisplayName("Should find all authors")
    void shouldFindAllAuthors() {
        List<Author> authors = List.of(testAuthor, new Author("Another Author"));
        when(authorRepository.findAll()).thenReturn(authors);

        List<Author> result = authorService.findAll();

        assertEquals(2, result.size());
        verify(authorRepository).findAll();
    }

    @Test
    @DisplayName("Should find all authors with pagination")
    void shouldFindAllAuthorsWithPagination() {
        Pageable pageable = PageRequest.of(0, 10);
        Page<Author> page = new PageImpl<>(List.of(testAuthor));
        when(authorRepository.findAll(pageable)).thenReturn(page);

        Page<Author> result = authorService.findAll(pageable);

        assertEquals(1, result.getContent().size());
        verify(authorRepository).findAll(pageable);
    }

    @Test
    @DisplayName("Should find author by ID")
    void shouldFindAuthorById() {
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));

        Author result = authorService.findById(testId);

        assertEquals(testAuthor.getName(), result.getName());
        verify(authorRepository).findById(testId);
    }

    @Test
    @DisplayName("Should throw exception when author not found by ID")
    void shouldThrowExceptionWhenAuthorNotFoundById() {
        when(authorRepository.findById(testId)).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, () -> authorService.findById(testId));
        verify(authorRepository).findById(testId);
    }

    @Test
    @DisplayName("Should find author by name")
    void shouldFindAuthorByName() {
        when(authorRepository.findByName("Test Author")).thenReturn(Optional.of(testAuthor));

        Author result = authorService.findByName("Test Author");

        assertEquals(testAuthor.getName(), result.getName());
        verify(authorRepository).findByName("Test Author");
    }

    @Test
    @DisplayName("Should throw exception when author not found by name")
    void shouldThrowExceptionWhenAuthorNotFoundByName() {
        when(authorRepository.findByName("Unknown Author")).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, () -> authorService.findByName("Unknown Author"));
        verify(authorRepository).findByName("Unknown Author");
    }

    @Test
    @DisplayName("Should search authors by name")
    void shouldSearchAuthorsByName() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findByNameContainingIgnoreCase("test")).thenReturn(authors);

        List<Author> result = authorService.searchByName("test");

        assertEquals(1, result.size());
        assertEquals(testAuthor.getName(), result.get(0).getName());
        verify(authorRepository).findByNameContainingIgnoreCase("test");
    }

    @Test
    @DisplayName("Should check if author exists by name")
    void shouldCheckIfAuthorExistsByName() {
        when(authorRepository.existsByName("Test Author")).thenReturn(true);
        when(authorRepository.existsByName("Unknown Author")).thenReturn(false);

        assertTrue(authorService.existsByName("Test Author"));
        assertFalse(authorService.existsByName("Unknown Author"));

        verify(authorRepository).existsByName("Test Author");
        verify(authorRepository).existsByName("Unknown Author");
    }

    @Test
    @DisplayName("Should create new author")
    void shouldCreateNewAuthor() {
        Author newAuthor = new Author("New Author");
        when(authorRepository.existsByName("New Author")).thenReturn(false);
        when(authorRepository.save(any(Author.class))).thenReturn(newAuthor);

        Author result = authorService.create(newAuthor);

        assertEquals("New Author", result.getName());
        verify(authorRepository).existsByName("New Author");
        verify(authorRepository).save(newAuthor);
    }

    @Test
    @DisplayName("Should throw exception when creating duplicate author")
    void shouldThrowExceptionWhenCreatingDuplicateAuthor() {
        Author duplicateAuthor = new Author("Test Author");
        when(authorRepository.existsByName("Test Author")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> authorService.create(duplicateAuthor));
        verify(authorRepository).existsByName("Test Author");
        verify(authorRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should update existing author")
    void shouldUpdateExistingAuthor() {
        Author updates = new Author("Updated Author");
        updates.setNotes("Updated notes");
        
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.existsByName("Updated Author")).thenReturn(false);
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        authorService.update(testId, updates);

        assertEquals("Updated Author", testAuthor.getName());
        assertEquals("Updated notes", testAuthor.getNotes());
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should throw exception when updating to duplicate name")
    void shouldThrowExceptionWhenUpdatingToDuplicateName() {
        Author updates = new Author("Existing Author");
        
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.existsByName("Existing Author")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> authorService.update(testId, updates));
        verify(authorRepository).findById(testId);
        verify(authorRepository).existsByName("Existing Author");
        verify(authorRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should delete author without stories")
    void shouldDeleteAuthorWithoutStories() {
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));

        authorService.delete(testId);

        verify(authorRepository).findById(testId);
        verify(authorRepository).delete(testAuthor);
    }

    @Test
    @DisplayName("Should throw exception when deleting author with stories")
    void shouldThrowExceptionWhenDeletingAuthorWithStories() {
        testAuthor.getStories().add(new com.storycove.entity.Story("Test Story"));
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));

        assertThrows(IllegalStateException.class, () -> authorService.delete(testId));
        verify(authorRepository).findById(testId);
        verify(authorRepository, never()).delete(any());
    }

    @Test
    @DisplayName("Should add URL to author")
    void shouldAddUrlToAuthor() {
        String url = "https://example.com/author";
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        Author result = authorService.addUrl(testId, url);

        assertTrue(result.getUrls().contains(url));
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should remove URL from author")
    void shouldRemoveUrlFromAuthor() {
        String url = "https://example.com/author";
        testAuthor.addUrl(url);
        
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        Author result = authorService.removeUrl(testId, url);

        assertFalse(result.getUrls().contains(url));
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should set direct author rating")
    void shouldSetDirectAuthorRating() {
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        Author result = authorService.setDirectRating(testId, 5);

        assertEquals(5, result.getAuthorRating());
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should throw exception for invalid direct rating")
    void shouldThrowExceptionForInvalidDirectRating() {
        assertThrows(IllegalArgumentException.class, () -> authorService.setDirectRating(testId, -1));
        assertThrows(IllegalArgumentException.class, () -> authorService.setDirectRating(testId, 6));
        
        verify(authorRepository, never()).findById(any());
        verify(authorRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should set author avatar")
    void shouldSetAuthorAvatar() {
        String avatarPath = "/images/avatar.jpg";
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        Author result = authorService.setAvatar(testId, avatarPath);

        assertEquals(avatarPath, result.getAvatarImagePath());
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should remove author avatar")
    void shouldRemoveAuthorAvatar() {
        testAuthor.setAvatarImagePath("/images/old-avatar.jpg");
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        Author result = authorService.removeAvatar(testId);

        assertNull(result.getAvatarImagePath());
        verify(authorRepository).findById(testId);
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should count recent authors")
    void shouldCountRecentAuthors() {
        when(authorRepository.countRecentAuthors(any(java.time.LocalDateTime.class))).thenReturn(5L);

        long count = authorService.countRecentAuthors(7);

        assertEquals(5L, count);
        verify(authorRepository).countRecentAuthors(any(java.time.LocalDateTime.class));
    }

    @Test
    @DisplayName("Should set author rating with validation")
    void shouldSetAuthorRating() {
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        authorService.setRating(testId, 4);

        assertEquals(4, testAuthor.getAuthorRating());
        verify(authorRepository, times(2)).findById(testId); // Called twice: once initially, once after flush
        verify(authorRepository).save(testAuthor);
        verify(authorRepository).flush();
    }

    @Test
    @DisplayName("Should throw exception for invalid rating range")
    void shouldThrowExceptionForInvalidRating() {
        assertThrows(IllegalArgumentException.class, () -> authorService.setRating(testId, 0));
        assertThrows(IllegalArgumentException.class, () -> authorService.setRating(testId, 6));
        
        verify(authorRepository, never()).findById(any());
        verify(authorRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should handle null rating")
    void shouldHandleNullRating() {
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
        when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);

        authorService.setRating(testId, null);

        assertNull(testAuthor.getAuthorRating());
        verify(authorRepository, times(2)).findById(testId); // Called twice: once initially, once after flush
        verify(authorRepository).save(testAuthor);
    }

    @Test
    @DisplayName("Should find all authors with stories")
    void shouldFindAllAuthorsWithStories() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findAll()).thenReturn(authors);

        List<Author> result = authorService.findAllWithStories();

        assertEquals(1, result.size());
        verify(authorRepository).findAll();
    }

    @Test
    @DisplayName("Should get author rating from database")
    void shouldGetAuthorRatingFromDb() {
        when(authorRepository.findAuthorRatingById(testId)).thenReturn(4);

        Integer rating = authorService.getAuthorRatingFromDb(testId);

        assertEquals(4, rating);
        verify(authorRepository).findAuthorRatingById(testId);
    }

    @Test
    @DisplayName("Should calculate average story rating")
    void shouldCalculateAverageStoryRating() {
        // Setup test author with stories
        Story story1 = new Story("Story 1");
        story1.setRating(4);
        Story story2 = new Story("Story 2");
        story2.setRating(5);
        
        testAuthor.getStories().add(story1);
        testAuthor.getStories().add(story2);
        
        when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));

        Double avgRating = authorService.calculateAverageStoryRating(testId);

        assertEquals(4.5, avgRating);
        verify(authorRepository).findById(testId);
    }

    @Test
    @DisplayName("Should find authors with stories using repository method")
    void shouldFindAuthorsWithStoriesFromRepository() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findAuthorsWithStories()).thenReturn(authors);

        List<Author> result = authorService.findAuthorsWithStories();

        assertEquals(1, result.size());
        verify(authorRepository).findAuthorsWithStories();
    }

    @Test
    @DisplayName("Should find top rated authors")
    void shouldFindTopRatedAuthors() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findTopRatedAuthors()).thenReturn(authors);

        List<Author> result = authorService.findTopRatedAuthors();

        assertEquals(1, result.size());
        verify(authorRepository).findTopRatedAuthors();
    }

    @Test
    @DisplayName("Should find most prolific authors")
    void shouldFindMostProlificAuthors() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findMostProlificAuthors()).thenReturn(authors);

        List<Author> result = authorService.findMostProlificAuthors();

        assertEquals(1, result.size());
        verify(authorRepository).findMostProlificAuthors();
    }

    @Test
    @DisplayName("Should find authors by URL domain")
    void shouldFindAuthorsByUrlDomain() {
        List<Author> authors = List.of(testAuthor);
        when(authorRepository.findByUrlDomain("example.com")).thenReturn(authors);

        List<Author> result = authorService.findByUrlDomain("example.com");

        assertEquals(1, result.size());
        verify(authorRepository).findByUrlDomain("example.com");
    }

}