package com.cameleer.server.app.security;

import com.cameleer.server.core.security.InvalidTokenException;
import com.cameleer.server.core.security.JwtService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Unit tests for {@link JwtServiceImpl}.
 * No Spring context needed — implementations are constructed directly.
 */
class JwtServiceTest {

    private JwtService jwtService;

    @BeforeEach
    void setUp() {
        SecurityProperties props = new SecurityProperties();
        props.setAccessTokenExpiryMs(3_600_000);   // 1 hour
        props.setRefreshTokenExpiryMs(604_800_000); // 7 days
        jwtService = new JwtServiceImpl(props);
    }

    @Test
    void createAccessToken_returnsSignedJwtWithCorrectClaims() {
        String token = jwtService.createAccessToken("agent-1", "group-a");
        assertNotNull(token);
        assertFalse(token.isBlank());
        // JWT format: header.payload.signature
        assertEquals(3, token.split("\\.").length, "JWT should have 3 parts");
    }

    @Test
    void createAccessToken_canBeValidated() {
        String token = jwtService.createAccessToken("agent-1", "group-a");
        String agentId = jwtService.validateAndExtractAgentId(token);
        assertEquals("agent-1", agentId);
    }

    @Test
    void createRefreshToken_returnsSignedJwt() {
        String token = jwtService.createRefreshToken("agent-2", "group-b");
        assertNotNull(token);
        assertFalse(token.isBlank());
        assertEquals(3, token.split("\\.").length, "JWT should have 3 parts");
    }

    @Test
    void createRefreshToken_canBeValidatedWithRefreshMethod() {
        String token = jwtService.createRefreshToken("agent-2", "group-b");
        JwtService.JwtValidationResult result = jwtService.validateRefreshToken(token);
        assertEquals("agent-2", result.subject());
    }

    @Test
    void validateAndExtractAgentId_rejectsRefreshToken() {
        String refreshToken = jwtService.createRefreshToken("agent-3", "group-c");
        assertThrows(InvalidTokenException.class, () ->
                jwtService.validateAndExtractAgentId(refreshToken),
                "Access validation should reject refresh tokens");
    }

    @Test
    void validateRefreshToken_rejectsAccessToken() {
        String accessToken = jwtService.createAccessToken("agent-4", "group-d");
        assertThrows(InvalidTokenException.class, () ->
                jwtService.validateRefreshToken(accessToken),
                "Refresh validation should reject access tokens");
    }

    @Test
    void accessToken_rolesRoundTrip() {
        List<String> roles = List.of("ADMIN", "OPERATOR");
        String token = jwtService.createAccessToken("user:admin", "user", roles);
        JwtService.JwtValidationResult result = jwtService.validateAccessToken(token);
        assertEquals("user:admin", result.subject());
        assertEquals("user", result.application());
        assertEquals(roles, result.roles());
    }

    @Test
    void refreshToken_rolesRoundTrip() {
        List<String> roles = List.of("AGENT");
        String token = jwtService.createRefreshToken("agent-1", "default", roles);
        JwtService.JwtValidationResult result = jwtService.validateRefreshToken(token);
        assertEquals("agent-1", result.subject());
        assertEquals("default", result.application());
        assertEquals(roles, result.roles());
    }

    @Test
    void legacyToken_emptyRoles() {
        // Backward compat: tokens without explicit roles get empty list
        String token = jwtService.createAccessToken("agent-1", "default");
        JwtService.JwtValidationResult result = jwtService.validateAccessToken(token);
        assertEquals(List.of(), result.roles());
    }

    @Test
    void configuredJwtSecret_producesStableTokens() {
        SecurityProperties props = new SecurityProperties();
        props.setAccessTokenExpiryMs(3_600_000);
        props.setRefreshTokenExpiryMs(604_800_000);
        props.setJwtSecret("my-test-secret-that-is-at-least-32-bytes");
        JwtService svc1 = new JwtServiceImpl(props);
        JwtService svc2 = new JwtServiceImpl(props);

        String token = svc1.createAccessToken("agent-1", "default", List.of("AGENT"));
        // Token created by svc1 should be validatable by svc2 (same secret)
        JwtService.JwtValidationResult result = svc2.validateAccessToken(token);
        assertEquals("agent-1", result.subject());
        assertEquals(List.of("AGENT"), result.roles());
    }

    @Test
    void validateAndExtractAgentId_rejectsExpiredToken() {
        // Create a service with 0ms expiry to produce already-expired tokens
        SecurityProperties shortProps = new SecurityProperties();
        shortProps.setAccessTokenExpiryMs(0);
        shortProps.setRefreshTokenExpiryMs(604_800_000);
        JwtService shortLivedService = new JwtServiceImpl(shortProps);

        String token = shortLivedService.createAccessToken("agent-5", "group-e");
        assertThrows(InvalidTokenException.class, () ->
                jwtService.validateAndExtractAgentId(token),
                "Should reject expired tokens");
    }
}
