package com.codebyte.api.auth.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;

import com.codebyte.api.auth.domain.OneTimeToken;
import com.codebyte.api.auth.domain.TokenType;
import com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.OneTimeTokenRepository;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class TokenServiceTest {

    @Mock private OneTimeTokenRepository repository;
    @InjectMocks private TokenService tokenService;

    private final User user = new User("user@example.com", "hash");

    private static String sha256Hex(String value) throws Exception {
        byte[] digest =
                MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(digest);
    }

    @Test
    void should_issue_hashed_single_use_token_with_ttl() throws Exception {
        String raw = tokenService.issue(user, TokenType.ACCOUNT_ACTIVATION, Duration.ofHours(1));

        ArgumentCaptor<OneTimeToken> captor = ArgumentCaptor.forClass(OneTimeToken.class);
        org.mockito.Mockito.verify(repository).save(captor.capture());
        OneTimeToken saved = captor.getValue();

        assertThat(raw).isNotBlank();
        assertThat(saved.getType()).isEqualTo(TokenType.ACCOUNT_ACTIVATION);
        assertThat(saved.getTokenHash()).hasSize(64).isEqualTo(sha256Hex(raw));
        assertThat(saved.isConsumed()).isFalse();
        assertThat(saved.getExpiresAt()).isAfter(Instant.now());
    }

    @Test
    void should_consume_valid_token_and_mark_it_used() throws Exception {
        String raw = "raw-token";
        OneTimeToken token =
                new OneTimeToken(
                        user,
                        sha256Hex(raw),
                        TokenType.ACCOUNT_ACTIVATION,
                        Instant.now().plusSeconds(60));
        when(repository.findByTokenHashAndType(sha256Hex(raw), TokenType.ACCOUNT_ACTIVATION))
                .thenReturn(Optional.of(token));

        User result = tokenService.consume(raw, TokenType.ACCOUNT_ACTIVATION);

        assertThat(result).isSameAs(user);
        assertThat(token.isConsumed()).isTrue();
    }

    @Test
    void should_reject_unknown_token() {
        when(repository.findByTokenHashAndType(
                        org.mockito.ArgumentMatchers.anyString(),
                        org.mockito.ArgumentMatchers.eq(TokenType.ACCOUNT_ACTIVATION)))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> tokenService.consume("nope", TokenType.ACCOUNT_ACTIVATION))
                .isInstanceOf(InvalidTokenException.class);
    }

    @Test
    void should_reject_already_consumed_token() throws Exception {
        String raw = "raw";
        OneTimeToken token =
                new OneTimeToken(
                        user,
                        sha256Hex(raw),
                        TokenType.ACCOUNT_ACTIVATION,
                        Instant.now().plusSeconds(60));
        token.setConsumedAt(Instant.now());
        when(repository.findByTokenHashAndType(sha256Hex(raw), TokenType.ACCOUNT_ACTIVATION))
                .thenReturn(Optional.of(token));

        assertThatThrownBy(() -> tokenService.consume(raw, TokenType.ACCOUNT_ACTIVATION))
                .isInstanceOf(InvalidTokenException.class);
    }

    @Test
    void should_reject_expired_token() throws Exception {
        String raw = "raw";
        OneTimeToken token =
                new OneTimeToken(
                        user,
                        sha256Hex(raw),
                        TokenType.ACCOUNT_ACTIVATION,
                        Instant.now().minusSeconds(1));
        when(repository.findByTokenHashAndType(sha256Hex(raw), TokenType.ACCOUNT_ACTIVATION))
                .thenReturn(Optional.of(token));

        assertThatThrownBy(() -> tokenService.consume(raw, TokenType.ACCOUNT_ACTIVATION))
                .isInstanceOf(InvalidTokenException.class);
    }

    @Test
    void should_invalidate_active_tokens() {
        tokenService.invalidateActive(user, TokenType.ACCOUNT_ACTIVATION);

        org.mockito.Mockito.verify(repository)
                .consumeAllActive(
                        org.mockito.ArgumentMatchers.eq(user),
                        org.mockito.ArgumentMatchers.eq(TokenType.ACCOUNT_ACTIVATION),
                        org.mockito.ArgumentMatchers.any(Instant.class));
    }
}
