package ai.decompile.project.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.ConflictException;
import ai.decompile.common.exception.NotFoundException;
import ai.decompile.workspace.controller.ProjectController;
import ai.decompile.workspace.model.dto.ProjectRequest;
import ai.decompile.workspace.model.dto.ProjectResponse;
import ai.decompile.workspace.service.ProjectService;
import com.fasterxml.jackson.databind.ObjectMapper;
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.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(ProjectController.class)
class ProjectControllerTest {

  @Autowired private MockMvc mockMvc;

  @MockitoBean private ProjectService projectService;

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Test
  void getProjectsShouldReturn200WithList() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    var response =
        new ProjectResponse(projectId, workspaceId, "Test Project", "Desc", Instant.now(), 0);
    when(projectService.getProjects(eq(workspaceId), isNull())).thenReturn(List.of(response));

    mockMvc
        .perform(get("/workspaces/{workspaceId}/projects", workspaceId))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].id").value(projectId.toString()))
        .andExpect(jsonPath("$[0].name").value("Test Project"))
        .andExpect(jsonPath("$[0].workspaceId").value(workspaceId.toString()));
  }

  @Test
  void getProjectsShouldReturn200WithEmptyList() throws Exception {
    var workspaceId = UUID.randomUUID();
    when(projectService.getProjects(eq(workspaceId), isNull())).thenReturn(List.of());

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

  @Test
  void getProjectsShouldFilterByName() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    var response =
        new ProjectResponse(projectId, workspaceId, "Filtered Project", "Desc", Instant.now(), 0);
    when(projectService.getProjects(eq(workspaceId), eq("filter"))).thenReturn(List.of(response));

    mockMvc
        .perform(get("/workspaces/{workspaceId}/projects", workspaceId).param("name", "filter"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].id").value(projectId.toString()))
        .andExpect(jsonPath("$[0].name").value("Filtered Project"));
  }

  @Test
  void createProjectShouldReturn200() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    var request = new ProjectRequest("New Project", "A description");
    var response =
        new ProjectResponse(
            projectId, workspaceId, "New Project", "A description", Instant.now(), 0);
    when(projectService.createProject(eq(workspaceId), any())).thenReturn(response);

    mockMvc
        .perform(
            post("/workspaces/{workspaceId}/projects", workspaceId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(projectId.toString()))
        .andExpect(jsonPath("$.name").value("New Project"))
        .andExpect(jsonPath("$.workspaceId").value(workspaceId.toString()));
  }

  @Test
  void createProjectShouldReturn404WhenWorkspaceNotFound() throws Exception {
    var workspaceId = UUID.randomUUID();
    var request = new ProjectRequest("New Project", "desc");
    when(projectService.createProject(eq(workspaceId), any()))
        .thenThrow(new NotFoundException("Workspace with id '" + workspaceId + "' not found"));

    mockMvc
        .perform(
            post("/workspaces/{workspaceId}/projects", workspaceId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.error").value("Not Found"));
  }

  @Test
  void createProjectShouldReturn409OnConflict() throws Exception {
    var workspaceId = UUID.randomUUID();
    var request = new ProjectRequest("Duplicate", "desc");
    when(projectService.createProject(eq(workspaceId), any()))
        .thenThrow(
            new ConflictException(
                "A project with the name 'Duplicate' already exists in this workspace."));

    mockMvc
        .perform(
            post("/workspaces/{workspaceId}/projects", workspaceId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isConflict())
        .andExpect(jsonPath("$.status").value(409))
        .andExpect(jsonPath("$.error").value("Conflict"));
  }

  @Test
  void updateProjectShouldReturn200() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    var request = new ProjectRequest("Updated", "Updated desc");
    var response =
        new ProjectResponse(projectId, workspaceId, "Updated", "Updated desc", Instant.now(), 0);
    when(projectService.updateProject(eq(projectId), any())).thenReturn(response);

    mockMvc
        .perform(
            put("/projects/{projectId}", projectId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.name").value("Updated"));
  }

  @Test
  void updateProjectShouldReturn404OnNotFound() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    var request = new ProjectRequest("Updated", "desc");
    when(projectService.updateProject(eq(projectId), any()))
        .thenThrow(new NotFoundException("Project with id '" + projectId + "' not found"));

    mockMvc
        .perform(
            put("/projects/{projectId}", projectId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.error").value("Not Found"));
  }

  @Test
  void deleteProjectShouldReturn200() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    doNothing().when(projectService).deleteProject(projectId);

    mockMvc.perform(delete("/projects/{projectId}", projectId)).andExpect(status().isOk());
  }

  @Test
  void deleteProjectShouldReturn404OnNotFound() throws Exception {
    var workspaceId = UUID.randomUUID();
    var projectId = UUID.randomUUID();
    doThrow(new NotFoundException("Project with id '" + projectId + "' not found"))
        .when(projectService)
        .deleteProject(projectId);

    mockMvc
        .perform(delete("/projects/{projectId}", projectId))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.error").value("Not Found"));
  }
}
