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.NotFoundException;
import ai.decompile.workspace.event.BinaryUploadedEvent;
import ai.decompile.workspace.model.dto.BinaryResponse;
import ai.decompile.workspace.model.entity.Binary;
import ai.decompile.workspace.model.entity.Project;
import ai.decompile.workspace.model.entity.Workspace;
import ai.decompile.workspace.model.repository.BinaryRepository;
import ai.decompile.workspace.service.storage.FileStorage;
import java.io.IOException;
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.context.ApplicationEventPublisher;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.mock.web.MockMultipartFile;

@ExtendWith(MockitoExtension.class)
class BinaryServiceTest {

  @Mock private ProjectService projectService;

  @Mock private BinaryRepository binaryRepository;

  @Mock private FileStorage fileStorage;

  @Mock private ApplicationEventPublisher eventPublisher;

  @InjectMocks private BinaryService binaryService;

  private UUID projectId;
  private UUID binaryId;
  private Project project;
  private Binary binary;
  private byte[] fileContent;

  @BeforeEach
  void setUp() {
    projectId = UUID.randomUUID();
    binaryId = UUID.randomUUID();
    var workspace =
        Workspace.builder()
            .id(UUID.randomUUID())
            .name("WS")
            .createdAt(Instant.now())
            .updatedAt(Instant.now())
            .build();
    project =
        Project.builder()
            .id(projectId)
            .name("Test Project")
            .workspace(workspace)
            .createdAt(Instant.now())
            .updatedAt(Instant.now())
            .build();
    binary =
        Binary.builder()
            .id(binaryId)
            .filename("test.bin")
            .fileSize(100L)
            .filePath("abc_test.bin")
            .project(project)
            .createdAt(Instant.now())
            .updatedAt(Instant.now())
            .build();
    fileContent = new byte[] {1, 2, 3, 4};
    project.setBinaries(List.of(binary));
  }

  @Test
  void getBinariesShouldReturnEmptyList() {
    when(projectService.getProject(projectId)).thenReturn(project);
    when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of());

    List<BinaryResponse> result = binaryService.getBinaries(projectId, null);

    assertTrue(result.isEmpty());
    verify(projectService).getProject(projectId);
    verify(binaryRepository).findAll(any(Specification.class));
  }

  @Test
  void getBinariesShouldReturnList() {
    when(projectService.getProject(projectId)).thenReturn(project);
    when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of(binary));

    List<BinaryResponse> result = binaryService.getBinaries(projectId, null);

    assertEquals(1, result.size());
    assertEquals(binaryId, result.getFirst().id());
    assertEquals("test.bin", result.getFirst().filename());
    assertEquals(projectId, result.getFirst().projectId());
    assertEquals(100L, result.getFirst().fileSize());
  }

  @Test
  void getBinariesShouldFilterByFilename() {
    when(projectService.getProject(projectId)).thenReturn(project);
    when(binaryRepository.findAll(any(Specification.class))).thenReturn(List.of(binary));

    List<BinaryResponse> result = binaryService.getBinaries(projectId, "test");

    assertEquals(1, result.size());
    assertEquals("test.bin", result.getFirst().filename());
    verify(binaryRepository).findAll(any(Specification.class));
  }

  @Test
  void getBinariesShouldThrowNotFoundWhenProjectDoesNotExist() {
    when(projectService.getProject(projectId))
        .thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));

    assertThrows(NotFoundException.class, () -> binaryService.getBinaries(projectId, null));
    verify(binaryRepository, never()).findAll(any(Specification.class));
  }

  @Test
  void uploadBinaryShouldSucceed() throws IOException {
    var file = new MockMultipartFile("file", "test.exe", "application/octet-stream", fileContent);
    when(projectService.getProject(projectId)).thenReturn(project);
    when(fileStorage.store(any(), eq("test.exe"))).thenReturn("stored_test.exe");
    when(binaryRepository.save(any(Binary.class))).thenReturn(binary);

    BinaryResponse result = binaryService.uploadBinary(projectId, file, null, null);

    assertEquals(binaryId, result.id());
    assertEquals("test.bin", result.filename());
    verify(projectService).getProject(projectId);
    verify(fileStorage).store(any(), eq("test.exe"));
    verify(binaryRepository).save(any(Binary.class));
    verify(eventPublisher).publishEvent(any(BinaryUploadedEvent.class));
  }

  @Test
  void uploadBinaryShouldThrowRuntimeExceptionOnStorageFailure() throws IOException {
    var file = new MockMultipartFile("file", "test.exe", "application/octet-stream", fileContent);
    when(projectService.getProject(projectId)).thenReturn(project);
    when(fileStorage.store(any(), eq("test.exe"))).thenThrow(new IOException("disk full"));

    assertThrows(
        RuntimeException.class, () -> binaryService.uploadBinary(projectId, file, null, null));
    verify(binaryRepository, never()).save(any());
    verify(eventPublisher, never()).publishEvent(any());
  }

  @Test
  void deleteBinaryShouldSucceed() throws IOException {
    when(binaryRepository.findById(binaryId)).thenReturn(Optional.of(binary));

    binaryService.deleteBinary(binaryId);

    verify(fileStorage).delete(binary.getFilePath());
    verify(binaryRepository).delete(binary);
  }

  @Test
  void deleteBinaryShouldThrowNotFound() throws IOException {
    when(binaryRepository.findById(binaryId)).thenReturn(Optional.empty());

    assertThrows(NotFoundException.class, () -> binaryService.deleteBinary(binaryId));
    verify(fileStorage, never()).delete(any());
    verify(binaryRepository, never()).delete((Binary) any());
  }
}
