package ai.decompile.workspace.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import ai.decompile.common.exception.NotFoundException;
import ai.decompile.workspace.model.dto.BinaryResponse;
import ai.decompile.workspace.service.BinaryService;
import ai.decompile.workspace.service.storage.FileResource;
import java.io.ByteArrayInputStream;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(BinaryController.class)
class BinaryControllerTest {

  @Autowired private MockMvc mockMvc;

  @MockitoBean private BinaryService binaryService;

  @MockitoBean private ApplicationEventPublisher eventPublisher;

  @Test
  void getBinariesShouldReturn200WithList() throws Exception {
    var projectId = UUID.randomUUID();
    var binaryId = UUID.randomUUID();
    var response =
        new BinaryResponse(
            binaryId,
            projectId,
            "test.exe",
            1024L,
            null,
            null,
            null,
            "abc_test.exe",
            false,
            Instant.now(),
            List.of());
    when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of(response));

    mockMvc
        .perform(get("/projects/{projectId}/binaries", projectId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].id").value(binaryId.toString()))
        .andExpect(jsonPath("$[0].filename").value("test.exe"))
        .andExpect(jsonPath("$[0].fileSize").value(1024));
  }

  @Test
  void getBinariesShouldReturn200WithEmptyList() throws Exception {
    var projectId = UUID.randomUUID();
    when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of());

    mockMvc
        .perform(get("/projects/{projectId}/binaries", projectId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$").isArray())
        .andExpect(jsonPath("$").isEmpty());
  }

  @Test
  void getBinariesShouldFilterByName() throws Exception {
    var projectId = UUID.randomUUID();
    var binaryId = UUID.randomUUID();
    var response =
        new BinaryResponse(
            binaryId,
            projectId,
            "filtered.exe",
            512L,
            null,
            null,
            null,
            "abc_filtered.exe",
            false,
            Instant.now(),
            List.of());
    when(binaryService.getBinaries(eq(projectId), eq("filter"))).thenReturn(List.of(response));

    mockMvc
        .perform(get("/projects/{projectId}/binaries", projectId).param("name", "filter"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].filename").value("filtered.exe"));
  }

  @Test
  void getBinariesShouldReturn404WhenProjectNotFound() throws Exception {
    var projectId = UUID.randomUUID();
    when(binaryService.getBinaries(eq(projectId), isNull()))
        .thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));

    mockMvc
        .perform(get("/projects/{projectId}/binaries", projectId))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404));
  }

  @Test
  void uploadBinaryShouldReturn201() throws Exception {
    var projectId = UUID.randomUUID();
    var binaryId = UUID.randomUUID();
    var file =
        new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2, 3});
    var response =
        new BinaryResponse(
            binaryId,
            projectId,
            "test.exe",
            3L,
            null,
            null,
            null,
            "abc_test.exe",
            false,
            Instant.now(),
            List.of());
    when(binaryService.uploadBinary(eq(projectId), any(), isNull(), isNull())).thenReturn(response);

    mockMvc
        .perform(multipart("/projects/{projectId}/binaries", projectId).file(file))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.id").value(binaryId.toString()))
        .andExpect(jsonPath("$.filename").value("test.exe"))
        .andExpect(jsonPath("$.fileSize").value(3));
  }

  @Test
  void deleteBinaryShouldReturn204() throws Exception {
    var projectId = UUID.randomUUID();
    var binaryId = UUID.randomUUID();
    doNothing().when(binaryService).deleteBinary(binaryId);

    mockMvc.perform(delete("/binaries/{binaryId}", binaryId)).andExpect(status().isNoContent());
  }

  @Test
  void deleteBinaryShouldReturn404OnNotFound() throws Exception {
    var projectId = UUID.randomUUID();
    var binaryId = UUID.randomUUID();
    doThrow(new NotFoundException("Binary with id '" + binaryId + "' not found"))
        .when(binaryService)
        .deleteBinary(binaryId);

    mockMvc
        .perform(delete("/binaries/{binaryId}", binaryId))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404));
  }

  @Test
  void downloadBinaryShouldReturn200() throws Exception {
    var binaryId = UUID.randomUUID();
    var content = new byte[] {1, 2, 3, 4};
    var resource = new FileResource(new ByteArrayInputStream(content), "test.exe", 4L);
    when(binaryService.downloadBinary(binaryId)).thenReturn(resource);

    mockMvc
        .perform(get("/binaries/{binaryId}/download", binaryId))
        .andExpect(status().isOk())
        .andExpect(
            header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.exe\""))
        .andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, 4))
        .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
        .andExpect(content().bytes(content));
  }

  @Test
  void downloadBinaryShouldReturn404OnNotFound() throws Exception {
    var binaryId = UUID.randomUUID();
    when(binaryService.downloadBinary(binaryId))
        .thenThrow(new NotFoundException("Binary with id '" + binaryId + "' not found"));

    mockMvc
        .perform(get("/binaries/{binaryId}/download", binaryId))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404));
  }
}
