package org.raddatz.familienarchiv.auth;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.support.TransactionTemplate;
import software.amazon.awssdk.services.s3.S3Client;

import java.time.Instant;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Integration test for {@link JdbcSessionRevocationAdapter} that verifies
 * session rows are actually written to / removed from the {@code spring_session}
 * table backed by a real PostgreSQL container.
 *
 * <p>Sessions are inserted via raw JDBC to avoid the module-access restriction on
 * {@code JdbcIndexedSessionRepository.JdbcSession}. The {@link SessionRevocationPort}
 * bean injected here is the real {@link JdbcSessionRevocationAdapter} wired by Spring.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class JdbcSessionRevocationAdapterIntegrationTest {

    @MockitoBean S3Client s3Client;

    @Autowired SessionRevocationPort adapter;
    @Autowired JdbcTemplate jdbcTemplate;
    @Autowired TransactionTemplate transactionTemplate;

    private static final String PRINCIPAL = "revocation-it@test.de";

    @BeforeEach
    void clearSessions() {
        // spring_session_attributes cascades on delete
        transactionTemplate.execute(status -> {
            jdbcTemplate.update("DELETE FROM spring_session");
            return null;
        });
    }

    // ── helper ─────────────────────────────────────────────────────────────────

    /**
     * Inserts a minimal {@code spring_session} row attributed to {@value #PRINCIPAL}
     * and returns its opaque primary-key ID (the value the repository uses as the
     * session identifier, not the {@code SESSION_ID} column which holds the public token).
     *
     * <p>Column layout mirrors the Flyway-managed schema shipped with the app:
     * PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL,
     * EXPIRY_TIME, PRINCIPAL_NAME.
     */
    /**
     * Inserts a persisted session row for {@value #PRINCIPAL} and returns the
     * {@code SESSION_ID} column value — this is the opaque identifier that
     * {@link JdbcIndexedSessionRepository} uses as the session's public key
     * (returned by {@code JdbcSession.getId()} and expected by
     * {@link JdbcIndexedSessionRepository#deleteById}).
     *
     * <p>The inserts run inside a {@link TransactionTemplate} so the rows are
     * committed before {@code findByPrincipalName} opens its own transaction and
     * can see the data via Read Committed isolation.
     */
    private String insertSession() {
        String primaryId = UUID.randomUUID().toString();
        // SESSION_ID is the value used by JdbcSession.getId() and findByPrincipalName map keys.
        String sessionId  = UUID.randomUUID().toString();
        long   now        = Instant.now().toEpochMilli();
        long   expiry     = now + 8L * 3600 * 1000; // 8-hour TTL
        transactionTemplate.execute(status -> {
            jdbcTemplate.update("""
                    INSERT INTO spring_session
                        (PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME,
                         MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME)
                    VALUES (?, ?, ?, ?, ?, ?, ?)
                    """,
                    primaryId, sessionId, now, now, 28800, expiry, PRINCIPAL);
            // Spring Session's listSessionsByPrincipalName query joins spring_session_attributes;
            // insert a minimal attribute row so the session appears in the result set.
            jdbcTemplate.update("""
                    INSERT INTO spring_session_attributes
                        (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
                    VALUES (?, ?, ?)
                    """,
                    primaryId, "test_attr", new byte[]{0});
            return null;
        });
        return sessionId;  // the public key used by JdbcSession.getId() and deleteById()
    }

    // ── tests ──────────────────────────────────────────────────────────────────

    @Test
    void revokeAllSessions_removes_every_row_from_spring_session_table() {
        insertSession();
        insertSession();

        int count = adapter.revokeAllSessions(PRINCIPAL);

        assertThat(count).isEqualTo(2);
        assertThat(jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM spring_session WHERE PRINCIPAL_NAME = ?",
                Long.class, PRINCIPAL))
            .isZero();
    }

    @Test
    void revokeOtherSessions_deletes_non_current_rows_and_keeps_current_session() {
        String keepId = insertSession();
        insertSession();
        insertSession();

        int count = adapter.revokeOtherSessions(keepId, PRINCIPAL);

        assertThat(count).isEqualTo(2);
        // The current session row must still be present (keyed by SESSION_ID)
        assertThat(jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM spring_session WHERE SESSION_ID = ?",
                Long.class, keepId))
            .isEqualTo(1L);
        // The total for this principal is now exactly 1
        assertThat(jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM spring_session WHERE PRINCIPAL_NAME = ?",
                Long.class, PRINCIPAL))
            .isEqualTo(1L);
    }
}
