package ai.decompile.workspace.controller;

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

import ai.decompile.workspace.model.dto.BinaryResponse;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.model.dto.WorkspaceRequest;
import ai.decompile.workspace.model.dto.WorkspaceResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import java.util.UUID;
import org.junit.jupiter.api.Test;
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.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class BinaryIntegrationTest {

  @Autowired private MockMvc mockMvc;

  @Autowired private EntityManager entityManager;

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Test
  void fullCrudFlow() throws Exception {
    var workspaceId = createWorkspace("Binary WS");
    var projectId = createProject(workspaceId, "Binary Project");

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

    var file =
        new MockMultipartFile(
            "file",
            "binary.exe",
            "application/octet-stream",
            new byte[] {0x01, 0x02, 0x03, 0x04, 0x05});

    var createdJson =
        mockMvc
            .perform(multipart("/projects/{pid}/binaries", projectId).file(file))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.filename").value("binary.exe"))
            .andExpect(jsonPath("$.fileSize").value(5))
            .andExpect(jsonPath("$.format").isEmpty())
            .andExpect(jsonPath("$.architecture").isEmpty())
            .andReturn()
            .getResponse()
            .getContentAsString();

    var created = objectMapper.readValue(createdJson, BinaryResponse.class);

    mockMvc
        .perform(get("/projects/{pid}/binaries", projectId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(1))
        .andExpect(jsonPath("$[0].filename").value("binary.exe"))
        .andExpect(jsonPath("$[0].fileSize").value(5));

    mockMvc
        .perform(get("/projects/{pid}/binaries", projectId).param("name", "binary"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(1));

    mockMvc
        .perform(get("/projects/{pid}/binaries", projectId).param("name", "nonexistent"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(0));

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

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

  @Test
  void uploadBinaryToNonexistentProjectShouldReturn404() throws Exception {
    var fakeProjectId = UUID.randomUUID();
    var file =
        new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2});

    mockMvc
        .perform(multipart("/projects/{pid}/binaries", fakeProjectId).file(file))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404));
  }

  @Test
  void deleteNonexistentBinaryShouldReturn404() throws Exception {
    var workspaceId = createWorkspace("Binary WS3");
    var projectId = createProject(workspaceId, "Binary Project 3");
    var fakeBinaryId = UUID.randomUUID();

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

  @Test
  void projectResponseShouldIncludeBinaryCount() throws Exception {
    var workspaceId = createWorkspace("Count WS");
    var projectId = createProject(workspaceId, "Count Project");

    mockMvc
        .perform(get("/projects/{pid}", projectId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.binaryCount").value(0));

    var file1 = new MockMultipartFile("file", "a.exe", "application/octet-stream", new byte[] {1});
    mockMvc
        .perform(multipart("/projects/{pid}/binaries", projectId).file(file1))
        .andExpect(status().isCreated());

    var file2 = new MockMultipartFile("file", "b.exe", "application/octet-stream", new byte[] {2});
    mockMvc
        .perform(multipart("/projects/{pid}/binaries", projectId).file(file2))
        .andExpect(status().isCreated());

    entityManager.flush();
    entityManager.clear();

    mockMvc
        .perform(get("/projects/{pid}", projectId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.binaryCount").value(2));
  }

  @Test
  void downloadBinaryShouldReturnFile() throws Exception {
    var workspaceId = createWorkspace("Download WS");
    var projectId = createProject(workspaceId, "Download Project");

    var content = new byte[] {0x01, 0x02, 0x03};
    var file = new MockMultipartFile("file", "download.bin", "application/octet-stream", content);

    var createdJson =
        mockMvc
            .perform(multipart("/projects/{pid}/binaries", projectId).file(file))
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse()
            .getContentAsString();
    var created = objectMapper.readValue(createdJson, BinaryResponse.class);

    mockMvc
        .perform(get("/binaries/{bid}/download", created.id()))
        .andExpect(status().isOk())
        .andExpect(
            header()
                .string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"download.bin\""))
        .andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, 3))
        .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
        .andExpect(content().bytes(content));
  }

  @Test
  void downloadBinaryShouldReturn404OnNotFound() throws Exception {
    var fakeBinaryId = UUID.randomUUID();

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

  private UUID createWorkspace(String name) throws Exception {
    var request = new WorkspaceRequest(name, "description");
    var json =
        mockMvc
            .perform(
                post("/workspaces")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk())
            .andReturn()
            .getResponse()
            .getContentAsString();
    return objectMapper.readValue(json, WorkspaceResponse.class).id();
  }

  private UUID createProject(UUID workspaceId, String name) throws Exception {
    var request = new ProjectRequest(name, "desc");
    var json =
        mockMvc
            .perform(
                post("/workspaces/{workspaceId}/projects", workspaceId)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk())
            .andReturn()
            .getResponse()
            .getContentAsString();
    return objectMapper.readValue(json, ProjectResponse.class).id();
  }
}
