package kz.konturai.parser.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class JwtServiceTest {

    private static final String TEST_SECRET = Base64.getEncoder().encodeToString(
            "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8));
    private static final String DEFAULT_SECRET =
            "dGVzdC1zZWNyZXQta2V5LWZvci1kZXZlbG9wbWVudC1vbmx5LWRvLW5vdC11c2UtaW4tcHJvZHVjdGlvbg==";

    private JwtService jwtService;

    @BeforeEach
    void setUp() {
        jwtService = new JwtService(TEST_SECRET);
    }

    @Test
    void parseAndValidateShouldReturnClaimsForValidToken() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(
                "uid", 42L,
                "roles", "ADMIN, USER"));

        Claims claims = jwtService.parseAndValidate(token);

        assertEquals("qa@example.com", claims.getSubject());
        assertEquals(42L, claims.get("uid", Long.class));
        assertEquals("ADMIN, USER", claims.get("roles", String.class));
    }

    @Test
    void parseAndValidateShouldThrowForInvalidSignature() {
        String token = createToken(
                Base64.getEncoder().encodeToString("another-valid-secret-key-1234567".getBytes(StandardCharsets.UTF_8)),
                "qa@example.com",
                Instant.now().plusSeconds(3600),
                Map.of("uid", 99L));

        assertThrows(JwtException.class, () -> jwtService.parseAndValidate(token));
    }

    @Test
    void parseAndValidateShouldThrowForMalformedToken() {
        assertThrows(JwtException.class, () -> jwtService.parseAndValidate("not-a-jwt"));
    }

    @Test
    void constructorWithoutConfiguredSecretShouldUseDefaultSigningKey() {
        JwtService serviceWithDefaultKey = new JwtService("");
        String token = createToken(DEFAULT_SECRET, "fallback@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 7L));

        assertEquals("7", serviceWithDefaultKey.extractUserIdFromToken(token));
    }

    @Test
    void extractTokenFromHeaderShouldHandleBearerPrefixBlankAndRawToken() {
        assertNull(jwtService.extractTokenFromHeader(null));
        assertNull(jwtService.extractTokenFromHeader(" "));
        assertEquals("", jwtService.extractTokenFromHeader("Bearer "));
        assertEquals("sample-token", jwtService.extractTokenFromHeader("Bearer sample-token"));
        assertEquals("raw-token", jwtService.extractTokenFromHeader("raw-token"));
    }

    @Test
    void extractUserIdFromTokenShouldPreferUidClaim() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 12345L));

        assertEquals("12345", jwtService.extractUserIdFromToken(token));
    }

    @ParameterizedTest
    @MethodSource("legacyUserIdClaims")
    void extractUserIdFromTokenShouldUseLegacyClaims(String claimName, Object claimValue, String expectedValue) {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(claimName, claimValue));

        assertEquals(expectedValue, jwtService.extractUserIdFromToken(token));
    }

    @Test
    void extractUserIdFromTokenShouldUseSubjectAsLastFallback() {
        String token = createToken(TEST_SECRET, "subject@example.com", Instant.now().plusSeconds(3600), Map.of());

        assertEquals("subject@example.com", jwtService.extractUserIdFromToken(token));
    }

    @Test
    void extractUserIdFromTokenShouldReturnNullForExpiredToken() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().minusSeconds(60), Map.of("uid", 10L));

        assertNull(jwtService.extractUserIdFromToken(token));
    }

    @Test
    void extractUserIdFromTokenShouldReturnNullForMalformedToken() {
        assertNull(jwtService.extractUserIdFromToken("not-a-jwt"));
    }

    @Test
    void extractUserIdFromTokenShouldReturnNullForInvalidSignature() {
        String token = createToken(
                Base64.getEncoder().encodeToString("another-valid-secret-key-1234567".getBytes(StandardCharsets.UTF_8)),
                "qa@example.com",
                Instant.now().plusSeconds(3600),
                Map.of("uid", 123L));

        assertNull(jwtService.extractUserIdFromToken(token));
    }

    @Test
    void extractUserIdFromHeaderShouldExtractFromBearerToken() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 501L));

        assertEquals("501", jwtService.extractUserIdFromHeader("Bearer " + token));
    }

    @Test
    void extractEmailFromTokenShouldReturnSubject() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 12L));

        assertEquals("qa@example.com", jwtService.extractEmailFromToken(token));
    }

    @Test
    void extractEmailFromTokenShouldReturnNullForInvalidToken() {
        assertNull(jwtService.extractEmailFromToken("broken-token"));
        assertNull(jwtService.extractEmailFromToken(" "));
    }

    @Test
    void extractRolesFromTokenShouldSplitAndTrimRoleList() {
        String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(
                "uid", 1L,
                "roles", "ADMIN, USER , ,MANAGER"));

        assertEquals(List.of("ADMIN", "USER", "MANAGER"), jwtService.extractRolesFromToken(token));
    }

    @Test
    void extractRolesFromTokenShouldReturnEmptyListWhenRolesAreMissingOrTokenInvalid() {
        String tokenWithoutRoles = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 1L));

        assertTrue(jwtService.extractRolesFromToken(tokenWithoutRoles).isEmpty());
        assertTrue(jwtService.extractRolesFromToken("invalid").isEmpty());
        assertTrue(jwtService.extractRolesFromToken(null).isEmpty());
    }

    private static Stream<Arguments> legacyUserIdClaims() {
        return Stream.of(
                Arguments.of("userId", "legacy-user", "legacy-user"),
                Arguments.of("id", 55L, "55"),
                Arguments.of("user_id", "external-user", "external-user"));
    }

    private String createToken(String base64Secret, String subject, Instant expiration, Map<String, Object> customClaims) {
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
        Map<String, Object> claims = new HashMap<>(customClaims);

        var builder = Jwts.builder()
                .subject(subject)
                .issuedAt(Date.from(Instant.now().minusSeconds(30)))
                .expiration(Date.from(expiration))
                .signWith(key);

        claims.forEach(builder::claim);
        return builder.compact();
    }
}
