package com.lingniu.ingest.tdenginehistory;

import org.junit.jupiter.api.Test;

import javax.sql.DataSource;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

class TdengineJdbcHistoryReaderTest {

    @Test
    void readsRawFramesAndReturnsNextCursorWhenLimitHasMoreRow() throws Exception {
        RecordingQueryJdbc jdbc = new RecordingQueryJdbc();
        jdbc.rows.add(rawFrameResult("frame-1", "2026-06-29T05:00:01Z"));
        jdbc.rows.add(rawFrameResult("frame-2", "2026-06-29T05:00:02Z"));
        TdengineJdbcHistoryReader reader = new TdengineJdbcHistoryReader(
                jdbc.dataSource(), new TdengineHistorySchema("vehicle_history"));

        TdenginePage<TdengineRawFrameRow> page = reader.queryRawFrames(new TdengineRawFrameQuery(
                "JT808",
                "jt808:g7gps",
                Instant.parse("2026-06-29T00:00:00Z"),
                Instant.parse("2026-06-30T00:00:00Z"),
                TdengineQueryOrder.ASC,
                1,
                null));

        assertThat(page.items())
                .extracting(TdengineRawFrameRow::frameId)
                .containsExactly("frame-1");
        assertThat(page.nextCursor())
                .contains(new TdenginePageCursor(Instant.parse("2026-06-29T05:00:01Z"), "frame-1"));
        assertThat(jdbc.preparedSql).contains("LIMIT ?");
        assertThat(jdbc.boundValues.get(3)).isEqualTo(2);
    }

    @Test
    void readsLocationRowsWithoutNextCursorWhenPageIsComplete() throws Exception {
        RecordingQueryJdbc jdbc = new RecordingQueryJdbc();
        jdbc.rows.add(locationResult("fact-1", "2026-06-29T05:00:01Z"));
        TdengineJdbcHistoryReader reader = new TdengineJdbcHistoryReader(
                jdbc.dataSource(), new TdengineHistorySchema("vehicle_history"));

        TdenginePage<TdengineLocationRow> page = reader.queryLocations(new TdengineLocationQuery(
                "GB32960",
                "vin:VIN123",
                Instant.parse("2026-06-29T00:00:00Z"),
                Instant.parse("2026-06-30T00:00:00Z"),
                TdengineQueryOrder.DESC,
                10,
                null));

        assertThat(page.items())
                .extracting(TdengineLocationRow::factId)
                .containsExactly("fact-1");
        assertThat(page.nextCursor()).isEmpty();
        assertThat(page.items().getFirst().longitude()).isEqualTo(113.12);
    }

    @Test
    void returnsEmptyPageWhenTdengineChildTableDoesNotExistInNestedCause() throws Exception {
        RecordingQueryJdbc jdbc = new RecordingQueryJdbc();
        jdbc.executeQueryFailure = new SQLException("wrapper",
                new RuntimeException("TDengine error: table not exist"));
        TdengineJdbcHistoryReader reader = new TdengineJdbcHistoryReader(
                jdbc.dataSource(), new TdengineHistorySchema("vehicle_history"));

        TdenginePage<TdengineLocationRow> page = reader.queryLocations(new TdengineLocationQuery(
                "JT808",
                "jt808:g7gps",
                Instant.parse("2026-06-29T00:00:00Z"),
                Instant.parse("2026-06-30T00:00:00Z"),
                TdengineQueryOrder.ASC,
                10,
                null));

        assertThat(page.items()).isEmpty();
        assertThat(page.nextCursor()).isEmpty();
    }

    private static Map<String, Object> rawFrameResult(String frameId, String ts) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("ts", Timestamp.from(Instant.parse(ts)));
        row.put("frame_id", frameId);
        row.put("received_at", Timestamp.from(Instant.parse(ts).plusMillis(500)));
        row.put("message_id", 0x0200);
        row.put("sub_type", 0);
        row.put("event_time", Timestamp.from(Instant.parse(ts)));
        row.put("raw_uri", "archive://jt808/" + frameId + ".bin");
        row.put("checksum", "sha256:" + frameId);
        row.put("raw_size_bytes", 67L);
        row.put("parse_status", "SUCCEEDED");
        row.put("parse_error", "");
        row.put("peer", "10.0.0.1:808");
        row.put("metadata_json", "{}");
        row.put("protocol", "JT808");
        row.put("vehicle_key", "jt808:g7gps");
        row.put("vin", "VIN123");
        row.put("phone", "013800000000");
        return row;
    }

    private static Map<String, Object> locationResult(String factId, String ts) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("ts", Timestamp.from(Instant.parse(ts)));
        row.put("fact_id", factId);
        row.put("frame_id", "frame-1");
        row.put("received_at", Timestamp.from(Instant.parse(ts).plusMillis(500)));
        row.put("longitude", 113.12);
        row.put("latitude", 23.45);
        row.put("altitude_m", 8.0);
        row.put("speed_kmh", 42.5);
        row.put("direction_deg", 90.0);
        row.put("alarm_flag", 1L);
        row.put("status_flag", 3L);
        row.put("total_mileage_km", null);
        row.put("raw_uri", "archive://gb32960/frame-1.bin");
        row.put("metadata_json", "{}");
        row.put("protocol", "GB32960");
        row.put("vehicle_key", "vin:VIN123");
        row.put("vin", "VIN123");
        row.put("phone", "");
        return row;
    }

    private static final class RecordingQueryJdbc {
        private final List<Map<String, Object>> rows = new ArrayList<>();
        private final Map<Integer, Object> boundValues = new HashMap<>();
        private SQLException executeQueryFailure;
        private String preparedSql;

        DataSource dataSource() {
            return (DataSource) Proxy.newProxyInstance(
                    getClass().getClassLoader(),
                    new Class<?>[]{DataSource.class},
                    (proxy, method, args) -> switch (method.getName()) {
                        case "getConnection" -> connection();
                        default -> null;
                    });
        }

        private Connection connection() {
            return (Connection) Proxy.newProxyInstance(
                    getClass().getClassLoader(),
                    new Class<?>[]{Connection.class},
                    (proxy, method, args) -> switch (method.getName()) {
                        case "prepareStatement" -> preparedStatement((String) args[0]);
                        case "getAutoCommit" -> true;
                        default -> null;
                    });
        }

        private PreparedStatement preparedStatement(String sql) {
            preparedSql = sql;
            return (PreparedStatement) Proxy.newProxyInstance(
                    getClass().getClassLoader(),
                    new Class<?>[]{PreparedStatement.class},
                    (proxy, method, args) -> switch (method.getName()) {
                        case "setTimestamp", "setObject" -> {
                            boundValues.put((Integer) args[0], args[1]);
                            yield null;
                        }
                        case "executeQuery" -> {
                            if (executeQueryFailure != null) {
                                throw executeQueryFailure;
                            }
                            yield resultSet();
                        }
                        default -> null;
                    });
        }

        private ResultSet resultSet() {
            final int[] index = {-1};
            return (ResultSet) Proxy.newProxyInstance(
                    getClass().getClassLoader(),
                    new Class<?>[]{ResultSet.class},
                    (proxy, method, args) -> switch (method.getName()) {
                        case "next" -> ++index[0] < rows.size();
                        case "getTimestamp", "getString", "getInt", "getLong", "getDouble", "getObject" ->
                                rows.get(index[0]).get((String) args[0]);
                        case "wasNull" -> false;
                        default -> null;
                    });
        }
    }
}
