package com.codebyte.api.auth;

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 com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.UserRepository;
import com.codebyte.api.support.AbstractPostgresIT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.icegreen.greenmail.junit5.GreenMailExtension;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;
import java.time.Instant;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

/** Password-reset and activation-resend flows over HTTP with real Postgres and SMTP (GreenMail). */
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class PasswordResetIT extends AbstractPostgresIT {

    private static final String EMAIL = "reset@example.com";
    private static final String OLD_PASSWORD = "old valid password";
    private static final String NEW_PASSWORD = "brand new password";
    private static final Pattern RESET_TOKEN = Pattern.compile("reset token is: (\\S+)");
    private static final Pattern ACTIVATION_TOKEN = Pattern.compile("activation token is: (\\S+)");

    @RegisterExtension
    static final GreenMailExtension GREENMAIL =
            new GreenMailExtension(ServerSetupTest.SMTP).withPerMethodLifecycle(true);

    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper objectMapper;
    @Autowired private UserRepository userRepository;
    @Autowired private PasswordEncoder passwordEncoder;

    @DynamicPropertySource
    static void mailProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.mail.host", () -> "127.0.0.1");
        registry.add("spring.mail.port", () -> ServerSetupTest.SMTP.getPort());
    }

    @BeforeEach
    void seedActivatedUser() {
        User user = new User(EMAIL, passwordEncoder.encode(OLD_PASSWORD));
        user.addRole("ROLE_USER");
        user.activate(Instant.now());
        userRepository.saveAndFlush(user);
    }

    private String login(String password) throws Exception {
        var result =
                mockMvc.perform(
                                post("/api/v1/auth/login")
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(
                                                "{\"email\":\"%s\",\"password\":\"%s\"}"
                                                        .formatted(EMAIL, password)))
                        .andExpect(status().isOk())
                        .andReturn();
        return objectMapper
                .readTree(result.getResponse().getContentAsString())
                .get("refreshToken")
                .asText();
    }

    private String extract(Pattern pattern) throws Exception {
        Matcher matcher =
                pattern.matcher(GreenMailUtil.getBody(GREENMAIL.getReceivedMessages()[0]));
        matcher.find();
        return matcher.group(1);
    }

    @Test
    void reset_changes_password_and_revokes_existing_sessions() throws Exception {
        String oldRefresh = login(OLD_PASSWORD);

        mockMvc.perform(
                        post("/api/v1/auth/password/forgot")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"email\":\"%s\"}".formatted(EMAIL)))
                .andExpect(status().isAccepted());

        String resetToken = extract(RESET_TOKEN);

        mockMvc.perform(
                        post("/api/v1/auth/password/reset")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        "{\"token\":\"%s\",\"newPassword\":\"%s\"}"
                                                .formatted(resetToken, NEW_PASSWORD)))
                .andExpect(status().isNoContent());

        // The pre-reset refresh token is now dead.
        mockMvc.perform(
                        post("/api/v1/auth/refresh")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"refreshToken\":\"%s\"}".formatted(oldRefresh)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("INVALID_REFRESH_TOKEN")));

        // Old password no longer works; new password does.
        mockMvc.perform(
                        post("/api/v1/auth/login")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        "{\"email\":\"%s\",\"password\":\"%s\"}"
                                                .formatted(EMAIL, OLD_PASSWORD)))
                .andExpect(status().isUnauthorized());
        login(NEW_PASSWORD); // succeeds (asserts 200 inside)
    }

    @Test
    void reset_token_is_single_use() throws Exception {
        mockMvc.perform(
                        post("/api/v1/auth/password/forgot")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"email\":\"%s\"}".formatted(EMAIL)))
                .andExpect(status().isAccepted());
        String resetToken = extract(RESET_TOKEN);
        String body =
                "{\"token\":\"%s\",\"newPassword\":\"%s\"}".formatted(resetToken, NEW_PASSWORD);

        mockMvc.perform(
                        post("/api/v1/auth/password/reset")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(body))
                .andExpect(status().isNoContent());
        // Reusing the consumed token fails.
        mockMvc.perform(
                        post("/api/v1/auth/password/reset")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code", Matchers.is("TOKEN_INVALID")));
    }

    @Test
    void forgot_is_accepted_even_for_unknown_email() throws Exception {
        mockMvc.perform(
                        post("/api/v1/auth/password/forgot")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"email\":\"nobody@example.com\"}"))
                .andExpect(status().isAccepted());
    }

    @Test
    void resend_activation_emails_a_fresh_token_for_unactivated_user() throws Exception {
        User pending = new User("pending@example.com", passwordEncoder.encode(OLD_PASSWORD));
        pending.addRole("ROLE_USER");
        userRepository.saveAndFlush(pending);

        mockMvc.perform(
                        post("/api/v1/auth/activation/resend")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"email\":\"pending@example.com\"}"))
                .andExpect(status().isAccepted());

        String activationToken = extract(ACTIVATION_TOKEN);
        mockMvc.perform(
                        post("/api/v1/auth/activate")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"token\":\"%s\"}".formatted(activationToken)))
                .andExpect(status().isOk());
    }
}
