package com.cameleer.server.app.storage;

import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.cameleer.server.app.ClickHouseTestHelper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.testcontainers.clickhouse.ClickHouseContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.sql.Timestamp;
import java.time.Instant;

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

@Testcontainers
class ClickHouseAgentEventRepositoryIT {

    @Container
    static final ClickHouseContainer clickhouse =
            new ClickHouseContainer("clickhouse/clickhouse-server:24.12");

    private JdbcTemplate jdbc;
    private ClickHouseAgentEventRepository repo;

    @BeforeEach
    void setUp() throws Exception {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(clickhouse.getJdbcUrl());
        ds.setUsername(clickhouse.getUsername());
        ds.setPassword(clickhouse.getPassword());

        jdbc = new JdbcTemplate(ds);

        ClickHouseTestHelper.executeInitSql(jdbc);
        jdbc.execute("TRUNCATE TABLE agent_events");

        repo = new ClickHouseAgentEventRepository("default", jdbc);
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    /**
     * Insert a row with an explicit timestamp so tests can control ordering and ranges.
     */
    private void insertAt(String instanceId, String applicationId, String eventType, String detail, Instant ts) {
        jdbc.update(
                "INSERT INTO agent_events (tenant_id, instance_id, application_id, environment, event_type, detail, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)",
                "default", instanceId, applicationId, "default", eventType, detail, Timestamp.from(ts));
    }

    // ── Tests ─────────────────────────────────────────────────────────────────

    @Test
    void insert_writesEvent() {
        repo.insert("agent-1", "app-a", "default", "CONNECTED", "agent came online");

        Long count = jdbc.queryForObject(
                "SELECT count() FROM agent_events WHERE instance_id = 'agent-1'",
                Long.class);
        assertThat(count).isEqualTo(1);
    }

    @Test
    void queryPage_emptyTable_returnsEmptyPage() {
        com.cameleer.server.core.agent.AgentEventPage page =
                repo.queryPage(null, null, null, null, null, null, 10);
        assertThat(page.data()).isEmpty();
        assertThat(page.hasMore()).isFalse();
        assertThat(page.nextCursor()).isNull();
    }

    @Test
    void queryPage_boundary_noHasMoreWhenLimitEqualsRowCount() {
        Instant base = Instant.parse("2026-04-01T10:00:00Z");
        for (int i = 0; i < 3; i++) {
            insertAt("agent-1", "app-a", "TICK", "t" + i, base.plusSeconds(i));
        }
        com.cameleer.server.core.agent.AgentEventPage page =
                repo.queryPage(null, null, null, null, null, null, 3);
        assertThat(page.data()).hasSize(3);
        assertThat(page.hasMore()).isFalse();
        assertThat(page.nextCursor()).isNull();
    }

    @Test
    void queryPage_paginatesAcrossThreePages() {
        Instant base = Instant.parse("2026-04-01T10:00:00Z");
        for (int i = 0; i < 5; i++) {
            insertAt("agent-1", "app-a", "TICK", "t" + i, base.plusSeconds(i));
        }

        com.cameleer.server.core.agent.AgentEventPage p1 =
                repo.queryPage(null, null, null, null, null, null, 2);
        assertThat(p1.data()).hasSize(2);
        assertThat(p1.hasMore()).isTrue();
        assertThat(p1.nextCursor()).isNotBlank();
        assertThat(p1.data().get(0).detail()).isEqualTo("t4");
        assertThat(p1.data().get(1).detail()).isEqualTo("t3");

        com.cameleer.server.core.agent.AgentEventPage p2 =
                repo.queryPage(null, null, null, null, null, p1.nextCursor(), 2);
        assertThat(p2.data()).hasSize(2);
        assertThat(p2.hasMore()).isTrue();
        assertThat(p2.data().get(0).detail()).isEqualTo("t2");
        assertThat(p2.data().get(1).detail()).isEqualTo("t1");

        com.cameleer.server.core.agent.AgentEventPage p3 =
                repo.queryPage(null, null, null, null, null, p2.nextCursor(), 2);
        assertThat(p3.data()).hasSize(1);
        assertThat(p3.hasMore()).isFalse();
        assertThat(p3.nextCursor()).isNull();
        assertThat(p3.data().get(0).detail()).isEqualTo("t0");
    }

    @Test
    void queryPage_tiebreak_sameMillisecond_returnsAllRowsNoDuplicates() {
        Instant ts = Instant.parse("2026-04-01T10:00:00Z");
        insertAt("agent-a", "app-a", "TICK", "a", ts);
        insertAt("agent-b", "app-a", "TICK", "b", ts);
        insertAt("agent-c", "app-a", "TICK", "c", ts);

        java.util.Set<String> seen = new java.util.HashSet<>();
        String cursor = null;
        for (int page = 0; page < 10; page++) {
            com.cameleer.server.core.agent.AgentEventPage p =
                    repo.queryPage(null, null, null, null, null, cursor, 1);
            for (com.cameleer.server.core.agent.AgentEventRecord r : p.data()) {
                assertThat(seen.add(r.instanceId())).as("duplicate row returned: " + r.instanceId()).isTrue();
            }
            cursor = p.nextCursor();
            if (!p.hasMore()) break;
        }
        assertThat(seen).containsExactlyInAnyOrder("agent-a", "agent-b", "agent-c");
    }

    @Test
    void queryPage_malformedCursor_invalidTimestamp_throws() {
        String raw = "not-a-timestamp|agent-1";
        String cursor = java.util.Base64.getUrlEncoder().withoutPadding()
                .encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8));

        org.junit.jupiter.api.Assertions.assertThrows(
                IllegalArgumentException.class,
                () -> repo.queryPage(null, null, null, null, null, cursor, 10));
    }

    @Test
    void queryPage_malformedCursor_emptyInsertId_throws() {
        String raw = "2026-04-01T10:00:00Z|";
        String cursor = java.util.Base64.getUrlEncoder().withoutPadding()
                .encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8));

        org.junit.jupiter.api.Assertions.assertThrows(
                IllegalArgumentException.class,
                () -> repo.queryPage(null, null, null, null, null, cursor, 10));
    }
}
