package kz.konturai.parser.service;

import kz.konturai.parser.exception.FacebookTokenExpiredException;
import kz.konturai.parser.exception.LinkedInTokenExpiredException;
import kz.konturai.parser.exception.TelegramTokenExpiredException;
import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.model.PostingTask;
import kz.konturai.parser.repository.MarketingStrategyRepository;
import kz.konturai.parser.repository.PostingTaskRepository;
import org.junit.jupiter.api.BeforeEach;
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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class PostingTaskServiceTest {

    private static final String TEST_RECIPIENT_ID = "25769470256007187";

    @Mock
    private PostingTaskRepository taskRepository;

    @Mock
    private MarketingStrategyRepository strategyRepository;

    @Mock
    private SocialMediaCredentialsService credentialsService;

    @Mock
    private FacebookPostingService facebookPostingService;

    @Mock
    private LinkedInPostingService linkedInPostingService;

    @Mock
    private TelegramPostingService telegramPostingService;

    @Mock
    private MinIOService minIOService;

    private PostingTaskService postingTaskService;

    @BeforeEach
    void setUp() {
        postingTaskService = spy(new PostingTaskService(
                taskRepository,
                strategyRepository,
                credentialsService,
                facebookPostingService,
                linkedInPostingService,
                telegramPostingService,
                minIOService));
    }

    @Test
    void createTasksFromStrategyShouldThrowWhenStrategyIsMissing() {
        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.empty());

        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> postingTaskService.createTasksFromStrategy("strategy-1"));

        assertEquals("Strategy not found: strategy-1", exception.getMessage());
        verifyNoInteractions(taskRepository, credentialsService);
    }

    @Test
    void createTasksFromStrategyShouldThrowWhenStrategyIsNotCompleted() {
        MarketingStrategy strategy = new MarketingStrategy();
        strategy.setStatus("processing");
        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));

        IllegalStateException exception = assertThrows(
                IllegalStateException.class,
                () -> postingTaskService.createTasksFromStrategy("strategy-1"));

        assertEquals("Strategy is not completed yet. Status: processing", exception.getMessage());
        verifyNoInteractions(credentialsService, taskRepository);
    }

    @Test
    void createTasksFromStrategyShouldReturnEmptyListWhenCalendarIsMissing() {
        MarketingStrategy strategy = new MarketingStrategy();
        strategy.setStatus("completed");
        strategy.setPostCalendar(List.of());
        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));

        List<PostingTask> tasks = postingTaskService.createTasksFromStrategy("strategy-1");

        assertTrue(tasks.isEmpty());
        verifyNoInteractions(credentialsService);
        verify(taskRepository, never()).saveAll(anyList());
    }

    @Test
    void createTasksFromStrategyShouldThrowWhenCredentialsAreMissing() {
        MarketingStrategy strategy = completedStrategy("user-1", List.of(
                calendarItem("telegram", LocalDateTime.now().plusDays(1), "Post", List.of("#marketing"))));

        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
        when(credentialsService.hasCredentials("user-1", "telegram")).thenReturn(false);

        IllegalStateException exception = assertThrows(
                IllegalStateException.class,
                () -> postingTaskService.createTasksFromStrategy("strategy-1"));

        assertEquals(
                "Credentials not found for platform: telegram. Please configure credentials first.",
                exception.getMessage());
        verify(taskRepository, never()).saveAll(anyList());
    }

    @Test
    void createTasksFromStrategyShouldReturnExistingTasksWhenAlreadyCreated() {
        MarketingStrategy strategy = completedStrategy("user-1", List.of(
                calendarItem("facebook", LocalDateTime.now().plusDays(1), "Post", List.of("#marketing"))));
        List<PostingTask> existingTasks = List.of(task("task-1", "user-1", "facebook", "pending"));

        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
        when(taskRepository.findByStrategyId("strategy-1")).thenReturn(existingTasks);

        List<PostingTask> result = postingTaskService.createTasksFromStrategy("strategy-1");

        assertSame(existingTasks, result);
        verify(taskRepository, never()).saveAll(anyList());
    }

    @Test
    void createTasksFromStrategyShouldCreateTasksAndTriggerImmediateExecutionForPastDates() {
        MarketingStrategy.PostCalendarItem pastItem = calendarItem(
                "facebook",
                LocalDateTime.now().minusHours(1),
                "Past post",
                List.of("#past"));
        pastItem.setImageUrl("https://cdn.example.com/post.png");
        pastItem.setImageFilename("post.png");

        MarketingStrategy.PostCalendarItem futureItem = calendarItem(
                "telegram",
                LocalDateTime.now().plusHours(2),
                "Future post",
                List.of("#future"));

        MarketingStrategy strategy = completedStrategy("user-1", List.of(pastItem, futureItem));

        when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
        when(credentialsService.hasCredentials("user-1", "telegram")).thenReturn(true);
        when(taskRepository.findByStrategyId("strategy-1")).thenReturn(List.of());
        when(taskRepository.saveAll(anyList())).thenAnswer(invocation -> {
            List<PostingTask> tasks = invocation.getArgument(0);
            tasks.get(0).setId("past-task");
            tasks.get(1).setId("future-task");
            return tasks;
        });
        doNothing().when(postingTaskService).executeTaskAsync(anyString());

        List<PostingTask> savedTasks = postingTaskService.createTasksFromStrategy("strategy-1");

        assertEquals(2, savedTasks.size());
        assertEquals("post.png", savedTasks.get(0).getImageFilename());
        assertEquals("https://cdn.example.com/post.png", savedTasks.get(0).getImageUrl());
        assertEquals("telegram", savedTasks.get(1).getPlatform());
        verify(postingTaskService).executeTaskAsync("past-task");
        verify(postingTaskService, never()).executeTaskAsync("future-task");
    }

    @Test
    void getPendingTasksShouldDelegateToRepository() {
        LocalDateTime beforeDate = LocalDateTime.now();
        List<PostingTask> expectedTasks = List.of(task("task-1", "user-1", "facebook", "pending"));
        when(taskRepository.findByStatusAndPublishDateLessThanEqual("pending", beforeDate)).thenReturn(expectedTasks);

        List<PostingTask> actualTasks = postingTaskService.getPendingTasks(beforeDate);

        assertSame(expectedTasks, actualTasks);
    }

    @Test
    void executeTaskShouldIgnoreMissingTask() {
        when(taskRepository.findById("task-1")).thenReturn(Optional.empty());

        postingTaskService.executeTask("task-1");

        verify(taskRepository, never()).save(any());
        verifyNoInteractions(credentialsService, facebookPostingService, linkedInPostingService, telegramPostingService);
    }

    @Test
    void executeTaskShouldSkipTaskWhenStatusIsNotPending() {
        PostingTask task = task("task-1", "user-1", "facebook", "completed");
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));

        postingTaskService.executeTask("task-1");

        verify(taskRepository, never()).save(any());
        verifyNoInteractions(credentialsService, facebookPostingService, linkedInPostingService, telegramPostingService);
    }

    @Test
    void executeTaskShouldCompleteFacebookTaskWithoutImage() throws Exception {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");
        task.setPostText("Product launch");
        task.setHashtags(List.of("#launch"));

        List<String> savedStatuses = new ArrayList<>();
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> {
            PostingTask savedTask = invocation.getArgument(0);
            savedStatuses.add(savedTask.getStatus());
            return savedTask;
        });
        // FacebookPostingService now handles credentials internally
        when(facebookPostingService.postToPage("Product launch", List.of("#launch")))
                .thenReturn("fb-post-1");

        postingTaskService.executeTask("task-1");

        assertEquals(List.of("processing", "completed"), savedStatuses);
        assertEquals("completed", task.getStatus());
        assertNull(task.getErrorMessage());
        assertNotNull(task.getExecutedAt());
        verify(facebookPostingService).postToPage("Product launch", List.of("#launch"));
        verify(facebookPostingService).sendPrivateMessage(
                eq(TEST_RECIPIENT_ID),
                argThat(message -> message.contains("Product launch") && message.contains("https://facebook.com/fb-post-1")));
        verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any());
        verify(minIOService, never()).downloadFile(anyString());
    }

    @Test
    void executeTaskShouldCompleteFacebookTaskWithImageAndIgnoreNotificationFailure() throws Exception {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");
        task.setPostText("Image post");
        task.setHashtags(List.of("#image"));
        task.setImageFilename("banner.png");
        byte[] imageBytes = "binary-image".getBytes(StandardCharsets.UTF_8);

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(minIOService.downloadFile("banner.png")).thenReturn(streamOf(imageBytes));
        when(facebookPostingService.postToPageWithImage(
                eq("Image post"),
                eq(List.of("#image")),
                aryEq(imageBytes)))
                .thenReturn("fb-post-2");
        doThrow(new RuntimeException("Messenger unavailable"))
                .when(facebookPostingService)
                .sendPrivateMessage(eq(TEST_RECIPIENT_ID), anyString());

        postingTaskService.executeTask("task-1");

        assertEquals("completed", task.getStatus());
        assertNull(task.getErrorMessage());
        verify(facebookPostingService).postToPageWithImage(
                eq("Image post"),
                eq(List.of("#image")),
                aryEq(imageBytes));
        verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any());
    }

    @Test
    void executeTaskShouldCompleteLinkedInTaskWithImage() {
        PostingTask task = task("task-1", "user-1", "linkedin", "pending");
        task.setPostText("B2B update");
        task.setHashtags(List.of("#b2b"));
        task.setImageFilename("banner.png");
        byte[] imageBytes = "binary-image".getBytes(StandardCharsets.UTF_8);

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "linkedin")).thenReturn("linkedin-token");
        when(minIOService.downloadFile("banner.png")).thenReturn(streamOf(imageBytes));
        when(linkedInPostingService.postToLinkedInWithImage(
                eq("linkedin-token"),
                eq("B2B update"),
                eq(List.of("#b2b")),
                aryEq(imageBytes),
                eq("image/png")))
                .thenReturn("linkedin-post-1");

        postingTaskService.executeTask("task-1");

        assertEquals("completed", task.getStatus());
        assertNull(task.getErrorMessage());
        verify(linkedInPostingService).postToLinkedInWithImage(
                eq("linkedin-token"),
                eq("B2B update"),
                eq(List.of("#b2b")),
                aryEq(imageBytes),
                eq("image/png"));
        verify(linkedInPostingService, never()).postToLinkedIn(anyString(), anyString(), anyList());
    }

    @Test
    void executeTaskShouldContinueWithoutImageWhenImageDownloadFails() {
        PostingTask task = task("task-1", "user-1", "telegram", "pending");
        task.setPostText("Telegram post");
        task.setHashtags(List.of("#telegram"));
        task.setImageFilename("missing.png");

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "telegram")).thenReturn("{\"botToken\":\"t\",\"chatId\":\"1\"}");
        when(minIOService.downloadFile("missing.png")).thenThrow(new RuntimeException("Missing object"));
        when(telegramPostingService.postToTelegram(
                "{\"botToken\":\"t\",\"chatId\":\"1\"}",
                "Telegram post",
                List.of("#telegram")))
                .thenReturn("telegram-message-1");

        postingTaskService.executeTask("task-1");

        assertEquals("completed", task.getStatus());
        verify(telegramPostingService).postToTelegram(
                "{\"botToken\":\"t\",\"chatId\":\"1\"}",
                "Telegram post",
                List.of("#telegram"));
        verify(telegramPostingService, never()).postToTelegramWithImage(anyString(), anyString(), anyList(), any(), anyString());
    }

    @Test
    void executeTaskShouldFailWhenCredentialsAreMissingForNonFacebookPlatforms() {
        PostingTask task = task("task-1", "user-1", "linkedin", "pending");
        List<String> savedStatuses = new ArrayList<>();

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> {
            PostingTask savedTask = invocation.getArgument(0);
            savedStatuses.add(savedTask.getStatus());
            return savedTask;
        });
        when(credentialsService.getCredentials("user-1", "linkedin")).thenReturn(null);

        postingTaskService.executeTask("task-1");

        assertEquals(List.of("processing", "failed"), savedStatuses);
        assertEquals("failed", task.getStatus());
        assertEquals("Credentials not found for platform: linkedin", task.getErrorMessage());
        verifyNoInteractions(linkedInPostingService);
    }

    @Test
    void executeTaskShouldFailWhenPlatformIsUnsupported() {
        PostingTask task = task("task-1", "user-1", "instagram", "pending");

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "instagram")).thenReturn("ig-token");

        postingTaskService.executeTask("task-1");

        assertEquals("failed", task.getStatus());
        assertEquals("Platform not supported: instagram", task.getErrorMessage());
    }

    @Test
    void executeTaskShouldCompleteWhenFacebookReturnsNullPostId() throws Exception {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");
        task.setPostText("Silent post");
        task.setHashtags(List.of("#silent"));

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(facebookPostingService.postToPage("Silent post", List.of("#silent"))).thenReturn(null);

        postingTaskService.executeTask("task-1");

        assertEquals("completed", task.getStatus());
        assertNull(task.getErrorMessage());
        verify(facebookPostingService).postToPage("Silent post", List.of("#silent"));
        verify(facebookPostingService, never()).sendPrivateMessage(anyString(), anyString());
        verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any());
    }

    @Test
    void executeTaskShouldFailWhenTelegramPublishingTimesOut() {
        PostingTask task = task("task-1", "user-1", "telegram", "pending");
        task.setPostText("Timeout post");
        task.setHashtags(List.of("#timeout"));

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "telegram")).thenReturn("{\"botToken\":\"t\",\"chatId\":\"1\"}");
        when(telegramPostingService.postToTelegram(
                "{\"botToken\":\"t\",\"chatId\":\"1\"}",
                "Timeout post",
                List.of("#timeout")))
                .thenThrow(new RuntimeException("Network timeout"));

        postingTaskService.executeTask("task-1");

        assertEquals("failed", task.getStatus());
        assertEquals("Network timeout", task.getErrorMessage());
        assertNotNull(task.getExecutedAt());
    }

    @Test
    void executeTaskShouldHandleFacebookExpiredToken() throws Exception {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        // FacebookPostingService now handles credentials internally
        when(facebookPostingService.postToPage(task.getPostText(), task.getHashtags()))
                .thenThrow(new FacebookTokenExpiredException("Expired", "code 190", 190, 463));

        postingTaskService.executeTask("task-1");

        assertEquals("failed", task.getStatus());
        assertEquals("Facebook token expired: code 190", task.getErrorMessage());
    }

    @Test
    void executeTaskShouldHandleLinkedInExpiredToken() {
        PostingTask task = task("task-1", "user-1", "linkedin", "pending");

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "linkedin")).thenReturn("linkedin-token");
        when(linkedInPostingService.postToLinkedIn("linkedin-token", task.getPostText(), task.getHashtags()))
                .thenThrow(new LinkedInTokenExpiredException("Expired", "status 401", 401));

        postingTaskService.executeTask("task-1");

        assertEquals("failed", task.getStatus());
        assertEquals("LinkedIn token expired: status 401", task.getErrorMessage());
    }

    @Test
    void executeTaskShouldHandleTelegramExpiredToken() {
        PostingTask task = task("task-1", "user-1", "telegram", "pending");

        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        when(credentialsService.getCredentials("user-1", "telegram")).thenReturn("{\"botToken\":\"t\",\"chatId\":\"1\"}");
        when(telegramPostingService.postToTelegram("{\"botToken\":\"t\",\"chatId\":\"1\"}", task.getPostText(), task.getHashtags()))
                .thenThrow(new TelegramTokenExpiredException("Expired", "status 401", 401));

        postingTaskService.executeTask("task-1");

        assertEquals("failed", task.getStatus());
        assertEquals("Telegram token expired: status 401", task.getErrorMessage());
    }

    @Test
    void executeTaskManuallyShouldThrowWhenTaskIsMissing() {
        when(taskRepository.findById("task-1")).thenReturn(Optional.empty());

        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> postingTaskService.executeTaskManually("task-1"));

        assertEquals("Task not found: task-1", exception.getMessage());
    }

    @Test
    void executeTaskManuallyShouldThrowWhenTaskStatusIsNotAllowed() {
        PostingTask task = task("task-1", "user-1", "facebook", "completed");
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));

        IllegalStateException exception = assertThrows(
                IllegalStateException.class,
                () -> postingTaskService.executeTaskManually("task-1"));

        assertEquals("Task cannot be executed manually. Current status: completed", exception.getMessage());
    }

    @Test
    void executeTaskManuallyShouldResetFailedTaskBeforeExecution() {
        PostingTask task = task("task-1", "user-1", "facebook", "failed");
        task.setErrorMessage("Old error");
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
        doNothing().when(postingTaskService).executeTask("task-1");

        postingTaskService.executeTaskManually("task-1");

        assertEquals("pending", task.getStatus());
        assertNull(task.getErrorMessage());
        verify(taskRepository).save(task);
        verify(postingTaskService).executeTask("task-1");
    }

    @Test
    void executeTaskManuallyShouldExecutePendingTaskWithoutReset() {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
        doNothing().when(postingTaskService).executeTask("task-1");

        postingTaskService.executeTaskManually("task-1");

        verify(taskRepository, never()).save(any());
        verify(postingTaskService).executeTask("task-1");
    }

    @Test
    void queryMethodsShouldDelegateToRepository() {
        PostingTask task = task("task-1", "user-1", "facebook", "pending");
        when(taskRepository.findByUserId("user-1")).thenReturn(List.of(task));
        when(taskRepository.findByStrategyId("strategy-1")).thenReturn(List.of(task));
        when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));

        assertEquals(List.of(task), postingTaskService.getUserTasks("user-1"));
        assertEquals(List.of(task), postingTaskService.getStrategyTasks("strategy-1"));
        assertEquals(Optional.of(task), postingTaskService.getTaskById("task-1"));
    }

    private MarketingStrategy completedStrategy(String userId, List<MarketingStrategy.PostCalendarItem> calendarItems) {
        MarketingStrategy strategy = new MarketingStrategy();
        strategy.setStatus("completed");
        strategy.setUserId(userId);
        strategy.setPostCalendar(calendarItems);
        return strategy;
    }

    private MarketingStrategy.PostCalendarItem calendarItem(
            String platform,
            LocalDateTime publishDate,
            String postText,
            List<String> hashtags) {
        MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();
        item.setPlatform(platform);
        item.setPublishDate(publishDate);
        item.setPostText(postText);
        item.setHashtags(hashtags);
        return item;
    }

    private PostingTask task(String id, String userId, String platform, String status) {
        PostingTask task = new PostingTask();
        task.setId(id);
        task.setUserId(userId);
        task.setStrategyId("strategy-1");
        task.setPlatform(platform);
        task.setPostText("Default post");
        task.setHashtags(List.of("#default"));
        task.setPublishDate(LocalDateTime.now().plusMinutes(5));
        task.setStatus(status);
        return task;
    }

    private InputStream streamOf(byte[] data) {
        return new ByteArrayInputStream(data);
    }
}
