package com.storycove.service;

import com.storycove.dto.CollectionDto;
import com.storycove.dto.SearchResultDto;
import com.storycove.entity.Collection;
import com.storycove.entity.CollectionStory;
import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import com.storycove.repository.CollectionRepository;
import com.storycove.repository.CollectionStoryRepository;
import com.storycove.repository.StoryRepository;
import com.storycove.repository.TagRepository;
import com.storycove.service.exception.ResourceNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.*;

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

@ExtendWith(MockitoExtension.class)
class CollectionServiceTest {

    @Mock
    private CollectionRepository collectionRepository;

    @Mock
    private CollectionStoryRepository collectionStoryRepository;

    @Mock
    private StoryRepository storyRepository;

    @Mock
    private TagRepository tagRepository;

    @Mock
    private SearchServiceAdapter searchServiceAdapter;

    @Mock
    private ReadingTimeService readingTimeService;

    @InjectMocks
    private CollectionService collectionService;

    private Collection testCollection;
    private Story testStory;
    private Tag testTag;
    private UUID collectionId;
    private UUID storyId;

    @BeforeEach
    void setUp() {
        collectionId = UUID.randomUUID();
        storyId = UUID.randomUUID();

        testCollection = new Collection();
        testCollection.setId(collectionId);
        testCollection.setName("Test Collection");
        testCollection.setDescription("Test Description");
        testCollection.setIsArchived(false);

        testStory = new Story();
        testStory.setId(storyId);
        testStory.setTitle("Test Story");
        testStory.setWordCount(1000);

        testTag = new Tag();
        testTag.setId(UUID.randomUUID());
        testTag.setName("test-tag");
    }

    // ========================================
    // Search Tests
    // ========================================

    @Test
    @DisplayName("Should search collections using SearchServiceAdapter")
    void testSearchCollections() {
        // Arrange
        CollectionDto dto = new CollectionDto();
        dto.setId(collectionId);
        dto.setName("Test Collection");

        SearchResultDto<CollectionDto> searchResult = new SearchResultDto<>(
            List.of(dto), 1, 0, 10, "test", 100L
        );

        when(searchServiceAdapter.searchCollections(anyString(), anyList(), anyBoolean(), anyInt(), anyInt()))
            .thenReturn(searchResult);
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));

        // Act
        SearchResultDto<Collection> result = collectionService.searchCollections("test", null, false, 0, 10);

        // Assert
        assertNotNull(result);
        assertEquals(1, result.getTotalHits());
        assertEquals(1, result.getResults().size());
        assertEquals(collectionId, result.getResults().get(0).getId());
        verify(searchServiceAdapter).searchCollections("test", null, false, 0, 10);
    }

    @Test
    @DisplayName("Should handle search with tag filters")
    void testSearchCollectionsWithTags() {
        // Arrange
        List<String> tags = List.of("fantasy", "adventure");
        CollectionDto dto = new CollectionDto();
        dto.setId(collectionId);

        SearchResultDto<CollectionDto> searchResult = new SearchResultDto<>(
            List.of(dto), 1, 0, 10, "test", 50L
        );

        when(searchServiceAdapter.searchCollections(anyString(), eq(tags), anyBoolean(), anyInt(), anyInt()))
            .thenReturn(searchResult);
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));

        // Act
        SearchResultDto<Collection> result = collectionService.searchCollections("test", tags, false, 0, 10);

        // Assert
        assertEquals(1, result.getResults().size());
        verify(searchServiceAdapter).searchCollections("test", tags, false, 0, 10);
    }

    @Test
    @DisplayName("Should return empty results when search fails")
    void testSearchCollectionsFailure() {
        // Arrange
        when(searchServiceAdapter.searchCollections(anyString(), anyList(), anyBoolean(), anyInt(), anyInt()))
            .thenThrow(new RuntimeException("Search failed"));

        // Act
        SearchResultDto<Collection> result = collectionService.searchCollections("test", null, false, 0, 10);

        // Assert
        assertNotNull(result);
        assertEquals(0, result.getTotalHits());
        assertTrue(result.getResults().isEmpty());
    }

    // ========================================
    // CRUD Operations Tests
    // ========================================

    @Test
    @DisplayName("Should find collection by ID")
    void testFindById() {
        // Arrange
        when(collectionRepository.findByIdWithStoriesAndTags(collectionId))
            .thenReturn(Optional.of(testCollection));

        // Act
        Collection result = collectionService.findById(collectionId);

        // Assert
        assertNotNull(result);
        assertEquals(collectionId, result.getId());
        assertEquals("Test Collection", result.getName());
    }

    @Test
    @DisplayName("Should throw exception when collection not found")
    void testFindByIdNotFound() {
        // Arrange
        when(collectionRepository.findByIdWithStoriesAndTags(any()))
            .thenReturn(Optional.empty());

        // Act & Assert
        assertThrows(ResourceNotFoundException.class, () -> {
            collectionService.findById(UUID.randomUUID());
        });
    }

    @Test
    @DisplayName("Should create collection with tags")
    void testCreateCollection() {
        // Arrange
        List<String> tagNames = List.of("fantasy", "adventure");
        when(tagRepository.findByName("fantasy")).thenReturn(Optional.of(testTag));
        when(tagRepository.findByName("adventure")).thenReturn(Optional.empty());
        when(tagRepository.save(any(Tag.class))).thenReturn(testTag);
        when(collectionRepository.save(any(Collection.class))).thenReturn(testCollection);

        // Act
        Collection result = collectionService.createCollection("New Collection", "Description", tagNames, null);

        // Assert
        assertNotNull(result);
        verify(collectionRepository).save(any(Collection.class));
        verify(tagRepository, times(2)).findByName(anyString());
    }

    @Test
    @DisplayName("Should create collection with initial stories")
    void testCreateCollectionWithStories() {
        // Arrange
        List<UUID> storyIds = List.of(storyId);
        when(collectionRepository.save(any(Collection.class))).thenReturn(testCollection);
        when(storyRepository.findAllById(storyIds)).thenReturn(List.of(testStory));
        when(collectionStoryRepository.existsByCollectionIdAndStoryId(any(), any())).thenReturn(false);
        when(collectionStoryRepository.getNextPosition(any())).thenReturn(1000);
        when(collectionStoryRepository.save(any())).thenReturn(new CollectionStory());
        when(collectionRepository.findByIdWithStoriesAndTags(any()))
            .thenReturn(Optional.of(testCollection));

        // Act
        Collection result = collectionService.createCollection("New Collection", "Description", null, storyIds);

        // Assert
        assertNotNull(result);
        verify(storyRepository).findAllById(storyIds);
        verify(collectionStoryRepository).save(any(CollectionStory.class));
    }

    @Test
    @DisplayName("Should update collection metadata")
    void testUpdateCollection() {
        // Arrange
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(collectionRepository.save(any(Collection.class)))
            .thenReturn(testCollection);

        // Act
        Collection result = collectionService.updateCollection(
            collectionId, "Updated Name", "Updated Description", null, 5
        );

        // Assert
        assertNotNull(result);
        verify(collectionRepository).save(any(Collection.class));
    }

    @Test
    @DisplayName("Should delete collection")
    void testDeleteCollection() {
        // Arrange
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        doNothing().when(collectionRepository).delete(any(Collection.class));

        // Act
        collectionService.deleteCollection(collectionId);

        // Assert
        verify(collectionRepository).delete(testCollection);
    }

    @Test
    @DisplayName("Should archive collection")
    void testArchiveCollection() {
        // Arrange
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(collectionRepository.save(any(Collection.class)))
            .thenReturn(testCollection);

        // Act
        Collection result = collectionService.archiveCollection(collectionId, true);

        // Assert
        assertNotNull(result);
        verify(collectionRepository).save(any(Collection.class));
    }

    // ========================================
    // Story Management Tests
    // ========================================

    @Test
    @DisplayName("Should add stories to collection")
    void testAddStoriesToCollection() {
        // Arrange
        List<UUID> storyIds = List.of(storyId);
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(storyRepository.findAllById(storyIds))
            .thenReturn(List.of(testStory));
        when(collectionStoryRepository.existsByCollectionIdAndStoryId(collectionId, storyId))
            .thenReturn(false);
        when(collectionStoryRepository.getNextPosition(collectionId))
            .thenReturn(1000);
        when(collectionStoryRepository.save(any()))
            .thenReturn(new CollectionStory());
        when(collectionStoryRepository.countByCollectionId(collectionId))
            .thenReturn(1L);

        // Act
        Map<String, Object> result = collectionService.addStoriesToCollection(collectionId, storyIds, null);

        // Assert
        assertEquals(1, result.get("added"));
        assertEquals(0, result.get("skipped"));
        assertEquals(1L, result.get("totalStories"));
        verify(collectionStoryRepository).save(any(CollectionStory.class));
    }

    @Test
    @DisplayName("Should skip duplicate stories when adding")
    void testAddDuplicateStories() {
        // Arrange
        List<UUID> storyIds = List.of(storyId);
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(storyRepository.findAllById(storyIds))
            .thenReturn(List.of(testStory));
        when(collectionStoryRepository.existsByCollectionIdAndStoryId(collectionId, storyId))
            .thenReturn(true);
        when(collectionStoryRepository.countByCollectionId(collectionId))
            .thenReturn(1L);

        // Act
        Map<String, Object> result = collectionService.addStoriesToCollection(collectionId, storyIds, null);

        // Assert
        assertEquals(0, result.get("added"));
        assertEquals(1, result.get("skipped"));
        verify(collectionStoryRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should throw exception when adding non-existent stories")
    void testAddNonExistentStories() {
        // Arrange
        List<UUID> storyIds = List.of(storyId, UUID.randomUUID());
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(storyRepository.findAllById(storyIds))
            .thenReturn(List.of(testStory)); // Only one story found

        // Act & Assert
        assertThrows(ResourceNotFoundException.class, () -> {
            collectionService.addStoriesToCollection(collectionId, storyIds, null);
        });
    }

    @Test
    @DisplayName("Should remove story from collection")
    void testRemoveStoryFromCollection() {
        // Arrange
        CollectionStory collectionStory = new CollectionStory();
        when(collectionStoryRepository.existsByCollectionIdAndStoryId(collectionId, storyId))
            .thenReturn(true);
        when(collectionStoryRepository.findByCollectionIdAndStoryId(collectionId, storyId))
            .thenReturn(collectionStory);
        doNothing().when(collectionStoryRepository).delete(any());

        // Act
        collectionService.removeStoryFromCollection(collectionId, storyId);

        // Assert
        verify(collectionStoryRepository).delete(collectionStory);
    }

    @Test
    @DisplayName("Should throw exception when removing non-existent story")
    void testRemoveNonExistentStory() {
        // Arrange
        when(collectionStoryRepository.existsByCollectionIdAndStoryId(any(), any()))
            .thenReturn(false);

        // Act & Assert
        assertThrows(ResourceNotFoundException.class, () -> {
            collectionService.removeStoryFromCollection(collectionId, storyId);
        });
    }

    @Test
    @DisplayName("Should reorder stories in collection")
    void testReorderStories() {
        // Arrange
        List<Map<String, Object>> storyOrders = List.of(
            Map.of("storyId", storyId.toString(), "position", 1)
        );
        when(collectionRepository.findById(collectionId))
            .thenReturn(Optional.of(testCollection));
        doNothing().when(collectionStoryRepository).updatePosition(any(), any(), anyInt());

        // Act
        collectionService.reorderStories(collectionId, storyOrders);

        // Assert
        verify(collectionStoryRepository, times(2)).updatePosition(any(), any(), anyInt());
    }

    // ========================================
    // Statistics Tests
    // ========================================

    @Test
    @DisplayName("Should get collection statistics")
    void testGetCollectionStatistics() {
        // Arrange
        testStory.setWordCount(1000);
        testStory.setRating(5);

        CollectionStory cs = new CollectionStory();
        cs.setStory(testStory);
        testCollection.setCollectionStories(List.of(cs));

        when(collectionRepository.findByIdWithStoriesAndTags(collectionId))
            .thenReturn(Optional.of(testCollection));
        when(readingTimeService.calculateReadingTime(1000))
            .thenReturn(5);

        // Act
        Map<String, Object> stats = collectionService.getCollectionStatistics(collectionId);

        // Assert
        assertNotNull(stats);
        assertEquals(1, stats.get("totalStories"));
        assertEquals(1000, stats.get("totalWordCount"));
        assertEquals(5, stats.get("estimatedReadingTime"));
        assertTrue(stats.containsKey("averageStoryRating"));
    }

    // ========================================
    // Helper Method Tests
    // ========================================

    @Test
    @DisplayName("Should find all collections with tags for indexing")
    void testFindAllWithTags() {
        // Arrange
        when(collectionRepository.findAllWithTags())
            .thenReturn(List.of(testCollection));

        // Act
        List<Collection> result = collectionService.findAllWithTags();

        // Assert
        assertNotNull(result);
        assertEquals(1, result.size());
        verify(collectionRepository).findAllWithTags();
    }

    @Test
    @DisplayName("Should get collections for a specific story")
    void testGetCollectionsForStory() {
        // Arrange
        CollectionStory cs = new CollectionStory();
        cs.setCollection(testCollection);
        when(collectionStoryRepository.findByStoryId(storyId))
            .thenReturn(List.of(cs));

        // Act
        List<Collection> result = collectionService.getCollectionsForStory(storyId);

        // Assert
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(collectionId, result.get(0).getId());
    }
}
