package org.raddatz.familienarchiv.user;

import tools.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.user.InvitePrefillDTO;
import org.raddatz.familienarchiv.user.RegisterRequest;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.InviteToken;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.user.InviteService;
import org.raddatz.familienarchiv.user.PasswordResetService;
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.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(AuthController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class AuthControllerTest {

    @Autowired MockMvc mockMvc;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @MockitoBean PasswordResetService passwordResetService;
    @MockitoBean InviteService inviteService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    // ─── GET /api/auth/invite/{code} ──────────────────────────────────────────

    @Test
    void getInvitePrefill_returns200_withPrefillData_whenCodeValid() throws Exception {
        InviteToken token = InviteToken.builder()
                .code("ABCDE12345")
                .prefillFirstName("Helga")
                .prefillLastName("Muster")
                .prefillEmail("helga@muster.de")
                .build();
        when(inviteService.validateCode("ABCDE12345")).thenReturn(token);

        mockMvc.perform(get("/api/auth/invite/ABCDE12345"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.firstName").value("Helga"))
                .andExpect(jsonPath("$.lastName").value("Muster"))
                .andExpect(jsonPath("$.email").value("helga@muster.de"));
    }

    @Test
    void getInvitePrefill_returns404_whenCodeNotFound() throws Exception {
        when(inviteService.validateCode("UNKNOWN123"))
                .thenThrow(DomainException.notFound(ErrorCode.INVITE_NOT_FOUND, "not found"));

        mockMvc.perform(get("/api/auth/invite/UNKNOWN123"))
                .andExpect(status().isNotFound());
    }

    @Test
    void getInvitePrefill_returns409_whenTokenRevoked() throws Exception {
        when(inviteService.validateCode("REVOKED123"))
                .thenThrow(DomainException.conflict(ErrorCode.INVITE_REVOKED, "revoked"));

        mockMvc.perform(get("/api/auth/invite/REVOKED123"))
                .andExpect(status().isConflict());
    }

    @Test
    void getInvitePrefill_returns409_whenTokenExhausted() throws Exception {
        when(inviteService.validateCode("EXHAUST123"))
                .thenThrow(DomainException.conflict(ErrorCode.INVITE_EXHAUSTED, "exhausted"));

        mockMvc.perform(get("/api/auth/invite/EXHAUST123"))
                .andExpect(status().isConflict());
    }

    @Test
    void getInvitePrefill_returns410_whenTokenExpired() throws Exception {
        when(inviteService.validateCode("EXPIRED123"))
                .thenThrow(new DomainException(ErrorCode.INVITE_EXPIRED,
                        org.springframework.http.HttpStatus.GONE, "expired"));

        mockMvc.perform(get("/api/auth/invite/EXPIRED123"))
                .andExpect(status().isGone());
    }

    // ─── POST /api/auth/register ──────────────────────────────────────────────

    @Test
    void register_returns201_withCreatedUser_onHappyPath() throws Exception {
        AppUser user = AppUser.builder()
                .id(UUID.randomUUID())
                .email("new@test.com")
                .firstName("Max")
                .lastName("Muster")
                .build();
        when(inviteService.redeemInvite(any())).thenReturn(user);

        RegisterRequest req = new RegisterRequest();
        req.setCode("ABCDE12345");
        req.setEmail("new@test.com");
        req.setPassword("password123");
        req.setFirstName("Max");
        req.setLastName("Muster");

        mockMvc.perform(post("/api/auth/register").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.email").value("new@test.com"));
    }

    @Test
    void register_returns409_whenEmailAlreadyInUse() throws Exception {
        when(inviteService.redeemInvite(any()))
                .thenThrow(DomainException.conflict(ErrorCode.EMAIL_ALREADY_IN_USE, "already in use"));

        RegisterRequest req = new RegisterRequest();
        req.setCode("ABCDE12345");
        req.setEmail("dupe@test.com");
        req.setPassword("password123");

        mockMvc.perform(post("/api/auth/register").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isConflict());
    }

    @Test
    void register_returns400_whenPasswordTooShort() throws Exception {
        when(inviteService.redeemInvite(any()))
                .thenThrow(DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "too short"));

        RegisterRequest req = new RegisterRequest();
        req.setCode("ABCDE12345");
        req.setEmail("new@test.com");
        req.setPassword("abc");

        mockMvc.perform(post("/api/auth/register").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isBadRequest());
    }

    @Test
    void register_returns404_whenInviteCodeNotFound() throws Exception {
        when(inviteService.redeemInvite(any()))
                .thenThrow(DomainException.notFound(ErrorCode.INVITE_NOT_FOUND, "not found"));

        RegisterRequest req = new RegisterRequest();
        req.setCode("INVALID123");
        req.setEmail("new@test.com");
        req.setPassword("password123");

        mockMvc.perform(post("/api/auth/register").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isNotFound());
    }

    @Test
    void register_isPublic_noAuthRequired() throws Exception {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("pub@test.com").build();
        when(inviteService.redeemInvite(any())).thenReturn(user);

        RegisterRequest req = new RegisterRequest();
        req.setCode("ABCDE12345");
        req.setEmail("pub@test.com");
        req.setPassword("password123");

        // No WithMockUser — must still succeed (no auth challenge)
        mockMvc.perform(post("/api/auth/register").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isCreated());
    }
}
