package com.codebyte.api.auth;

import static org.assertj.core.api.Assertions.assertThat;
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 com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.UserRepository;
import com.codebyte.api.support.AbstractPostgresIT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;

/** Full auth journey: login → /users/me → /items → refresh → reuse detection → chain revoked. */
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class AuthJourneyIT extends AbstractPostgresIT {

    private static final String EMAIL = "journey@example.com";
    private static final String PASSWORD = "correct horse battery";

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

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

    private JsonNode login() throws Exception {
        MvcResult 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());
    }

    private JsonNode refresh(String refreshToken) throws Exception {
        MvcResult result =
                mockMvc.perform(
                                post("/api/v1/auth/refresh")
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(
                                                "{\"refreshToken\":\"%s\"}"
                                                        .formatted(refreshToken)))
                        .andExpect(status().isOk())
                        .andReturn();
        return objectMapper.readTree(result.getResponse().getContentAsString());
    }

    @Test
    void full_journey_with_refresh_rotation_and_reuse_detection() throws Exception {
        JsonNode tokens = login();
        String access = tokens.get("accessToken").asText();
        String refresh = tokens.get("refreshToken").asText();
        assertThat(tokens.get("expiresIn").asLong()).isPositive();

        // Access-token-protected endpoints.
        mockMvc.perform(get("/api/v1/users/me").header("Authorization", "Bearer " + access))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email", Matchers.is(EMAIL)))
                .andExpect(jsonPath("$.password").doesNotExist());
        mockMvc.perform(get("/api/v1/items").header("Authorization", "Bearer " + access))
                .andExpect(status().isOk());

        // Rotate the refresh token.
        JsonNode rotated = refresh(refresh);
        String newRefresh = rotated.get("refreshToken").asText();
        assertThat(newRefresh).isNotEqualTo(refresh);

        // Reusing the old (already-rotated) refresh token is detected.
        mockMvc.perform(
                        post("/api/v1/auth/refresh")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"refreshToken\":\"%s\"}".formatted(refresh)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("TOKEN_REUSE_DETECTED")));

        // Reuse detection revoked the whole chain: even the freshly-rotated token is now dead.
        mockMvc.perform(
                        post("/api/v1/auth/refresh")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"refreshToken\":\"%s\"}".formatted(newRefresh)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("INVALID_REFRESH_TOKEN")));
    }

    @Test
    void logout_revokes_only_that_refresh_token() throws Exception {
        String refresh = login().get("refreshToken").asText();

        mockMvc.perform(
                        post("/api/v1/auth/logout")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"refreshToken\":\"%s\"}".formatted(refresh)))
                .andExpect(status().isNoContent());

        // The revoked token can no longer be refreshed.
        mockMvc.perform(
                        post("/api/v1/auth/refresh")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"refreshToken\":\"%s\"}".formatted(refresh)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("INVALID_REFRESH_TOKEN")));
    }

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

        mockMvc.perform(
                        post("/api/v1/auth/login")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        "{\"email\":\"pending@example.com\",\"password\":\"%s\"}"
                                                .formatted(PASSWORD)))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.code", Matchers.is("ACCOUNT_NOT_ACTIVATED")));
    }

    @Test
    void login_with_wrong_password_returns_invalid_credentials() throws Exception {
        mockMvc.perform(
                        post("/api/v1/auth/login")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        "{\"email\":\"%s\",\"password\":\"wrong-password-here\"}"
                                                .formatted(EMAIL)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("INVALID_CREDENTIALS")));
    }
}
