package org.raddatz.familienarchiv.person.relationship;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.document.DatePrecision;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipWithPersonDTO;
import org.raddatz.familienarchiv.person.relationship.dto.NetworkDTO;
import org.raddatz.familienarchiv.person.relationship.dto.PersonNodeDTO;
import org.raddatz.familienarchiv.person.relationship.dto.RelationshipDTO;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(RelationshipController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class RelationshipControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean RelationshipService relationshipService;
    @MockitoBean UserService userService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private static final UUID PERSON_ID = UUID.randomUUID();
    private static final UUID OTHER_ID  = UUID.randomUUID();

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getRelationshipBetween_returns404_with_RELATIONSHIP_NOT_FOUND_code_when_no_path() throws Exception {
        when(relationshipService.getRelationshipBetween(any(), any())).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/persons/{aId}/relationship-to/{bId}", PERSON_ID, OTHER_ID))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value(ErrorCode.RELATIONSHIP_NOT_FOUND.name()));
    }

    @Test
    void getRelationships_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/persons/{id}/relationships", PERSON_ID))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void getNetwork_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/network"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void addRelationship_returns403_for_user_with_READ_ALL_only() throws Exception {
        mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"PARENT_OF\"}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void deleteRelationship_returns403_for_READ_ALL_only_user() throws Exception {
        mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void patchFamilyMember_returns403_for_READ_ALL_only_user() throws Exception {
        mockMvc.perform(patch("/api/persons/{id}/family-member", PERSON_ID).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"familyMember\":true}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNetwork_returns200_with_NetworkDTO_for_authenticated_user() throws Exception {
        PersonNodeDTO node = new PersonNodeDTO(PERSON_ID, "Alice Müller", 1900, 1980, null, true);
        RelationshipDTO edge = new RelationshipDTO(
                UUID.randomUUID(), PERSON_ID, OTHER_ID,
                "Alice Müller", 1900, 1980,
                "Bob Müller", 1930, null,
                RelationType.PARENT_OF, null, DatePrecision.UNKNOWN, null, DatePrecision.UNKNOWN, null);
        when(relationshipService.getFamilyNetwork())
                .thenReturn(new NetworkDTO(List.of(node), List.of(edge)));

        mockMvc.perform(get("/api/network"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.nodes[0].displayName").value("Alice Müller"))
                .andExpect(jsonPath("$.edges[0].relationType").value("PARENT_OF"));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getInferredRelationships_returns200_with_list_for_authenticated_user() throws Exception {
        PersonNodeDTO relative = new PersonNodeDTO(OTHER_ID, "Bob Müller", 1930, null, null, true);
        InferredRelationshipWithPersonDTO inferred =
                new InferredRelationshipWithPersonDTO(relative, "Großvater", 2);
        when(relationshipService.getInferredRelationships(PERSON_ID))
                .thenReturn(List.of(inferred));

        mockMvc.perform(get("/api/persons/{id}/inferred-relationships", PERSON_ID))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].label").value("Großvater"))
                .andExpect(jsonPath("$[0].hops").value(2));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void addRelationship_returns400_when_relationType_is_unknown_value() throws Exception {
        mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"NOT_A_REAL_TYPE\"}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void addRelationship_returns201_with_RelationshipDTO_for_WRITE_ALL_user() throws Exception {
        RelationshipDTO created = new RelationshipDTO(
                UUID.randomUUID(), PERSON_ID, OTHER_ID,
                "Alice Müller", null, null,
                "Bob Müller", null, null,
                RelationType.PARENT_OF, null, DatePrecision.UNKNOWN, null, DatePrecision.UNKNOWN, null);
        when(relationshipService.addRelationship(any(), any())).thenReturn(created);

        mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"PARENT_OF\"}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.relationType").value("PARENT_OF"));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void deleteRelationship_returns204_for_WRITE_ALL_user() throws Exception {
        UUID relId = UUID.randomUUID();
        doNothing().when(relationshipService).deleteRelationship(any(), any());

        mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, relId).with(csrf()))
                .andExpect(status().isNoContent());
    }

    // ─── PUT /api/persons/{id}/relationships/{relId} ──────────────────────────

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void updateRelationship_returns200_with_RelationshipDTO_for_WRITE_ALL_user() throws Exception {
        UUID relId = UUID.randomUUID();
        RelationshipDTO updated = new RelationshipDTO(
                relId, PERSON_ID, OTHER_ID,
                "Alice Müller", null, null,
                "Bob Müller", null, null,
                RelationType.SPOUSE_OF, null, DatePrecision.UNKNOWN, null, DatePrecision.UNKNOWN, null);
        when(relationshipService.updateRelationship(any(), any(), any())).thenReturn(updated);

        mockMvc.perform(put("/api/persons/{id}/relationships/{relId}", PERSON_ID, relId).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"SPOUSE_OF\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.relationType").value("SPOUSE_OF"));
    }

    @Test
    void updateRelationship_returns401_whenUnauthenticated_and_does_not_touch_service() throws Exception {
        mockMvc.perform(put("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"SPOUSE_OF\"}"))
                .andExpect(status().isUnauthorized());
        verify(relationshipService, never()).updateRelationship(any(), any(), any());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void updateRelationship_returns403_for_READ_ALL_only_user() throws Exception {
        mockMvc.perform(put("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"SPOUSE_OF\"}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void updateRelationship_returns400_when_relationType_is_unknown_value() throws Exception {
        mockMvc.perform(put("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID()).with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"NOT_A_REAL_TYPE\"}"))
                .andExpect(status().isBadRequest());
    }
}
