package com.storycove.service;

import com.storycove.entity.Story;
import com.storycove.repository.ReadingPositionRepository;
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.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 java.time.LocalDateTime;
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.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("Story Service Unit Tests - Reading Progress")
class StoryServiceTest {

    @Mock
    private StoryRepository storyRepository;
    
    @Mock
    private TagRepository tagRepository;
    
    @Mock
    private ReadingPositionRepository readingPositionRepository;

    @Mock
    private SearchServiceAdapter searchServiceAdapter;

    private StoryService storyService;
    private Story testStory;
    private UUID testId;

    @BeforeEach
    void setUp() {
        testId = UUID.randomUUID();
        testStory = new Story("Test Story");
        testStory.setId(testId);
        testStory.setContentHtml("<p>Test content for reading progress tracking</p>");
        
        // Create StoryService with mocked dependencies
        storyService = new StoryService(
            storyRepository,
            tagRepository,
            readingPositionRepository,
            null,  // authorService - not needed for reading progress tests
            null,  // tagService - not needed for reading progress tests
            null,  // seriesService - not needed for reading progress tests
            null,  // sanitizationService - not needed for reading progress tests
            searchServiceAdapter
        );
    }

    @Test
    @DisplayName("Should update reading progress successfully")
    void shouldUpdateReadingProgress() {
        Integer position = 150;
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, position);

        assertEquals(position, result.getReadingPosition());
        assertNotNull(result.getLastReadAt());
        verify(storyRepository).findById(testId);
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should update reading progress with zero position")
    void shouldUpdateReadingProgressWithZeroPosition() {
        Integer position = 0;
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, position);

        assertEquals(0, result.getReadingPosition());
        // When position is 0, lastReadAt should be reset to null so the story doesn't appear in "last read" sorting
        assertNull(result.getLastReadAt());
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should throw exception for negative reading position")
    void shouldThrowExceptionForNegativeReadingPosition() {
        Integer position = -1;

        assertThrows(IllegalArgumentException.class, 
            () -> storyService.updateReadingProgress(testId, position));
        
        verify(storyRepository, never()).findById(any());
        verify(storyRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should handle null reading position")
    void shouldHandleNullReadingPosition() {
        Integer position = null;
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, position);

        assertNull(result.getReadingPosition());
        // When position is null, lastReadAt should be reset to null so the story doesn't appear in "last read" sorting
        assertNull(result.getLastReadAt());
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should throw exception when story not found for reading progress update")
    void shouldThrowExceptionWhenStoryNotFoundForReadingProgress() {
        Integer position = 100;
        when(storyRepository.findById(testId)).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, 
            () -> storyService.updateReadingProgress(testId, position));
        
        verify(storyRepository).findById(testId);
        verify(storyRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should mark story as read")
    void shouldMarkStoryAsRead() {
        Boolean isRead = true;
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingStatus(testId, isRead);

        assertTrue(result.getIsRead());
        assertNotNull(result.getLastReadAt());
        assertNotNull(result.getLastCompletedAt());
        // When marked as read, position should be set to content length
        assertTrue(result.getReadingPosition() > 0);
        verify(storyRepository).findById(testId);
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should mark story as unread")
    void shouldMarkStoryAsUnread() {
        Boolean isRead = false;
        // First mark story as read to test transition
        testStory.markAsRead();
        
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingStatus(testId, isRead);

        assertFalse(result.getIsRead());
        assertNotNull(result.getLastReadAt());
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should handle null reading status")
    void shouldHandleNullReadingStatus() {
        Boolean isRead = null;
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingStatus(testId, isRead);

        assertFalse(result.getIsRead());
        assertNotNull(result.getLastReadAt());
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should throw exception when story not found for reading status update")
    void shouldThrowExceptionWhenStoryNotFoundForReadingStatus() {
        Boolean isRead = true;
        when(storyRepository.findById(testId)).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, 
            () -> storyService.updateReadingStatus(testId, isRead));
        
        verify(storyRepository).findById(testId);
        verify(storyRepository, never()).save(any());
    }


    @Test
    @DisplayName("Should update lastReadAt timestamp when updating progress")
    void shouldUpdateLastReadAtWhenUpdatingProgress() {
        Integer position = 50;
        LocalDateTime beforeUpdate = LocalDateTime.now().minusMinutes(1);
        
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, position);

        assertNotNull(result.getLastReadAt());
        assertTrue(result.getLastReadAt().isAfter(beforeUpdate));
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should update lastReadAt timestamp when updating status")
    void shouldUpdateLastReadAtWhenUpdatingStatus() {
        Boolean isRead = true;
        LocalDateTime beforeUpdate = LocalDateTime.now().minusMinutes(1);
        
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingStatus(testId, isRead);

        assertNotNull(result.getLastReadAt());
        assertTrue(result.getLastReadAt().isAfter(beforeUpdate));
        assertNotNull(result.getLastCompletedAt());
        assertTrue(result.getLastCompletedAt().isAfter(beforeUpdate));
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should set lastCompletedAt when resetting progress from non-zero position")
    void shouldSetLastCompletedAtWhenResettingProgress() {
        LocalDateTime beforeUpdate = LocalDateTime.now().minusMinutes(1);
        // Give the story some reading progress first
        testStory.updateReadingProgress(100);

        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, 0);

        assertEquals(0, result.getReadingPosition());
        assertNull(result.getLastReadAt());
        assertNotNull(result.getLastCompletedAt());
        assertTrue(result.getLastCompletedAt().isAfter(beforeUpdate));
        verify(storyRepository).save(testStory);
    }

    @Test
    @DisplayName("Should not set lastCompletedAt when resetting progress that was already zero")
    void shouldNotSetLastCompletedAtWhenAlreadyAtZeroProgress() {
        // testStory starts at position 0, no prior reading
        when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
        when(storyRepository.save(any(Story.class))).thenReturn(testStory);

        Story result = storyService.updateReadingProgress(testId, 0);

        assertNull(result.getLastCompletedAt());
        verify(storyRepository).save(testStory);
    }
}