package com.storycove.service;

import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import com.storycove.entity.TagAlias;
import com.storycove.repository.TagAliasRepository;
import com.storycove.repository.TagRepository;
import com.storycove.service.exception.DuplicateResourceException;
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 TagServiceTest {

    @Mock
    private TagRepository tagRepository;

    @Mock
    private TagAliasRepository tagAliasRepository;

    @InjectMocks
    private TagService tagService;

    private Tag testTag;
    private UUID tagId;

    @BeforeEach
    void setUp() {
        tagId = UUID.randomUUID();
        testTag = new Tag();
        testTag.setId(tagId);
        testTag.setName("fantasy");
        testTag.setStories(new HashSet<>());
    }

    // ========================================
    // Basic CRUD Tests
    // ========================================

    @Test
    @DisplayName("Should find tag by ID")
    void testFindById() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));

        Tag result = tagService.findById(tagId);

        assertNotNull(result);
        assertEquals(tagId, result.getId());
        assertEquals("fantasy", result.getName());
    }

    @Test
    @DisplayName("Should throw exception when tag not found by ID")
    void testFindByIdNotFound() {
        when(tagRepository.findById(any())).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, () -> {
            tagService.findById(UUID.randomUUID());
        });
    }

    @Test
    @DisplayName("Should find tag by name")
    void testFindByName() {
        when(tagRepository.findByName("fantasy")).thenReturn(Optional.of(testTag));

        Tag result = tagService.findByName("fantasy");

        assertNotNull(result);
        assertEquals("fantasy", result.getName());
    }

    @Test
    @DisplayName("Should create new tag")
    void testCreateTag() {
        when(tagRepository.existsByName("fantasy")).thenReturn(false);
        when(tagRepository.save(any(Tag.class))).thenReturn(testTag);

        Tag result = tagService.create(testTag);

        assertNotNull(result);
        verify(tagRepository).save(testTag);
    }

    @Test
    @DisplayName("Should throw exception when creating duplicate tag")
    void testCreateDuplicateTag() {
        when(tagRepository.existsByName("fantasy")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> {
            tagService.create(testTag);
        });

        verify(tagRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should update existing tag")
    void testUpdateTag() {
        Tag updates = new Tag();
        updates.setName("sci-fi");

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagRepository.existsByName("sci-fi")).thenReturn(false);
        when(tagRepository.save(any(Tag.class))).thenReturn(testTag);

        Tag result = tagService.update(tagId, updates);

        assertNotNull(result);
        verify(tagRepository).save(testTag);
    }

    @Test
    @DisplayName("Should throw exception when updating to duplicate name")
    void testUpdateToDuplicateName() {
        Tag updates = new Tag();
        updates.setName("sci-fi");

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagRepository.existsByName("sci-fi")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> {
            tagService.update(tagId, updates);
        });
    }

    @Test
    @DisplayName("Should delete unused tag")
    void testDeleteUnusedTag() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        doNothing().when(tagRepository).delete(testTag);

        tagService.delete(tagId);

        verify(tagRepository).delete(testTag);
    }

    @Test
    @DisplayName("Should throw exception when deleting tag in use")
    void testDeleteTagInUse() {
        Story story = new Story();
        testTag.getStories().add(story);

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));

        assertThrows(IllegalStateException.class, () -> {
            tagService.delete(tagId);
        });

        verify(tagRepository, never()).delete(any());
    }

    // ========================================
    // Tag Alias Tests
    // ========================================

    @Test
    @DisplayName("Should add alias to tag")
    void testAddAlias() {
        TagAlias alias = new TagAlias();
        alias.setAliasName("sci-fantasy");
        alias.setCanonicalTag(testTag);

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagAliasRepository.existsByAliasNameIgnoreCase("sci-fantasy")).thenReturn(false);
        when(tagRepository.existsByNameIgnoreCase("sci-fantasy")).thenReturn(false);
        when(tagAliasRepository.save(any(TagAlias.class))).thenReturn(alias);

        TagAlias result = tagService.addAlias(tagId, "sci-fantasy");

        assertNotNull(result);
        assertEquals("sci-fantasy", result.getAliasName());
        verify(tagAliasRepository).save(any(TagAlias.class));
    }

    @Test
    @DisplayName("Should throw exception when alias already exists")
    void testAddDuplicateAlias() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagAliasRepository.existsByAliasNameIgnoreCase("sci-fantasy")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> {
            tagService.addAlias(tagId, "sci-fantasy");
        });

        verify(tagAliasRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should throw exception when alias conflicts with tag name")
    void testAddAliasConflictsWithTagName() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagAliasRepository.existsByAliasNameIgnoreCase("sci-fi")).thenReturn(false);
        when(tagRepository.existsByNameIgnoreCase("sci-fi")).thenReturn(true);

        assertThrows(DuplicateResourceException.class, () -> {
            tagService.addAlias(tagId, "sci-fi");
        });
    }

    @Test
    @DisplayName("Should remove alias from tag")
    void testRemoveAlias() {
        UUID aliasId = UUID.randomUUID();
        TagAlias alias = new TagAlias();
        alias.setId(aliasId);
        alias.setCanonicalTag(testTag);

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagAliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));
        doNothing().when(tagAliasRepository).delete(alias);

        tagService.removeAlias(tagId, aliasId);

        verify(tagAliasRepository).delete(alias);
    }

    @Test
    @DisplayName("Should throw exception when removing alias from wrong tag")
    void testRemoveAliasFromWrongTag() {
        UUID aliasId = UUID.randomUUID();
        Tag differentTag = new Tag();
        differentTag.setId(UUID.randomUUID());

        TagAlias alias = new TagAlias();
        alias.setId(aliasId);
        alias.setCanonicalTag(differentTag);

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagAliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));

        assertThrows(IllegalArgumentException.class, () -> {
            tagService.removeAlias(tagId, aliasId);
        });

        verify(tagAliasRepository, never()).delete(any());
    }

    @Test
    @DisplayName("Should resolve tag by name")
    void testResolveTagByName() {
        when(tagRepository.findByNameIgnoreCase("fantasy")).thenReturn(Optional.of(testTag));

        Tag result = tagService.resolveTagByName("fantasy");

        assertNotNull(result);
        assertEquals("fantasy", result.getName());
    }

    @Test
    @DisplayName("Should resolve tag by alias")
    void testResolveTagByAlias() {
        TagAlias alias = new TagAlias();
        alias.setAliasName("sci-fantasy");
        alias.setCanonicalTag(testTag);

        when(tagRepository.findByNameIgnoreCase("sci-fantasy")).thenReturn(Optional.empty());
        when(tagAliasRepository.findByAliasNameIgnoreCase("sci-fantasy")).thenReturn(Optional.of(alias));

        Tag result = tagService.resolveTagByName("sci-fantasy");

        assertNotNull(result);
        assertEquals("fantasy", result.getName());
    }

    @Test
    @DisplayName("Should return null when tag/alias not found")
    void testResolveTagNotFound() {
        when(tagRepository.findByNameIgnoreCase(anyString())).thenReturn(Optional.empty());
        when(tagAliasRepository.findByAliasNameIgnoreCase(anyString())).thenReturn(Optional.empty());

        Tag result = tagService.resolveTagByName("nonexistent");

        assertNull(result);
    }

    // ========================================
    // Tag Merge Tests
    // ========================================

    @Test
    @DisplayName("Should merge tags successfully")
    void testMergeTags() {
        UUID sourceId = UUID.randomUUID();
        Tag sourceTag = new Tag();
        sourceTag.setId(sourceId);
        sourceTag.setName("sci-fi");

        Story story = new Story();
        story.setTags(new HashSet<>(Arrays.asList(sourceTag)));
        sourceTag.setStories(new HashSet<>(Arrays.asList(story)));

        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));
        when(tagRepository.findById(sourceId)).thenReturn(Optional.of(sourceTag));
        when(tagAliasRepository.save(any(TagAlias.class))).thenReturn(new TagAlias());
        when(tagRepository.save(any(Tag.class))).thenReturn(testTag);
        doNothing().when(tagRepository).delete(sourceTag);

        Tag result = tagService.mergeTags(List.of(sourceId), tagId);

        assertNotNull(result);
        verify(tagAliasRepository).save(any(TagAlias.class));
        verify(tagRepository).delete(sourceTag);
    }

    @Test
    @DisplayName("Should not merge tag with itself")
    void testMergeTagWithItself() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));

        assertThrows(IllegalArgumentException.class, () -> {
            tagService.mergeTags(List.of(tagId), tagId);
        });
    }

    @Test
    @DisplayName("Should throw exception when no valid source tags to merge")
    void testMergeNoValidSourceTags() {
        when(tagRepository.findById(tagId)).thenReturn(Optional.of(testTag));

        assertThrows(IllegalArgumentException.class, () -> {
            tagService.mergeTags(Collections.emptyList(), tagId);
        });
    }

    // ========================================
    // Search and Query Tests
    // ========================================

    @Test
    @DisplayName("Should find all tags")
    void testFindAll() {
        when(tagRepository.findAll()).thenReturn(List.of(testTag));

        List<Tag> result = tagService.findAll();

        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    @DisplayName("Should search tags by name")
    void testSearchByName() {
        when(tagRepository.findByNameContainingIgnoreCase("fan"))
            .thenReturn(List.of(testTag));

        List<Tag> result = tagService.searchByName("fan");

        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    @DisplayName("Should find used tags")
    void testFindUsedTags() {
        when(tagRepository.findUsedTags()).thenReturn(List.of(testTag));

        List<Tag> result = tagService.findUsedTags();

        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    @DisplayName("Should find most used tags")
    void testFindMostUsedTags() {
        when(tagRepository.findMostUsedTags()).thenReturn(List.of(testTag));

        List<Tag> result = tagService.findMostUsedTags();

        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    @DisplayName("Should find unused tags")
    void testFindUnusedTags() {
        when(tagRepository.findUnusedTags()).thenReturn(List.of(testTag));

        List<Tag> result = tagService.findUnusedTags();

        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    @DisplayName("Should delete all unused tags")
    void testDeleteUnusedTags() {
        when(tagRepository.findUnusedTags()).thenReturn(List.of(testTag));
        doNothing().when(tagRepository).deleteAll(anyList());

        List<Tag> result = tagService.deleteUnusedTags();

        assertNotNull(result);
        assertEquals(1, result.size());
        verify(tagRepository).deleteAll(anyList());
    }

    @Test
    @DisplayName("Should find or create tag")
    void testFindOrCreate() {
        when(tagRepository.findByName("fantasy")).thenReturn(Optional.of(testTag));

        Tag result = tagService.findOrCreate("fantasy");

        assertNotNull(result);
        assertEquals("fantasy", result.getName());
        verify(tagRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should create tag when not found")
    void testFindOrCreateNew() {
        when(tagRepository.findByName("new-tag")).thenReturn(Optional.empty());
        when(tagRepository.existsByName("new-tag")).thenReturn(false);
        when(tagRepository.save(any(Tag.class))).thenReturn(testTag);

        Tag result = tagService.findOrCreate("new-tag");

        assertNotNull(result);
        verify(tagRepository).save(any(Tag.class));
    }

    // ========================================
    // Tag Suggestion Tests
    // ========================================

    @Test
    @DisplayName("Should suggest tags based on content")
    void testSuggestTags() {
        when(tagRepository.findAll()).thenReturn(List.of(testTag));

        var suggestions = tagService.suggestTags(
            "Fantasy Adventure",
            "A fantasy story about magic",
            "Epic fantasy tale",
            5
        );

        assertNotNull(suggestions);
        assertFalse(suggestions.isEmpty());
    }

    @Test
    @DisplayName("Should return empty suggestions for empty content")
    void testSuggestTagsEmptyContent() {
        when(tagRepository.findAll()).thenReturn(List.of(testTag));

        var suggestions = tagService.suggestTags("", "", "", 5);

        assertNotNull(suggestions);
        assertTrue(suggestions.isEmpty());
    }

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

    @Test
    @DisplayName("Should count all tags")
    void testCountAll() {
        when(tagRepository.count()).thenReturn(10L);

        long count = tagService.countAll();

        assertEquals(10L, count);
    }

    @Test
    @DisplayName("Should count used tags")
    void testCountUsedTags() {
        when(tagRepository.countUsedTags()).thenReturn(5L);

        long count = tagService.countUsedTags();

        assertEquals(5L, count);
    }
}
