package ai.decompile.job.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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.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.model.repository.ProjectRepository;
import ai.decompile.workspace.model.repository.WorkspaceRepository;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class JobIntegrationTest {

  @Autowired private MockMvc mockMvc;

  @Autowired private JobRepository jobRepository;

  @Autowired private WorkspaceRepository workspaceRepository;

  @Autowired private ProjectRepository projectRepository;

  @Autowired private BinaryRepository binaryRepository;

  @MockitoBean private RabbitTemplate rabbitTemplate;

  private UUID projectId;
  private UUID binaryId;
  private String binaryName;
  private String projectName;

  @BeforeEach
  void setUp() {
    jobRepository.deleteAll();
    binaryRepository.deleteAll();
    projectRepository.deleteAll();
    workspaceRepository.deleteAll();

    var workspace =
        workspaceRepository.save(Workspace.builder().name("Test WS").description("desc").build());

    binaryName = "test.exe";
    projectName = "Test Project";

    var project =
        projectRepository.save(
            Project.builder().name(projectName).description("desc").workspace(workspace).build());

    var binary =
        binaryRepository.save(
            Binary.builder()
                .filename(binaryName)
                .fileSize(100L)
                .filePath("/tmp/test.exe")
                .project(project)
                .build());

    projectId = project.getId();
    binaryId = binary.getId();
  }

  @AfterEach
  void tearDown() {
    jobRepository.deleteAll();
    binaryRepository.deleteAll();
    projectRepository.deleteAll();
    workspaceRepository.deleteAll();
  }

  @Test
  void getJobsShouldReturnEmptyList() throws Exception {
    mockMvc
        .perform(get("/jobs").param("projectId", projectId.toString()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.jobs").isArray())
        .andExpect(jsonPath("$.jobs").isEmpty())
        .andExpect(jsonPath("$.running").value(0));
  }

  @Test
  void getJobsShouldReturnListWithFilters() throws Exception {
    createJob(JobStatus.ENQUEUED);

    mockMvc
        .perform(get("/jobs").param("projectId", projectId.toString()).param("status", "ENQUEUED"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.jobs.length()").value(1))
        .andExpect(jsonPath("$.jobs[0].status").value("ENQUEUED"))
        .andExpect(jsonPath("$.jobs[0].type").value("ANALYZE_FILE"))
        .andExpect(jsonPath("$.jobs[0].projectId").value(projectId.toString()))
        .andExpect(jsonPath("$.jobs[0].binaryId").value(binaryId.toString()))
        .andExpect(jsonPath("$.jobs[0].binaryName").value(binaryName))
        .andExpect(jsonPath("$.jobs[0].projectName").value(projectName));
  }

  @Test
  void getJobsShouldFilterByStatus() throws Exception {
    createJob(JobStatus.COMPLETED);
    createJob(JobStatus.FAILED);

    mockMvc
        .perform(get("/jobs").param("projectId", projectId.toString()).param("status", "COMPLETED"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.jobs.length()").value(1))
        .andExpect(jsonPath("$.jobs[0].status").value("COMPLETED"));
  }

  @Test
  void getJobShouldReturnJob() throws Exception {
    var job = createJob(JobStatus.ENQUEUED);

    mockMvc
        .perform(get("/jobs/{jobId}", job.getId()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(job.getId().toString()))
        .andExpect(jsonPath("$.status").value("ENQUEUED"))
        .andExpect(jsonPath("$.binaryName").value(binaryName))
        .andExpect(jsonPath("$.projectName").value(projectName));
  }

  @Test
  void getJobShouldReturn404OnNotFound() throws Exception {
    var fakeId = UUID.randomUUID();

    mockMvc
        .perform(get("/jobs/{jobId}", fakeId))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404));
  }

  @Test
  void cancelJobShouldSetStatusToCanceled() throws Exception {
    var job = createJob(JobStatus.ENQUEUED);

    mockMvc
        .perform(post("/jobs/{jobId}/cancel", job.getId()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(job.getId().toString()))
        .andExpect(jsonPath("$.status").value("CANCELED"));

    var updated = jobRepository.findById(job.getId()).orElseThrow();
    assert updated.getStatus() == JobStatus.CANCELED;
  }

  @Test
  void cancelJobShouldReturn409OnAlreadyCanceled() throws Exception {
    var job = createJob(JobStatus.CANCELED);

    mockMvc
        .perform(post("/jobs/{jobId}/cancel", job.getId()))
        .andExpect(status().isConflict())
        .andExpect(jsonPath("$.status").value(409));
  }

  @Test
  void cancelJobShouldReturn409OnAlreadyCompleted() throws Exception {
    var job = createJob(JobStatus.COMPLETED);

    mockMvc
        .perform(post("/jobs/{jobId}/cancel", job.getId()))
        .andExpect(status().isConflict())
        .andExpect(jsonPath("$.status").value(409));
  }

  private Job createJob(JobStatus status) {
    return jobRepository.save(
        Job.builder()
            .type(JobType.ANALYZE_FILE)
            .status(status)
            .projectId(projectId)
            .binaryId(binaryId)
            .retryCount(0)
            .build());
  }
}
