package com.codebyte.api.auth;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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.config.AppProperties;
import com.codebyte.api.support.AbstractPostgresIT;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
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.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

/** Authorization matrix: anonymous, valid, expired, tampered, wrong-issuer, and missing-role. */
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class AuthorizationMatrixIT extends AbstractPostgresIT {

    @Autowired private MockMvc mockMvc;
    @Autowired private JwtEncoder jwtEncoder;
    @Autowired private AppProperties properties;
    @Autowired private UserRepository userRepository;

    private UUID userId;

    @BeforeEach
    void seedUser() {
        User user = new User("matrix@example.com", "hash");
        user.addRole("ROLE_USER");
        user.activate(Instant.now());
        userId = userRepository.saveAndFlush(user).getId();
    }

    private String token(String issuer, String audience, Instant expiresAt, List<String> roles) {
        JwtClaimsSet claims =
                JwtClaimsSet.builder()
                        .issuer(issuer)
                        .audience(List.of(audience))
                        .subject(userId.toString())
                        .issuedAt(expiresAt.minusSeconds(600))
                        .expiresAt(expiresAt)
                        .claim("roles", roles)
                        .build();
        return jwtEncoder
                .encode(
                        JwtEncoderParameters.from(
                                JwsHeader.with(MacAlgorithm.HS256).build(), claims))
                .getTokenValue();
    }

    private String validToken() {
        return token(
                properties.jwt().issuer(),
                properties.jwt().audience(),
                Instant.now().plusSeconds(900),
                List.of("ROLE_USER"));
    }

    @Test
    void anonymous_request_to_protected_endpoint_is_unauthorized() throws Exception {
        mockMvc.perform(get("/api/v1/users/me"))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code", Matchers.is("UNAUTHENTICATED")));
        mockMvc.perform(get("/api/v1/items")).andExpect(status().isUnauthorized());
    }

    @Test
    void valid_token_is_authorized() throws Exception {
        mockMvc.perform(get("/api/v1/users/me").header("Authorization", "Bearer " + validToken()))
                .andExpect(status().isOk());
    }

    @Test
    void expired_token_is_unauthorized() throws Exception {
        // Beyond the decoder's default 60s clock-skew allowance.
        String expired =
                token(
                        properties.jwt().issuer(),
                        properties.jwt().audience(),
                        Instant.now().minusSeconds(300),
                        List.of("ROLE_USER"));
        mockMvc.perform(get("/api/v1/users/me").header("Authorization", "Bearer " + expired))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void tampered_token_is_unauthorized() throws Exception {
        // Flip the first character of the signature segment (its high bits, which always affect the
        // signature bytes — unlike the final char, whose low bits are padding).
        String valid = validToken();
        String[] parts = valid.split("\\.");
        char first = parts[2].charAt(0);
        parts[2] = (first == 'A' ? 'B' : 'A') + parts[2].substring(1);
        String tampered = parts[0] + "." + parts[1] + "." + parts[2];
        mockMvc.perform(get("/api/v1/users/me").header("Authorization", "Bearer " + tampered))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void wrong_issuer_token_is_unauthorized() throws Exception {
        String wrongIssuer =
                token(
                        "evil-issuer",
                        properties.jwt().audience(),
                        Instant.now().plusSeconds(900),
                        List.of("ROLE_USER"));
        mockMvc.perform(get("/api/v1/users/me").header("Authorization", "Bearer " + wrongIssuer))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void authenticated_but_missing_role_is_forbidden_on_items() throws Exception {
        String noRole =
                token(
                        properties.jwt().issuer(),
                        properties.jwt().audience(),
                        Instant.now().plusSeconds(900),
                        List.of());
        mockMvc.perform(get("/api/v1/items").header("Authorization", "Bearer " + noRole))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.code", Matchers.is("ACCESS_DENIED")));
    }
}
