package com.cameleer.server.app.storage;

import com.cameleer.server.app.search.ClickHouseSearchIndex;
import com.cameleer.server.core.ingestion.ChunkAccumulator;
import com.cameleer.server.core.ingestion.MergedExecution;
import com.cameleer.server.core.storage.DiagramStore;
import com.cameleer.server.core.search.ExecutionSummary;
import com.cameleer.server.core.search.SearchRequest;
import com.cameleer.server.core.search.SearchResult;
import com.cameleer.common.model.ExecutionChunk;
import com.cameleer.common.model.ExecutionStatus;
import com.cameleer.common.model.FlatProcessorRecord;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

@Testcontainers
class ClickHouseChunkPipelineIT {

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

    private JdbcTemplate jdbc;
    private ClickHouseExecutionStore executionStore;
    private ClickHouseSearchIndex searchIndex;
    private ChunkAccumulator accumulator;
    private List<MergedExecution> executionBuffer;
    private List<ChunkAccumulator.ProcessorBatch> processorBuffer;

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

        // Schema files were collapsed into clickhouse/init.sql.
        com.cameleer.server.app.ClickHouseTestHelper.executeInitSql(jdbc);
        jdbc.execute("TRUNCATE TABLE executions");
        jdbc.execute("TRUNCATE TABLE processor_executions");

        executionStore = new ClickHouseExecutionStore("default", jdbc);
        searchIndex = new ClickHouseSearchIndex("default", jdbc);

        executionBuffer = new ArrayList<>();
        processorBuffer = new ArrayList<>();
        DiagramStore noOpDiagramStore = org.mockito.Mockito.mock(DiagramStore.class);
        accumulator = new ChunkAccumulator("default", executionBuffer::add, processorBuffer::add,
                noOpDiagramStore, Duration.ofMinutes(5), id -> "default");
    }

    @Test
    void fullPipeline_chunkedIngestion_thenSearch() {
        Instant start = Instant.parse("2026-03-31T12:00:00Z");

        // Chunk 0: RUNNING with initial processors
        ExecutionChunk chunk0 = new ExecutionChunk();
        chunk0.setExchangeId("pipeline-1");
        chunk0.setApplicationId("order-service");
        chunk0.setInstanceId("pod-1");
        chunk0.setRouteId("order-route");
        chunk0.setCorrelationId("corr-1");
        chunk0.setStatus(ExecutionStatus.RUNNING);
        chunk0.setStartTime(start);
        chunk0.setEngineLevel("DEEP");
        chunk0.setAttributes(Map.of("orderId", "ORD-123"));
        chunk0.setChunkSeq(0);
        chunk0.setFinal(false);

        FlatProcessorRecord p1 = new FlatProcessorRecord(1, "log1", "log");
        p1.setStatus(ExecutionStatus.COMPLETED);
        p1.setStartTime(start);
        p1.setDurationMs(2L);

        FlatProcessorRecord p2 = new FlatProcessorRecord(2, "split1", "split");
        p2.setIterationSize(3);
        p2.setStatus(ExecutionStatus.COMPLETED);
        p2.setStartTime(start.plusMillis(2));
        p2.setDurationMs(100L);

        FlatProcessorRecord p3 = new FlatProcessorRecord(3, "to1", "to");
        p3.setParentSeq(2);
        p3.setParentProcessorId("split1");
        p3.setIteration(0);
        p3.setStatus(ExecutionStatus.COMPLETED);
        p3.setStartTime(start.plusMillis(5));
        p3.setDurationMs(30L);
        p3.setResolvedEndpointUri("http://inventory/api");
        p3.setInputBody("order ABC-123 check stock");
        p3.setOutputBody("stock available");

        chunk0.setProcessors(List.of(p1, p2, p3));
        accumulator.onChunk(chunk0);

        // Processors should be buffered immediately
        assertThat(processorBuffer).hasSize(1);
        assertThat(executionBuffer).isEmpty();

        // Chunk 1: COMPLETED (final)
        ExecutionChunk chunk1 = new ExecutionChunk();
        chunk1.setExchangeId("pipeline-1");
        chunk1.setApplicationId("order-service");
        chunk1.setInstanceId("pod-1");
        chunk1.setRouteId("order-route");
        chunk1.setCorrelationId("corr-1");
        chunk1.setStatus(ExecutionStatus.COMPLETED);
        chunk1.setStartTime(start);
        chunk1.setEndTime(start.plusMillis(750));
        chunk1.setDurationMs(750L);
        chunk1.setEngineLevel("DEEP");
        chunk1.setChunkSeq(1);
        chunk1.setFinal(true);

        FlatProcessorRecord p4 = new FlatProcessorRecord(4, "to1", "to");
        p4.setParentSeq(2);
        p4.setParentProcessorId("split1");
        p4.setIteration(1);
        p4.setStatus(ExecutionStatus.COMPLETED);
        p4.setStartTime(start.plusMillis(40));
        p4.setDurationMs(25L);
        p4.setResolvedEndpointUri("http://inventory/api");
        p4.setInputBody("order DEF-456 check stock");
        p4.setOutputBody("stock available");

        chunk1.setProcessors(List.of(p4));
        accumulator.onChunk(chunk1);

        assertThat(executionBuffer).hasSize(1);
        assertThat(processorBuffer).hasSize(2);

        // Flush to ClickHouse (simulating ExecutionFlushScheduler)
        executionStore.insertExecutionBatch(executionBuffer);
        for (ChunkAccumulator.ProcessorBatch batch : processorBuffer) {
            executionStore.insertProcessorBatch(
                    batch.tenantId(), batch.executionId(),
                    batch.routeId(), batch.applicationId(),
                    batch.execStartTime(), batch.processors());
        }

        // Search by order ID in attributes (via _search_text on executions)
        SearchResult<ExecutionSummary> result = searchIndex.search(new SearchRequest(
                null, null, null, null, null, null,
                "ORD-123", null, null, null,
                null, null, null, null, null,
                0, 50, null, null, null, null));
        assertThat(result.total()).isEqualTo(1);
        assertThat(result.data().get(0).executionId()).isEqualTo("pipeline-1");
        assertThat(result.data().get(0).status()).isEqualTo("COMPLETED");
        assertThat(result.data().get(0).durationMs()).isEqualTo(750L);

        // Search in processor body
        SearchResult<ExecutionSummary> bodyResult = searchIndex.search(new SearchRequest(
                null, null, null, null, null, null,
                null, "ABC-123", null, null,
                null, null, null, null, null,
                0, 50, null, null, null, null));
        assertThat(bodyResult.total()).isEqualTo(1);

        // Verify iteration data in processor_executions
        Integer iterSize = jdbc.queryForObject(
                "SELECT iteration_size FROM processor_executions WHERE execution_id = 'pipeline-1' AND seq = 2",
                Integer.class);
        assertThat(iterSize).isEqualTo(3);

        Integer iter0 = jdbc.queryForObject(
                "SELECT iteration FROM processor_executions WHERE execution_id = 'pipeline-1' AND seq = 3",
                Integer.class);
        assertThat(iter0).isEqualTo(0);

        // Verify total processor count
        Integer procCount = jdbc.queryForObject(
                "SELECT count() FROM processor_executions WHERE execution_id = 'pipeline-1'",
                Integer.class);
        assertThat(procCount).isEqualTo(4);
    }
}
