package ai.decompile.workspace.service;

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

import ai.decompile.common.exception.ConflictException;
import ai.decompile.common.exception.NotFoundException;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.model.entity.Project;
import ai.decompile.workspace.model.entity.Workspace;
import ai.decompile.workspace.model.repository.ProjectRepository;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.jpa.domain.Specification;

@ExtendWith(MockitoExtension.class)
class ProjectServiceTest {

  @Mock private ProjectRepository projectRepository;

  @Mock private WorkspaceService workspaceService;

  @InjectMocks private ProjectService projectService;

  private UUID workspaceId;
  private UUID projectId;
  private Workspace workspace;
  private Project project;
  private ProjectRequest request;

  @BeforeEach
  void setUp() {
    workspaceId = UUID.randomUUID();
    projectId = UUID.randomUUID();
    workspace =
        Workspace.builder()
            .id(workspaceId)
            .name("Test Workspace")
            .description("WS Desc")
            .createdAt(Instant.now())
            .updatedAt(Instant.now())
            .build();
    project =
        Project.builder()
            .id(projectId)
            .name("Test Project")
            .description("Project Desc")
            .workspace(workspace)
            .createdAt(Instant.now())
            .updatedAt(Instant.now())
            .build();
    request = new ProjectRequest("Test Project", "Project Desc");
  }

  @Test
  void getProjectsShouldReturnEmptyList() {
    when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of());

    List<ProjectResponse> result = projectService.getProjects(workspaceId, null);

    assertTrue(result.isEmpty());
    verify(projectRepository).findAll(any(Specification.class));
  }

  @Test
  void getProjectsShouldReturnList() {
    when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(project));

    List<ProjectResponse> result = projectService.getProjects(workspaceId, null);

    assertEquals(1, result.size());
    assertEquals(projectId, result.getFirst().id());
    assertEquals("Test Project", result.getFirst().name());
    assertEquals(workspaceId, result.getFirst().workspaceId());
  }

  @Test
  void getProjectsShouldFilterByName() {
    when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(project));

    List<ProjectResponse> result = projectService.getProjects(workspaceId, "test");

    assertEquals(1, result.size());
    assertEquals("Test Project", result.getFirst().name());
    verify(projectRepository).findAll(any(Specification.class));
  }

  @Test
  void createProjectShouldSucceed() {
    when(workspaceService.getWorkspace(workspaceId)).thenReturn(workspace);
    when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId))
        .thenReturn(false);
    when(projectRepository.save(any(Project.class))).thenReturn(project);

    ProjectResponse result = projectService.createProject(workspaceId, request);

    assertEquals(projectId, result.id());
    assertEquals("Test Project", result.name());
    assertEquals(workspaceId, result.workspaceId());
    verify(workspaceService).getWorkspace(workspaceId);
    verify(projectRepository).existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId);
    verify(projectRepository).save(any(Project.class));
  }

  @Test
  void createProjectShouldThrowNotFoundWhenWorkspaceDoesNotExist() {
    when(workspaceService.getWorkspace(workspaceId))
        .thenThrow(new NotFoundException("Workspace with id '" + workspaceId + "' not found"));

    NotFoundException ex =
        assertThrows(
            NotFoundException.class, () -> projectService.createProject(workspaceId, request));

    assertTrue(ex.getMessage().contains(workspaceId.toString()));
    verify(projectRepository, never()).save(any());
  }

  @Test
  void createProjectShouldThrowConflictWhenNameExistsInWorkspace() {
    when(workspaceService.getWorkspace(workspaceId)).thenReturn(workspace);
    when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id(request.name(), workspaceId))
        .thenReturn(true);

    ConflictException ex =
        assertThrows(
            ConflictException.class, () -> projectService.createProject(workspaceId, request));

    assertTrue(ex.getMessage().contains("Test Project"));
    verify(projectRepository, never()).save(any());
  }

  @Test
  void updateProjectShouldSucceed() {
    when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
    when(projectRepository.save(any(Project.class))).thenReturn(project);

    var newRequest = new ProjectRequest("Updated Name", "Updated Desc");
    ProjectResponse result = projectService.updateProject(projectId, newRequest);

    assertEquals(projectId, result.id());
    assertEquals("Updated Name", result.name());
    verify(projectRepository).findById(projectId);
    verify(projectRepository).save(any(Project.class));
  }

  @Test
  void updateProjectShouldSucceedWhenNameUnchanged() {
    when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
    when(projectRepository.save(any(Project.class))).thenReturn(project);

    ProjectResponse result = projectService.updateProject(projectId, request);

    assertEquals(projectId, result.id());
    verify(projectRepository).save(any(Project.class));
  }

  @Test
  void updateProjectShouldThrowNotFound() {
    when(projectRepository.findById(projectId)).thenReturn(Optional.empty());

    NotFoundException ex =
        assertThrows(
            NotFoundException.class, () -> projectService.updateProject(projectId, request));

    assertTrue(ex.getMessage().contains(projectId.toString()));
    verify(projectRepository, never()).save(any());
  }

  @Test
  void updateProjectShouldThrowConflictWhenNameExistsInWorkspace() {
    var newRequest = new ProjectRequest("New Name", "desc");
    when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
    when(projectRepository.existsByNameIgnoreCaseAndWorkspace_Id("New Name", workspaceId))
        .thenReturn(true);

    ConflictException ex =
        assertThrows(
            ConflictException.class, () -> projectService.updateProject(projectId, newRequest));

    assertTrue(ex.getMessage().contains("New Name"));
    verify(projectRepository, never()).save(any());
  }

  @Test
  void deleteProjectShouldSucceed() {
    when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));

    projectService.deleteProject(projectId);

    verify(projectRepository).findById(projectId);
    verify(projectRepository).delete(project);
  }

  @Test
  void deleteProjectShouldThrowNotFound() {
    when(projectRepository.findById(projectId)).thenReturn(Optional.empty());

    NotFoundException ex =
        assertThrows(NotFoundException.class, () -> projectService.deleteProject(projectId));

    assertTrue(ex.getMessage().contains(projectId.toString()));
    verify(projectRepository, never()).delete((Project) any());
  }
}
