package ai.decompile.job.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.job.messaging.JobMessagePublisher;
import ai.decompile.job.model.entity.Job;
import ai.decompile.job.model.enums.JobStatus;
import ai.decompile.job.model.enums.JobType;
import ai.decompile.job.model.repository.JobRepository;
import ai.decompile.workspace.event.BinaryUploadedEvent;
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.ArgumentCaptor;
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 JobServiceTest {

  @Mock private JobRepository jobRepository;

  @Mock private JobMessagePublisher jobMessagePublisher;

  @InjectMocks private JobService jobService;

  private UUID projectId;
  private UUID binaryId;
  private UUID jobId;
  private Job job;

  @BeforeEach
  void setUp() {
    projectId = UUID.randomUUID();
    binaryId = UUID.randomUUID();
    jobId = UUID.randomUUID();
    job =
        Job.builder()
            .id(jobId)
            .type(JobType.ANALYZE_FILE)
            .status(JobStatus.ENQUEUED)
            .projectId(projectId)
            .binaryId(binaryId)
            .retryCount(0)
            .build();
  }

  @Test
  void getJobsShouldReturnEmptyList() {
    when(jobRepository.findAll(any(Specification.class))).thenReturn(List.of());
    when(jobRepository.count(any(Specification.class))).thenReturn(0L);

    var result = jobService.getJobs(projectId, null, null, null);

    assertTrue(result.jobs().isEmpty());
    assertEquals(0L, result.running());
    assertEquals(0L, result.queued());
    assertEquals(0L, result.completed());
    assertEquals(0L, result.failed());
    verify(jobRepository).findAll(any(Specification.class));
  }

  @Test
  void getJobsShouldReturnList() {
    when(jobRepository.findAll(any(Specification.class))).thenReturn(List.of(job));
    when(jobRepository.count(any(Specification.class))).thenReturn(0L);

    var result = jobService.getJobs(projectId, null, null, null);

    assertEquals(1, result.jobs().size());
    assertEquals(jobId, result.jobs().getFirst().id());
    assertEquals(JobType.ANALYZE_FILE, result.jobs().getFirst().type());
    assertEquals(JobStatus.ENQUEUED, result.jobs().getFirst().status());
    assertEquals(projectId, result.jobs().getFirst().projectId());
    assertEquals(binaryId, result.jobs().getFirst().binaryId());
  }

  @Test
  void getJobsShouldFilterByStatus() {
    when(jobRepository.findAll(any(Specification.class))).thenReturn(List.of(job));
    when(jobRepository.count(any(Specification.class))).thenReturn(0L);

    var result = jobService.getJobs(projectId, JobStatus.ENQUEUED, null, null);

    assertEquals(1, result.jobs().size());
    assertEquals(JobStatus.ENQUEUED, result.jobs().getFirst().status());
  }

  @Test
  void getJobsShouldFilterByType() {
    when(jobRepository.findAll(any(Specification.class))).thenReturn(List.of(job));
    when(jobRepository.count(any(Specification.class))).thenReturn(0L);

    var result = jobService.getJobs(projectId, null, JobType.ANALYZE_FILE, null);

    assertEquals(1, result.jobs().size());
    assertEquals(JobType.ANALYZE_FILE, result.jobs().getFirst().type());
  }

  @Test
  void getJobShouldReturnJob() {
    when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));

    var result = jobService.getJob(jobId);

    assertEquals(jobId, result.id());
    assertEquals(JobStatus.ENQUEUED, result.status());
  }

  @Test
  void getJobShouldThrowNotFound() {
    when(jobRepository.findById(jobId)).thenReturn(Optional.empty());

    assertThrows(NotFoundException.class, () -> jobService.getJob(jobId));
  }

  @Test
  void cancelJobShouldUpdateStatus() {
    when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));
    when(jobRepository.save(any(Job.class))).thenReturn(job);

    var result = jobService.cancelJob(jobId);

    assertEquals(JobStatus.CANCELED, result.status());
    verify(jobRepository).save(any(Job.class));
  }

  @Test
  void cancelJobShouldThrowConflictWhenAlreadyCompleted() {
    job.setStatus(JobStatus.COMPLETED);
    when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));

    assertThrows(ConflictException.class, () -> jobService.cancelJob(jobId));
    verify(jobRepository, never()).save(any());
  }

  @Test
  void cancelJobShouldThrowConflictWhenAlreadyFailed() {
    job.setStatus(JobStatus.FAILED);
    when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));

    assertThrows(ConflictException.class, () -> jobService.cancelJob(jobId));
    verify(jobRepository, never()).save(any());
  }

  @Test
  void cancelJobShouldThrowConflictWhenAlreadyCanceled() {
    job.setStatus(JobStatus.CANCELED);
    when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));

    assertThrows(ConflictException.class, () -> jobService.cancelJob(jobId));
    verify(jobRepository, never()).save(any());
  }

  @Test
  void cancelJobShouldThrowNotFound() {
    when(jobRepository.findById(jobId)).thenReturn(Optional.empty());

    assertThrows(NotFoundException.class, () -> jobService.cancelJob(jobId));
    verify(jobRepository, never()).save(any());
  }

  @Test
  void onBinaryUploadedShouldCreateJobAndPublish() {
    var event = new BinaryUploadedEvent(binaryId, projectId, UUID.randomUUID());
    when(jobRepository.save(any(Job.class))).thenReturn(job);

    jobService.onBinaryUploaded(event);

    var captor = ArgumentCaptor.forClass(Job.class);
    verify(jobRepository).save(captor.capture());
    var savedJob = captor.getValue();
    assertEquals(JobType.ANALYZE_FILE, savedJob.getType());
    assertEquals(JobStatus.ENQUEUED, savedJob.getStatus());
    assertEquals(projectId, savedJob.getProjectId());
    assertEquals(binaryId, savedJob.getBinaryId());

    verify(jobMessagePublisher).publish(jobId);
  }
}
