package dev.ksan.etfoglasiserver.controller;

import dev.ksan.etfoglasiserver.config.SecurityConfig;
import dev.ksan.etfoglasiserver.dto.JwtResponseDTO;
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
import dev.ksan.etfoglasiserver.dto.UserDTO;
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
import dev.ksan.etfoglasiserver.model.User;
import dev.ksan.etfoglasiserver.service.JWTService;
import dev.ksan.etfoglasiserver.service.MyUserDetailsService;
import dev.ksan.etfoglasiserver.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;

import dev.ksan.etfoglasiserver.util.SubjectLoader;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
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.Optional;
import java.util.UUID;

import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
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.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerTest {

    @Autowired
    MockMvc mvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockitoBean
    SubjectLoader subjectLoader;

    @MockitoBean
    UserService userService;

    @MockitoBean
    JWTService jwtService;

    @MockitoBean
    MyUserDetailsService userDetailsService;

    private static final String EMAIL = "test@example.com";

    @Test
    void login_validCredentials_returns200WithToken() throws Exception {
        JwtResponseDTO response = new JwtResponseDTO("jwt-token", EMAIL, "user-id", "OK");
        when(userService.verify(any(UserLoginDTO.class))).thenReturn(response);

        mvc.perform(post("/api/login")
                        .contentType(APPLICATION_JSON)
                        .content("""
                    {"email":"test@example.com","password":"correct"}
                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.token").value("jwt-token"))
                .andExpect(jsonPath("$.email").value(EMAIL));
    }

    @Test
    void login_badCredentials_returns400() throws Exception {
        when(userService.verify(any()))
                .thenThrow(new RuntimeException("bad credentials"));

        mvc.perform(post("/api/login")
                        .contentType(APPLICATION_JSON)
                        .content("""
                    {"email":"test@example.com","password":"wrong"}
                """))
                .andExpect(status().isBadRequest());
    }

    @Test
    void login_nullResponse_returns400() throws Exception {
        when(userService.verify(any())).thenReturn(null);

        mvc.perform(post("/api/login")
                        .contentType(APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(
                                new UserLoginDTO("x@x.com", "p"))))
                .andExpect(status().isBadRequest());
    }

    @Test
    void register_newUser_returns201() throws Exception {
        User user = new User();
        user.setEmail(EMAIL);

        when(userService.register(any()))
                .thenReturn(new UserDTO(user));

        mvc.perform(post("/api/register")
                        .contentType(APPLICATION_JSON)
                        .content("""
                    {"email":"test@example.com","password":"pass123"}
                """))
                .andExpect(status().isCreated());
    }

    @Test
    void register_duplicateEmail_returns400() throws Exception {
        when(userService.register(any()))
                .thenThrow(new RuntimeException("Email already exists"));

        mvc.perform(post("/api/register")
                        .contentType(APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(
                                new UserCreationDTO("dupe@example.com", "pass"))))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = EMAIL)
    void getUser_authenticated_returns200() throws Exception {
        User user = new User();
        user.setEmail(EMAIL);

        when(userService.getAccountByEmail(EMAIL))
                .thenReturn(Optional.of(user));

        mvc.perform(get("/api/users/me"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value(EMAIL));
    }

    @Test
    void getUser_unauthenticated_returns401or403() throws Exception {
        mvc.perform(get("/api/users/me"))
                .andExpect(status().is(anyOf(equalTo(401), equalTo(403))));
    }

    @Test
    @WithMockUser(username = EMAIL)
    void getUser_notFound_returns404() throws Exception {
        when(userService.getAccountByEmail(EMAIL))
                .thenReturn(Optional.empty());

        mvc.perform(get("/api/users/me"))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(username = EMAIL)
    void updateUser_validBody_returns200() throws Exception {
        User user = new User();
        user.setEmail("new@example.com");

        when(userService.updateUser(eq(EMAIL), any()))
                .thenReturn(new UserDTO(user));

        mvc.perform(put("/api/users/me")
                        .contentType(APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(
                                new UserCreationDTO("new@example.com", "newpass"))))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value("new@example.com"));
    }

    @Test
    @WithMockUser(username = EMAIL)
    void addSubject_validId_returns200() throws Exception {
        UUID subjectId = UUID.randomUUID();

        User user = new User();
        user.setEmail(EMAIL);

        when(userService.addSubject(EMAIL, subjectId))
                .thenReturn(new UserDTO(user));

        mvc.perform(put("/api/users/me/subjects/{id}", subjectId))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = EMAIL)
    void removeSubject_validId_returns200() throws Exception {
        UUID subjectId = UUID.randomUUID();

        User user = new User();
        user.setEmail(EMAIL);

        when(userService.removeSubject(EMAIL, subjectId))
                .thenReturn(new UserDTO(user));

        mvc.perform(delete("/api/users/me/subjects/{id}", subjectId))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = EMAIL)
    void deleteUser_authenticated_returns204() throws Exception {
        doNothing().when(userService).deleteUser(EMAIL);

        mvc.perform(delete("/api/users/me"))
                .andExpect(status().isNoContent());
    }
}