package org.raddatz.familienarchiv.dashboard;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.audit.ActivityFeedRow;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditLogQueryRepository;
import org.raddatz.familienarchiv.config.FlywayConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;

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

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
@Transactional
class AuditLogQueryRepositoryRolledUpTest {

    static final UUID USER_ID = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd");
    static final UUID OTHER_USER_ID = UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc");
    static final UUID DOC_ID = UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
    static final UUID OTHER_DOC_ID = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");

    static final List<String> ALL_ELIGIBLE_KINDS =
            AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList();

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Autowired AuditLogQueryRepository auditLogQueryRepository;
    @Autowired JdbcTemplate jdbcTemplate;

    private NamedParameterJdbcTemplate named() {
        return new NamedParameterJdbcTemplate(jdbcTemplate);
    }

    private void insertUserAndDocs() {
        jdbcTemplate.update(
                "INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
                USER_ID, "rollup-" + USER_ID + "@test.com");
        jdbcTemplate.update(
                "INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
                OTHER_USER_ID, "rollup-" + OTHER_USER_ID + "@test.com");
        jdbcTemplate.update(
                "INSERT INTO documents (id, title, original_filename, status) VALUES (?, 'Brief A', 'a.pdf', 'PLACEHOLDER')",
                DOC_ID);
        jdbcTemplate.update(
                "INSERT INTO documents (id, title, original_filename, status) VALUES (?, 'Brief B', 'b.pdf', 'PLACEHOLDER')",
                OTHER_DOC_ID);
    }

    private void insertAuditEvent(UUID actorId, UUID docId, String kind, Instant happenedAt) {
        insertAuditEvent(actorId, docId, kind, happenedAt, Map.of());
    }

    private void insertAuditEvent(UUID actorId, UUID docId, String kind, Instant happenedAt, Map<String, String> payload) {
        String payloadJson;
        try {
            payloadJson = payload.isEmpty() ? null : MAPPER.writeValueAsString(payload);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        MapSqlParameterSource params = new MapSqlParameterSource()
                .addValue("kind", kind)
                .addValue("actor", actorId)
                .addValue("doc", docId)
                .addValue("t", OffsetDateTime.ofInstant(happenedAt, java.time.ZoneOffset.UTC))
                .addValue("payload", payloadJson, java.sql.Types.OTHER);
        named().update(
                "INSERT INTO audit_log (kind, actor_id, document_id, happened_at, payload) "
                        + "VALUES (:kind, :actor, :doc, :t, :payload::jsonb)",
                params);
    }

    private void insertReplyNotification(UUID recipientId, UUID docId, UUID commentId) {
        jdbcTemplate.update(
                "INSERT INTO notifications (recipient_id, type, document_id, reference_id) VALUES (?, 'REPLY', ?, ?)",
                recipientId, docId, commentId);
    }

    @Test
    void rolledUpFeed_combines_same_actor_same_doc_within_2h() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T09:00:00Z");
        for (int i = 0; i < 20; i++) {
            insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L));
        }

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        ActivityFeedRow row = rows.get(0);
        assertThat(row.getKind()).isEqualTo("TEXT_SAVED");
        assertThat(row.getDocumentId()).isEqualTo(DOC_ID);
        assertThat(row.getCount()).isEqualTo(20);
        assertThat(row.getHappenedAt()).isEqualTo(base);
        assertThat(row.getHappenedAtUntil()).isEqualTo(base.plusSeconds(19 * 480L));
    }

    @Test
    void rolledUpFeed_splits_at_2h_boundary() {
        insertUserAndDocs();
        Instant sessionOneStart = Instant.parse("2026-04-20T08:00:00Z");
        Instant sessionOneLast = sessionOneStart.plusSeconds(600);
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionOneStart);
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionOneLast);
        Instant sessionTwoStart = sessionOneLast.plusSeconds(2L * 60L * 60L + 60L);
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart);
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart.plusSeconds(300));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(2);
        assertThat(rows.get(0).getCount()).isEqualTo(2);
        assertThat(rows.get(0).getHappenedAt()).isEqualTo(sessionTwoStart);
        assertThat(rows.get(1).getCount()).isEqualTo(2);
        assertThat(rows.get(1).getHappenedAt()).isEqualTo(sessionOneStart);
    }

    @Test
    void rolledUpFeed_has_no_hard_cap_on_long_session() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T06:00:00Z");
        for (int i = 0; i < 30; i++) {
            insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(i * 60L * 30L));
        }

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getCount()).isEqualTo(30);
        assertThat(rows.get(0).getHappenedAt()).isEqualTo(base);
        assertThat(rows.get(0).getHappenedAtUntil()).isEqualTo(base.plusSeconds(29 * 60L * 30L));
    }

    @Test
    void rolledUpFeed_never_rolls_up_COMMENT_ADDED_or_MENTION_CREATED() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T10:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base);
        insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(60));
        insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(120));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(3);
        assertThat(rows).allSatisfy(r -> {
            assertThat(r.getKind()).isEqualTo("COMMENT_ADDED");
            assertThat(r.getCount()).isEqualTo(1);
            assertThat(r.getHappenedAtUntil()).isNull();
        });
    }

    @Test
    void rolledUpFeed_excludes_non_eligible_kinds() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T12:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "STATUS_CHANGED", base);
        insertAuditEvent(USER_ID, DOC_ID, "METADATA_UPDATED", base.plusSeconds(60));
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(120));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
    }

    @Test
    void rolledUpFeed_exposes_count_and_happenedAtUntil_on_singletons_and_rollups() {
        insertUserAndDocs();
        Instant rollupStart = Instant.parse("2026-04-20T11:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", rollupStart);
        insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(300));
        insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(900));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(2);
        assertThat(rows).anySatisfy(r -> {
            assertThat(r.getDocumentId()).isEqualTo(DOC_ID);
            assertThat(r.getCount()).isEqualTo(2);
            assertThat(r.getHappenedAt()).isEqualTo(rollupStart);
            assertThat(r.getHappenedAtUntil()).isEqualTo(rollupStart.plusSeconds(300));
        });
        assertThat(rows).anySatisfy(r -> {
            assertThat(r.getDocumentId()).isEqualTo(OTHER_DOC_ID);
            assertThat(r.getCount()).isEqualTo(1);
            assertThat(r.getHappenedAt()).isEqualTo(rollupStart.plusSeconds(900));
            assertThat(r.getHappenedAtUntil()).isNull();
        });
    }

    @Test
    void youParticipated_is_true_when_user_has_reply_notification_for_comment() {
        insertUserAndDocs();
        UUID commentId = UUID.randomUUID();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
        insertReplyNotification(USER_ID, DOC_ID, commentId);

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).anySatisfy(r ->
                assertThat(r.isYouParticipated()).isTrue()
        );
    }

    @Test
    void youParticipated_is_false_for_comment_with_no_reply_notification() {
        insertUserAndDocs();
        UUID commentId = UUID.randomUUID();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).allSatisfy(r ->
                assertThat(r.isYouParticipated()).isFalse()
        );
    }

    @Test
    void youParticipated_is_false_when_comment_added_has_no_commentId_in_payload() {
        insertUserAndDocs();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of());

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).allSatisfy(r ->
                assertThat(r.isYouParticipated()).isFalse()
        );
    }

    @Test
    void youParticipated_is_false_when_reply_notification_belongs_to_other_user() {
        insertUserAndDocs();
        UUID commentId = UUID.randomUUID();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
        insertReplyNotification(OTHER_USER_ID, DOC_ID, commentId);

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).allSatisfy(r ->
                assertThat(r.isYouParticipated()).isFalse()
        );
    }

    @Test
    void youMentioned_is_true_when_mention_created_payload_matches_current_user() {
        insertUserAndDocs();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "MENTION_CREATED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", USER_ID.toString()));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).anySatisfy(r ->
                assertThat(r.isYouMentioned()).isTrue()
        );
    }

    @Test
    void rolledUpFeed_exposes_commentId_for_COMMENT_ADDED_events() {
        insertUserAndDocs();
        UUID commentId = UUID.randomUUID();
        insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getCommentId()).isEqualTo(commentId);
    }

    @Test
    void rolledUpFeed_exposes_commentId_for_MENTION_CREATED_events() {
        insertUserAndDocs();
        UUID commentId = UUID.randomUUID();
        insertAuditEvent(OTHER_USER_ID, DOC_ID, "MENTION_CREATED",
                Instant.parse("2026-04-20T10:00:00Z"),
                Map.of("commentId", commentId.toString(), "mentionedUserId", USER_ID.toString()));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getCommentId()).isEqualTo(commentId);
    }

    @Test
    void rolledUpFeed_commentId_is_null_for_non_comment_kinds() {
        insertUserAndDocs();
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("blockId", "ccc", "pageNumber", "1"));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getCommentId()).isNull();
    }

    @Test
    void youMentioned_is_false_when_mention_created_payload_targets_different_user() {
        insertUserAndDocs();
        insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED",
                Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", OTHER_USER_ID.toString()));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);

        assertThat(rows).allSatisfy(r ->
                assertThat(r.isYouMentioned()).isFalse()
        );
    }

    // ─── kinds filter ─────────────────────────────────────────────────────────

    @Test
    void rolledUpFeed_with_single_kind_returns_only_that_kind() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T10:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
        insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
                USER_ID.toString(), 40, List.of("FILE_UPLOADED"));

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getKind()).isEqualTo("FILE_UPLOADED");
    }

    @Test
    void rolledUpFeed_with_multiple_kinds_returns_union() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T10:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
        insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
        insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
                USER_ID.toString(), 40, List.of("TEXT_SAVED", "FILE_UPLOADED"));

        assertThat(rows).hasSize(2);
        assertThat(rows).extracting(ActivityFeedRow::getKind)
                .containsExactlyInAnyOrder("TEXT_SAVED", "FILE_UPLOADED");
    }

    @Test
    void rolledUpFeed_with_default_returns_all_six_eligible_kinds() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T10:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
        insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
        insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120));
        insertAuditEvent(USER_ID, DOC_ID, "BLOCK_REVIEWED", base.plusSeconds(7300));
        insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(7360));
        insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED", base.plusSeconds(7420));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
                USER_ID.toString(), 40,
                List.of("TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED",
                        "BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED"));

        assertThat(rows).hasSize(6);
    }

    @Test
    void rolledUpFeed_excludes_rows_not_in_filter_set() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T10:00:00Z");
        insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
        insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
                USER_ID.toString(), 40, List.of("TEXT_SAVED"));

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
    }

    @Test
    void rolledUpFeed_rollup_still_works_when_kind_set_is_filtered_to_single_rollable_kind() {
        insertUserAndDocs();
        Instant base = Instant.parse("2026-04-20T09:00:00Z");
        for (int i = 0; i < 10; i++) {
            insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L));
        }
        insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(20));

        List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
                USER_ID.toString(), 40, List.of("TEXT_SAVED"));

        assertThat(rows).hasSize(1);
        assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
        assertThat(rows.get(0).getCount()).isEqualTo(10);
    }
}
