// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::BTreeMap;
use std::io::Write;
use std::str::FromStr;
use std::time::Duration;

use api::greptime_proto::io::prometheus::write::v2::histogram::{Count, ZeroCount};
use api::greptime_proto::io::prometheus::write::v2::metadata::MetricType as RemoteWriteV2MetricType;
use api::greptime_proto::io::prometheus::write::v2::{
    BucketSpan, Histogram, Metadata as RemoteWriteV2Metadata, Sample as RemoteWriteV2Sample,
    TimeSeries as RemoteWriteV2TimeSeries,
};
use api::prom_store::remote::label_matcher::Type as MatcherType;
use api::prom_store::remote::{
    Label, LabelMatcher, Query, ReadRequest, ReadResponse, Sample, TimeSeries, WriteRequest,
};
use auth::{UserProviderRef, user_provider_from_option};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use base64::prelude::{BASE64_STANDARD, Engine as _};
use chrono::Utc;
use cmd::options::GreptimeOptions;
use common_base::Plugins;
use common_catalog::consts::{
    DEFAULT_PRIVATE_SCHEMA_NAME, TRACE_TABLE_NAME, trace_operations_table_name,
    trace_services_table_name,
};
use common_config::Configurable;
use common_error::status_code::StatusCode as ErrorCode;
use common_frontend::slow_query_event::{
    SLOW_QUERY_TABLE_NAME, SLOW_QUERY_TABLE_QUERY_COLUMN_NAME,
};
use common_memory_manager::OnExhaustedPolicy;
use common_options::plugin_options::StandaloneFlag;
use flate2::Compression;
use flate2::write::GzEncoder;
use log_query::{AggFunc, Context, Limit, LogExpr, LogQuery, TimeFilter};
use loki_proto::logproto::{EntryAdapter, LabelPairAdapter, PushRequest, StreamAdapter};
use loki_proto::prost_types::Timestamp;
use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
use opentelemetry_proto::tonic::collector::metrics::v1::{
    ExportMetricsServiceRequest, ExportMetricsServiceResponse,
};
use opentelemetry_proto::tonic::collector::trace::v1::{
    ExportTraceServiceRequest, ExportTraceServiceResponse,
};
use pipeline::GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME;
use prost::Message;
use serde_json::{Value, json};
use servers::http::GreptimeQueryOutput;
use servers::http::handler::HealthResponse;
use servers::http::header::constants::{
    GREPTIME_LOG_EXTRACT_KEYS_HEADER_NAME, GREPTIME_LOG_TABLE_NAME_HEADER_NAME,
    GREPTIME_PIPELINE_NAME_HEADER_NAME,
};
use servers::http::header::{GREPTIME_DB_HEADER_NAME, GREPTIME_TIMEZONE_HEADER_NAME};
use servers::http::otlp::GoogleRpcStatus;
use servers::http::prometheus::{Column, PrometheusJsonResponse, PrometheusResponse};
use servers::http::result::error_result::ErrorResponse;
use servers::http::result::greptime_result_v1::GreptimedbV1Response;
use servers::http::result::influxdb_result_v1::{InfluxdbOutput, InfluxdbV1Response};
use servers::http::test_helpers::{TestClient, TestResponse};
use servers::prom_remote_write::v2::test_util as remote_write_v2;
use servers::prom_store::{self, mock_timeseries_new_label};
use servers::request_memory_limiter::ServerMemoryLimiter;
use standalone::options::StandaloneOptions;
use table::table_name::TableName;
use tests_integration::test_util::{
    StorageType, setup_test_http_app, setup_test_http_app_with_frontend,
    setup_test_http_app_with_frontend_and_slow_query_threshold,
    setup_test_http_app_with_frontend_and_user_provider, setup_test_prom_app_with_frontend,
    setup_test_prom_app_with_frontend_batched, setup_test_prom_app_with_frontend_native_histogram,
};
use urlencoding::encode;
use yaml_rust::YamlLoader;

use crate::event_recorder_test_util::assert_procedure_actor_by_table;

#[macro_export]
macro_rules! http_test {
    ($service:ident, $($(#[$meta:meta])* $test:ident),*,) => {
        paste::item! {
            mod [<integration_http_ $service:lower _test>] {
                $(
                    #[tokio::test(flavor = "multi_thread")]
                    $(
                        #[$meta]
                    )*
                    async fn [< $test >]() {
                        let store_type = tests_integration::test_util::StorageType::$service;
                        if store_type.test_on() {
                            let _ = $crate::http::$test(store_type).await;
                        }
                    }
                )*
            }
        }
    };
}

#[macro_export]
macro_rules! http_tests {
     ($($service:ident),*) => {
        $(
            http_test!(
                $service,

                test_http_auth,
                test_sql_api,
                test_http_analyze_stream_tql,
                test_http_sql_slow_query,
                test_prometheus_promql_api,
                test_prometheus_label_replace_response,
                test_prom_http_api,
                test_metrics_api,
                test_health_api,
                test_status_api,
                test_config_api,
                test_dynamic_tracer_toggle,
                test_dashboard_path,
                test_dashboard_api,
                test_prometheus_remote_write,
                test_prometheus_remote_write_v2,
                test_prometheus_remote_write_v2_native_histogram,
                test_prometheus_remote_write_batched,
                test_prometheus_remote_special_labels,
                test_prometheus_remote_schema_labels,
                test_prometheus_remote_write_with_pipeline,
                test_vm_proto_remote_write,

                test_pipeline_api,
                test_test_pipeline_api,
                test_pipeline_name_in_header,
                test_plain_text_ingestion,
                test_pipeline_auto_transform,
                test_pipeline_auto_transform_with_select,
                test_identity_pipeline,
                test_identity_pipeline_with_null_column,
                test_identity_pipeline_with_flatten,
                test_identity_pipeline_with_custom_ts,
                test_pipeline_dispatcher,
                test_pipeline_suffix_template,
                test_pipeline_context,
                test_pipeline_with_vrl,
                test_pipeline_with_hint_vrl,
                test_pipeline_one_to_many_vrl,
                test_pipeline_2,
                test_pipeline_skip_error,
                test_pipeline_filter,
                test_pipeline_create_table,
                test_pipeline_index_options,

                test_otlp_metrics_new,
                test_otlp_exponential_histogram,
                test_otlp_metric_translation_strategies,
                test_otlp_metrics_resource_info_conflicts,
                test_otlp_traces_v0,
                test_otlp_traces_v1,
                test_otlp_traces_v1_entity_graph,
                test_otlp_logs,
                test_loki_pb_logs,
                test_loki_pb_logs_with_pipeline,
                test_loki_json_logs,
                test_loki_json_logs_with_pipeline,
                test_elasticsearch_logs,
                test_elasticsearch_logs_with_index,
                test_splunk_health,
                test_splunk_health_is_public,
                test_splunk_logs,
                test_splunk_raw,
                test_log_query,
                test_jaeger_query_api,
                test_jaeger_query_api_for_trace_v1,

                test_influxdb_write,
                test_influxdb_write_with_hints,
                test_influxdb_write_with_append_mode_hint,
                test_http_memory_limit,
            );
        )*
    };
}

pub async fn test_http_auth(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();

    let user_provider = user_provider_from_option(
        "static_user_provider:cmd:greptime_user=greptime_pwd,readonly_user:ro=readonly_pwd,writeonly_user:wo=writeonly_pwd",
    )
    .unwrap();

    let (app, mut guard) = setup_test_http_app_with_frontend_and_user_provider(
        store_type,
        "sql_api",
        Some(user_provider),
    )
    .await;
    let client = TestClient::new(app).await;

    // 1. no auth
    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);

    // 2. wrong auth
    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .header("Authorization", basic_auth("greptime_user", "wrong_pwd"))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::UNAUTHORIZED);

    // 3. right auth
    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 4. readonly user cannot write
    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .header("Authorization", basic_auth("readonly_user", "readonly_pwd"))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?db=public&sql=create table auth_test(ts timestamp time index);")
        .header("Authorization", basic_auth("readonly_user", "readonly_pwd"))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::FORBIDDEN);

    // 5. writeonly user cannot read
    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .header(
            "Authorization",
            basic_auth("writeonly_user", "writeonly_pwd"),
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::FORBIDDEN);
    let res = client
        .get("/v1/sql?db=public&sql=create table auth_test(ts timestamp time index);")
        .header(
            "Authorization",
            basic_auth("writeonly_user", "writeonly_pwd"),
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let actor_query_client = &client;
    assert_procedure_actor_by_table(
        "create_table",
        "auth_test",
        "writeonly_user",
        |query| async move {
            let res = actor_query_client
                .get(&format!("/v1/sql?db=public&sql={}", encode(&query)))
                .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
                .send()
                .await;
            let Ok(body) = serde_json::from_str::<GreptimedbV1Response>(&res.text().await) else {
                return false;
            };
            matches!(
                body.output().first(),
                Some(GreptimeQueryOutput::Records(records))
                    if records.rows().first().and_then(|row| row.first()).and_then(Value::as_bool)
                        == Some(true)
            )
        },
    )
    .await;
    let res = client
        .get("/v1/sql?db=public&sql=insert into auth_test values(1);")
        .header(
            "Authorization",
            basic_auth("writeonly_user", "writeonly_pwd"),
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?db=public&sql=select * from auth_test;")
        .header(
            "Authorization",
            basic_auth("writeonly_user", "writeonly_pwd"),
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::FORBIDDEN);

    guard.remove_all().await;
}

fn basic_auth(username: &str, password: &str) -> String {
    format!(
        "basic {}",
        BASE64_STANDARD.encode(format!("{username}:{password}"))
    )
}

fn assert_remote_write_v2_written_headers(headers: &HeaderMap, samples: &str) {
    assert_remote_write_v2_written_headers_with_histograms(headers, samples, "0")
}

fn assert_remote_write_v2_written_headers_with_histograms(
    headers: &HeaderMap,
    samples: &str,
    histograms: &str,
) {
    assert_eq!(
        Some(samples),
        headers
            .get("X-Prometheus-Remote-Write-Samples-Written")
            .map(|x| x.to_str().unwrap())
    );
    assert_eq!(
        Some(histograms),
        headers
            .get("X-Prometheus-Remote-Write-Histograms-Written")
            .map(|x| x.to_str().unwrap())
    );
    assert_eq!(
        Some("0"),
        headers
            .get("X-Prometheus-Remote-Write-Exemplars-Written")
            .map(|x| x.to_str().unwrap())
    );
}

pub async fn test_http_auth_from_standalone_user_provider_config() {
    common_telemetry::init_default_ut_logging();

    let config = tempfile::NamedTempFile::new().unwrap();
    let user_provider = "static_user_provider:cmd:greptime_user=greptime_pwd";
    std::fs::write(
        config.path(),
        format!("user_provider = \"{user_provider}\"\n"),
    )
    .unwrap();

    let options =
        GreptimeOptions::<StandaloneOptions>::load_layered_options(config.path().to_str(), "")
            .unwrap();
    let fe_opts = options.component.frontend_options();

    let mut plugins = Plugins::new();
    plugins.insert(StandaloneFlag);
    plugins::setup_frontend_plugins_pre_build(&mut plugins, &[], &fe_opts, None)
        .await
        .unwrap();
    let user_provider = plugins.get::<UserProviderRef>();

    let (app, mut guard) = setup_test_http_app_with_frontend_and_user_provider(
        StorageType::File,
        "sql_api_user_provider_config",
        user_provider,
    )
    .await;
    let client = TestClient::new(app).await;

    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .send()
        .await;
    let status = res.status();
    let body = res.text().await;
    assert_eq!(
        status,
        StatusCode::UNAUTHORIZED,
        "unexpected response body: {body}"
    );

    let res = client
        .get("/v1/sql?db=public&sql=show tables;")
        .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    guard.remove_all().await;
}

#[tokio::test]
pub async fn test_cors() {
    let (app, mut guard) = setup_test_http_app_with_frontend(StorageType::File, "test_cors").await;
    let client = TestClient::new(app).await;

    let res = client.get("/health").send().await;

    assert_eq!(res.status(), StatusCode::OK);
    assert_eq!(
        res.headers()
            .get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
            .expect("expect cors header origin"),
        "*"
    );

    let res = client
        .options("/health")
        .header("Access-Control-Request-Headers", "x-greptime-auth")
        .header("Access-Control-Request-Method", "DELETE")
        .header("Origin", "https://example.com")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    assert_eq!(
        res.headers()
            .get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
            .expect("expect cors header origin"),
        "*"
    );
    assert_eq!(
        res.headers()
            .get(http::header::ACCESS_CONTROL_ALLOW_HEADERS)
            .expect("expect cors header headers"),
        "*"
    );
    assert_eq!(
        res.headers()
            .get(http::header::ACCESS_CONTROL_ALLOW_METHODS)
            .expect("expect cors header methods"),
        "GET,POST,PUT,DELETE,HEAD"
    );

    guard.remove_all().await;
}

pub async fn test_sql_api(store_type: StorageType) {
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "sql_api").await;
    let client = TestClient::new(app).await;
    let res = client.get("/v1/sql").send().await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert_eq!(body.code(), 1004);
    assert_eq!(body.error(), "sql parameter is required.");

    let res = client
        .get("/v1/sql?sql=select * from numbers limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let output = body.output();
    assert_eq!(output.len(), 1);
    assert_eq!(
        output[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records" :{"schema":{"column_schemas":[{"name":"number","data_type":"UInt32"}]},"rows":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]],"total_rows":10}
        })).unwrap()
    );

    // test influxdb_v1 result format
    let res = client
        .get("/v1/sql?format=influxdb_v1&sql=select * from numbers limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<InfluxdbV1Response>(&res.text().await).unwrap();
    let output = body.results();
    assert_eq!(output.len(), 1);
    assert_eq!(
        output[0],
        serde_json::from_value::<InfluxdbOutput>(json!({
            "statement_id":0,"series":[{"name":"","columns":["number"],"values":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]}]
        })).unwrap()
    );

    // test json result format
    let res = client
        .get("/v1/sql?format=json&sql=select * from numbers limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = res.json::<Value>().await;
    let data = body.get("data").expect("Missing 'data' field in response");

    let expected = json!([
        {"number": 0}, {"number": 1}, {"number": 2}, {"number": 3}, {"number": 4},
        {"number": 5}, {"number": 6}, {"number": 7}, {"number": 8}, {"number": 9}
    ]);
    assert_eq!(data, &expected);

    // test insert and select
    let res = client
        .get("/v1/sql?sql=insert into demo values('host, \"name', 66.6, 1024, 0)")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // select *
    let res = client
        .get("/v1/sql?sql=select * from demo limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let output = body.output();
    assert_eq!(output.len(), 1);

    assert_eq!(
        output[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"host","data_type":"String"},{"name":"cpu","data_type":"Float64"},{"name":"memory","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]},"rows":[["host, \"name",66.6,1024.0,0]],"total_rows":1}
        })).unwrap()
    );

    // select with projections
    let res = client
        .get("/v1/sql?sql=select cpu, ts from demo limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let output = body.output();
    assert_eq!(output.len(), 1);

    assert_eq!(
        output[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"cpu","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]},"rows":[[66.6,0]],"total_rows":1}
        })).unwrap()
    );

    // select with column alias
    let res = client
        .get("/v1/sql?sql=select cpu as c, ts as time from demo limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let output = body.output();
    assert_eq!(output.len(), 1);
    assert_eq!(
        output[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"c","data_type":"Float64"},{"name":"time","data_type":"TimestampMillisecond"}]},"rows":[[66.6,0]],"total_rows":1}
        })).unwrap()
    );

    // test multi-statement
    let res = client
        .get("/v1/sql?sql=select cpu, ts from demo limit 1;select cpu, ts from demo where ts > 0;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let outputs = body.output();
    assert_eq!(outputs.len(), 2);
    assert_eq!(
        outputs[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"cpu","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]},"rows":[[66.6,0]],"total_rows":1}
        })).unwrap()
    );
    assert_eq!(
        outputs[1],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"rows":[], "schema":{"column_schemas":[{"name":"cpu","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]}, "total_rows":0}
        }))
        .unwrap()
    );

    // test multi-statement with error
    let res = client
        .get("/v1/sql?sql=select cpu, ts from demo limit 1;select cpu, ts from demo2 where ts > 0;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert!(body.error().contains("Table not found"));
    assert_eq!(body.code(), ErrorCode::TableNotFound as u32);

    // test database given
    let res = client
        .get("/v1/sql?db=public&sql=select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let outputs = body.output();
    assert_eq!(outputs.len(), 1);
    assert_eq!(
        outputs[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"cpu","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]},"rows":[[66.6,0]],"total_rows":1}
        })).unwrap()
    );

    // test database not found
    let res = client
        .get("/v1/sql?db=notfound&sql=select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert_eq!(body.code(), ErrorCode::DatabaseNotFound as u32);

    // test catalog-schema given
    let res = client
        .get("/v1/sql?db=greptime-public&sql=select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let outputs = body.output();
    assert_eq!(outputs.len(), 1);
    assert_eq!(
        outputs[0],
        serde_json::from_value::<GreptimeQueryOutput>(json!({
            "records":{"schema":{"column_schemas":[{"name":"cpu","data_type":"Float64"},{"name":"ts","data_type":"TimestampMillisecond"}]},"rows":[[66.6,0]],"total_rows":1}
        })).unwrap()
    );

    // test invalid catalog
    let res = client
        .get("/v1/sql?db=notfound2-schema&sql=select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert_eq!(body.code(), ErrorCode::DatabaseNotFound as u32);

    // test invalid schema
    let res = client
        .get("/v1/sql?db=greptime-schema&sql=select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert_eq!(body.code(), ErrorCode::DatabaseNotFound as u32);

    // test analyze format
    let res = client
        .get("/v1/sql?sql=explain analyze format json select cpu, ts from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<GreptimedbV1Response>(&res.text().await).unwrap();
    let output = body.output();
    assert_eq!(output.len(), 1);
    // this is something only json format can show
    assert!(format!("{:?}", output[0]).contains("\\\"param\\\""));

    // test csv format
    let res = client
        .get("/v1/sql?format=csv&sql=select cpu,ts,host from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = &res.text().await;
    // Must be escaped correctly: 66.6,0,"host, ""name"
    assert_eq!(body, "66.6,0,\"host, \"\"name\"\r\n");

    // csv with names
    let res = client
        .get("/v1/sql?format=csvWithNames&sql=select cpu,ts,host from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = &res.text().await;
    assert_eq!(body, "cpu,ts,host\r\n66.6,0,\"host, \"\"name\"\r\n");

    // csv with names and types
    let res = client
        .get("/v1/sql?format=csvWithNamesAndTypes&sql=select cpu,ts,host from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = &res.text().await;
    assert_eq!(
        body,
        "cpu,ts,host\r\nFloat64,TimestampMillisecond,String\r\n66.6,0,\"host, \"\"name\"\r\n"
    );
    // test null format
    let res = client
        .get("/v1/sql?format=null&sql=select cpu,ts,host from demo limit 1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = &res.text().await;
    assert!(body.contains("1 rows in set."));
    assert!(body.ends_with("sec.\n"));

    // test parse method
    let res = client.get("/v1/sql/parse?sql=desc table t").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    assert_eq!(
        res.text().await,
        r#"[{"DescribeTable":{"name":[{"Identifier":{"value":"t","quote_style":null,"span":{"start":{"line":0,"column":0},"end":{"line":0,"column":0}}}}]}}]"#,
    );

    // test timezone header
    let res = client
        .get("/v1/sql?&sql=show variables system_time_zone")
        .header(
            TryInto::<HeaderName>::try_into(GREPTIME_TIMEZONE_HEADER_NAME.to_string()).unwrap(),
            "Asia/Shanghai",
        )
        .send()
        .await
        .text()
        .await;
    assert!(res.contains("SYSTEM_TIME_ZONE") && res.contains("UTC"));
    let res = client
        .get("/v1/sql?&sql=show variables time_zone")
        .header(
            TryInto::<HeaderName>::try_into(GREPTIME_TIMEZONE_HEADER_NAME.to_string()).unwrap(),
            "Asia/Shanghai",
        )
        .send()
        .await
        .text()
        .await;
    assert!(res.contains("TIME_ZONE") && res.contains("Asia/Shanghai"));
    let res = client
        .get("/v1/sql?&sql=show variables system_time_zone")
        .send()
        .await
        .text()
        .await;
    assert!(res.contains("SYSTEM_TIME_ZONE") && res.contains("UTC"));
    let res = client
        .get("/v1/sql?&sql=show variables time_zone")
        .send()
        .await
        .text()
        .await;
    assert!(res.contains("TIME_ZONE") && res.contains("UTC"));
    guard.remove_all().await;
}

#[tokio::test]
async fn test_sql_format_api() {
    let (app, mut guard) =
        setup_test_http_app_with_frontend(StorageType::File, "sql_format_api").await;
    let client = TestClient::new(app).await;

    // missing sql
    let res = client.get("/v1/sql/format").send().await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    let body = serde_json::from_str::<ErrorResponse>(&res.text().await).unwrap();
    assert_eq!(body.code(), 1004);
    assert_eq!(body.error(), "sql parameter is required.");

    // with sql
    let res = client
        .get("/v1/sql/format?sql=select%201%20as%20x")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap();
    let formatted = json
        .get("formatted")
        .and_then(|v| v.as_str())
        .unwrap_or_default();
    assert_eq!(formatted, "SELECT 1 AS x;");

    // complex query
    let complex_query = "WITH RECURSIVE slow_cte AS (SELECT 1 AS n, md5(CAST(random() AS STRING)) AS hash UNION ALL SELECT n + 1, md5(concat(hash, n)) FROM slow_cte WHERE n < 4500) SELECT COUNT(*) FROM slow_cte";
    let encoded_complex_query = encode(complex_query);

    let query_params = format!("/v1/sql/format?sql={encoded_complex_query}");
    let res = client.get(&query_params).send().await;
    assert_eq!(res.status(), StatusCode::OK);

    let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap();
    let formatted = json
        .get("formatted")
        .and_then(|v| v.as_str())
        .unwrap_or_default();
    assert_eq!(
        formatted,
        "WITH RECURSIVE slow_cte AS (SELECT 1 AS n, md5(CAST(random() AS STRING)) AS hash UNION ALL SELECT n + 1, md5(concat(hash, n)) FROM slow_cte WHERE n < 4500) SELECT COUNT(*) FROM slow_cte;"
    );

    guard.remove_all().await;
}

pub async fn test_http_analyze_stream_tql(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "analyze_stream_tql").await;
    let client = TestClient::new(app).await;

    let res = client
        .post("/v1/sql/analyze/stream")
        .header("Accept", "text/event-stream")
        .form(&BTreeMap::from([(
            "sql".to_string(),
            "TQL ANALYZE VERBOSE (0, 10, '5s') demo".to_string(),
        )]))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    assert!(
        res.headers()
            .get("content-type")
            .and_then(|value| value.to_str().ok())
            .is_some_and(|value| value.starts_with("text/event-stream"))
    );
    let body = res.text().await;
    let final_event = body
        .split("\n\n")
        .find(|event| event.lines().any(|line| line == "event: final"))
        .expect(&body);
    let payload = final_event
        .lines()
        .find_map(|line| line.strip_prefix("data: "))
        .map(|data| serde_json::from_str::<Value>(data).unwrap())
        .unwrap();
    assert_eq!(payload["state"], "final");
    assert!(payload["metrics"].as_array().is_some_and(|v| !v.is_empty()));
    assert!(
        payload["output"]["records"]["rows"]
            .as_array()
            .is_some_and(|v| !v.is_empty())
    );

    let res = client
        .post("/v1/sql/analyze/stream")
        .header("Accept", "text/event-stream")
        .form(&BTreeMap::from([(
            "sql".to_string(),
            "TQL ANALYZE VERBOSE FORMAT JSON (0, 10, '5s') demo".to_string(),
        )]))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    assert!(res.text().await.contains("event: final"));

    guard.remove_all().await;
}

pub async fn test_http_sql_slow_query(store_type: StorageType) {
    let (app, mut guard) = setup_test_http_app_with_frontend_and_slow_query_threshold(
        store_type,
        "sql_api",
        Duration::from_millis(100),
    )
    .await;
    let client = TestClient::new(app).await;

    let slow_query = "SELECT count(*) FROM generate_series(1, 50000000)";
    let encoded_slow_query = encode(slow_query);

    let query_params = format!("/v1/sql?sql={encoded_slow_query}");
    let res = client.get(&query_params).send().await;
    assert_eq!(res.status(), StatusCode::OK);

    let table = format!("{}.{}", DEFAULT_PRIVATE_SCHEMA_NAME, SLOW_QUERY_TABLE_NAME);
    let query = format!(
        "SELECT {} FROM {table} WHERE {} = '{slow_query}'",
        SLOW_QUERY_TABLE_QUERY_COLUMN_NAME, SLOW_QUERY_TABLE_QUERY_COLUMN_NAME
    );

    let expected = format!(r#"[["{}"]]"#, slow_query);
    wait_for_data(&client, &query, &expected).await;

    guard.remove_all().await;
}

pub async fn test_prometheus_promql_api(store_type: StorageType) {
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "promql_api").await;
    let client = TestClient::new(app).await;

    let res = client
        .get("/v1/promql?query=abs(demo{host=\"Hangzhou\"})&start=0&end=100&step=5s")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let json_text = res.text().await;
    assert!(serde_json::from_str::<GreptimedbV1Response>(&json_text).is_ok());

    let res = client
        .get("/v1/promql?query=1&start=0&end=100&step=5s&format=csv")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let csv_body = &res.text().await;
    assert_eq!(
        "0,1.0\r\n5000,1.0\r\n10000,1.0\r\n15000,1.0\r\n20000,1.0\r\n25000,1.0\r\n30000,1.0\r\n35000,1.0\r\n40000,1.0\r\n45000,1.0\r\n50000,1.0\r\n55000,1.0\r\n60000,1.0\r\n65000,1.0\r\n70000,1.0\r\n75000,1.0\r\n80000,1.0\r\n85000,1.0\r\n90000,1.0\r\n95000,1.0\r\n100000,1.0\r\n",
        csv_body
    );

    guard.remove_all().await;
}

pub async fn test_prometheus_label_replace_response(store_type: StorageType) {
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "prometheus_label_replace_response").await;
    let client = TestClient::new(app).await;

    let query = encode(r#"label_replace(demo, "host_copy", "$1", "host", "(.*)")"#);
    let res = client
        .get(&format!("/v1/prometheus/api/v1/query?query={query}&time=0"))
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!({
            "resultType": "vector",
            "result": [{
                "metric": {
                    "__name__": "demo",
                    "host": "host1",
                    "host_copy": "host1"
                },
                "value": [0.0, "1.1"]
            }]
        }))
        .unwrap()
    );

    guard.remove_all().await;
}

pub async fn test_prom_http_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_prom_app_with_frontend(store_type, "promql_api").await;
    let client = TestClient::new(app).await;

    // format_query
    let res = client
        .get("/v1/prometheus/api/v1/format_query?query=foo/bar")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    assert_eq!(
        res.text().await.as_str(),
        r#"{"status":"success","data":"foo / bar"}"#
    );

    // instant query
    let res = client
        .get("/v1/prometheus/api/v1/query?query=up&time=1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/query?query=up&time=1")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // instant query 1+1
    let res = client
        .get("/v1/prometheus/api/v1/query?query=1%2B1&time=1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(
            json!({"resultType":"scalar","result":[1.0,"2.0"]})
        )
        .unwrap()
    );

    // range query
    let res = client
        .get("/v1/prometheus/api/v1/query_range?query=up&start=1&end=100&step=5")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/query_range?query=up&start=1&end=100&step=5")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/query_range?query=count(count(up))&start=1&end=100&step=5")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    // query a metric table backed by a physical table with nanosecond precision to ensure
    // time cast and __tsid path work with logical table aliasing.
    let res = client
        .get(
            "/v1/prometheus/api/v1/query_range?query=demo_metrics_with_nanos&start=0&end=10&step=1",
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    let res = client
        .get("/v1/prometheus/api/v1/query_range?query=up&start=1&end=100&step=0.5")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/query_range?query=up&start=1&end=100&step=0.5")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // labels
    let res = client
        .get("/v1/prometheus/api/v1/labels?match[]=demo")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/labels?match[]=up")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/prometheus/api/v1/labels?match[]=demo&start=0")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["__name__", "host",])).unwrap()
    );

    // labels without match[] param
    let res = client.get("/v1/prometheus/api/v1/labels").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!([
            "__name__", "env", "host", "idc", "number",
        ]))
        .unwrap()
    );

    // labels query with multiple match[] params
    let res = client
        .get("/v1/prometheus/api/v1/labels?match[]=up&match[]=down")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/prometheus/api/v1/labels?match[]=up&match[]=down")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // series
    let res = client
        .get("/v1/prometheus/api/v1/series?match[]=demo{}&start=0&end=0")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");

    let PrometheusResponse::Series(mut series) = body.data else {
        unreachable!()
    };
    let actual = series
        .remove(0)
        .into_iter()
        .collect::<BTreeMap<Column, String>>();
    let expected = BTreeMap::from([
        ("__name__".into(), "demo".to_string()),
        ("host".into(), "host1".to_string()),
    ]);
    assert_eq!(actual, expected);

    let res = client
        .post("/v1/prometheus/api/v1/series?match[]=up&match[]=down")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    // An empty array will be deserialized into PrometheusResponse::Labels.
    // So here we compare the text directly.
    assert_eq!(res.text().await, r#"{"status":"success","data":[]}"#);

    // label values
    // should return error if there is no match[]
    let res = client
        .get("/v1/prometheus/api/v1/label/instance/values")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "error");
    assert!(
        prom_resp
            .error
            .is_some_and(|err| err.eq_ignore_ascii_case("match[] parameter is required"))
    );
    assert!(
        prom_resp
            .error_type
            .is_some_and(|err| err.eq_ignore_ascii_case("InvalidArguments"))
    );

    // single match[]
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // single match[]
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=300")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1"])).unwrap()
    );

    // single match[] with __name__
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]={__name__%3D%22demo%22}&start=0&end=300")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1"])).unwrap()
    );

    // single match[]
    let res = client
        .get("/v1/prometheus/api/v1/label/idc/values?match[]=demo_metrics_with_nanos&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["idc1"])).unwrap()
    );

    // match labels.
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=multi_labels{idc=\"idc1\", env=\"dev\"}&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // special labels
    let res = client
        .get("/v1/prometheus/api/v1/label/__schema__/values?start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!([
            "greptime_private",
            "information_schema",
            "public"
        ]))
        .unwrap()
    );

    // special labels
    let res = client
        .get("/v1/prometheus/api/v1/label/__schema__/values?match[]=demo&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["public"])).unwrap()
    );

    // special labels
    let res = client
        .get("/v1/prometheus/api/v1/label/__database__/values?match[]=demo&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["public"])).unwrap()
    );

    // special labels
    let res = client
        .get("/v1/prometheus/api/v1/label/__database__/values?match[]=multi_labels{idc=\"idc1\", env=\"dev\"}&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["public"])).unwrap()
    );

    // match special labels.
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=multi_labels{__schema__=\"public\", idc=\"idc1\", env=\"dev\"}&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // match special labels.
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=multi_labels{__schema__=\"information_schema\", idc=\"idc1\", env=\"dev\"}&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!([])).unwrap()
    );

    // search field name
    let res = client
        .get("/v1/prometheus/api/v1/label/__field__/values?match[]=demo")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["val"])).unwrap()
    );

    // limit
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600&limit=1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1"])).unwrap()
    );

    // limit 0
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600&limit=0")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // limit 10
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600&limit=10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // special labels limit
    let res = client
        .get("/v1/prometheus/api/v1/label/__schema__/values?start=0&end=600&limit=2")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!([
            "greptime_private",
            "information_schema",
        ]))
        .unwrap()
    );

    // query an empty database should return nothing
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600")
        .header(GREPTIME_DB_HEADER_NAME.clone(), "nonexistent")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!([])).unwrap()
    );

    // db header will be overrode by `db` in param
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=demo&start=0&end=600&db=public")
        .header(GREPTIME_DB_HEADER_NAME.clone(), "nonexistent")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");
    assert_eq!(
        body.data,
        serde_json::from_value::<PrometheusResponse>(json!(["host1", "host2"])).unwrap()
    );

    // multiple match[]
    let res = client
        .get("/v1/prometheus/api/v1/label/instance/values?match[]=up&match[]=system_metrics")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "success");
    assert!(prom_resp.error.is_none());
    assert!(prom_resp.error_type.is_none());

    // query non-string value
    let res = client
        .get("/v1/prometheus/api/v1/label/host/values?match[]=mito")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // query non-exist label in metric table
    let res = client
        .get("/v1/prometheus/api/v1/label/not_exist_label/values?match[]=demo&start=0&end=600")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "success");
    assert!(prom_resp.error.is_none() && prom_resp.error_type.is_none());
    assert_eq!(
        prom_resp.data,
        serde_json::from_value::<PrometheusResponse>(json!([])).unwrap()
    );

    // query `__name__` without match[]
    // create a physical table and a logical table
    let res = client
        .get("/v1/sql?sql=create table physical_table (`ts` timestamp time index, `message` string) with ('physical_metric_table' = 'true');")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    let res = client
        .get("/v1/sql?sql=create table logic_table (`ts` timestamp time index, `message` string) with ('on_physical_table' = 'physical_table');")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    // query `__name__`
    let res = client
        .get("/v1/prometheus/api/v1/label/__name__/values")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "success");
    assert!(prom_resp.error.is_none());
    assert!(prom_resp.error_type.is_none());
    assert_eq!(
        prom_resp.data,
        serde_json::from_value::<PrometheusResponse>(json!([
            "demo",
            "demo_metrics",
            "demo_metrics_with_nanos",
            "logic_table",
            "multi_labels",
        ]))
        .unwrap()
    );

    // query `__name__`
    let res = client
        .get("/v1/prometheus/api/v1/label/__name__/values?match[]={__name__=\"demo\"}")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "success");
    assert!(prom_resp.error.is_none());
    assert!(prom_resp.error_type.is_none());
    assert_eq!(
        prom_resp.data,
        serde_json::from_value::<PrometheusResponse>(json!(["demo",])).unwrap()
    );

    // query `__name__`
    let res = client
        .get("/v1/prometheus/api/v1/label/__name__/values?match[]={__name__=~\".*demo.*\"}")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let prom_resp = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(prom_resp.status, "success");
    assert!(prom_resp.error.is_none());
    assert!(prom_resp.error_type.is_none());
    assert_eq!(
        prom_resp.data,
        serde_json::from_value::<PrometheusResponse>(json!([
            "demo",
            "demo_metrics",
            "demo_metrics_with_nanos",
        ]))
        .unwrap()
    );

    // buildinfo
    let res = client
        .get("/v1/prometheus/api/v1/status/buildinfo")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
    assert_eq!(body.status, "success");

    // parse_query
    let res = client
        .get("/v1/prometheus/api/v1/parse_query?query=http_requests")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let data = res.text().await;
    // we don't have deserialization for ast so we keep test simple and compare
    //  the json output directly.
    // the correctness should be covered by parser. In this test we only check
    //  response format.
    let expected = "{\"status\":\"success\",\"data\":{\"type\":\"vectorSelector\",\"name\":\"http_requests\",\"matchers\":[],\"offset\":0,\"startOrEnd\":null,\"timestamp\":null}}";
    assert_eq!(expected, data);

    let res = client
        .get("/v1/prometheus/api/v1/parse_query?query=not http_requests")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let data = res.text().await;
    let expected = "{\"status\":\"error\",\"error\":\"invalid promql query\",\"errorType\":\"InvalidArguments\"}";
    assert_eq!(expected, data);

    // range_query with __name__ not-equal matcher
    let res = client
        .post("/v1/prometheus/api/v1/query_range?query=count by(__name__)({__name__=~'demo.*'})&start=1&end=100&step=5")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let data = res.text().await;
    assert!(
        data.contains("{\"__name__\":\"demo_metrics\"}")
            && data.contains("{\"__name__\":\"demo\"}")
    );

    let res = client
        .post("/v1/prometheus/api/v1/query_range?query=count by(__name__)({__name__=~'demo_metrics'})&start=1&end=100&step=5")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let data = res.text().await;
    assert!(
        data.contains("{\"__name__\":\"demo_metrics\"}")
            && !data.contains("{\"__name__\":\"demo\"}")
    );

    guard.remove_all().await;
}

pub async fn test_metrics_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app(store_type, "metrics_api").await;
    let client = TestClient::new(app).await;

    // Send a sql
    let res = client
        .get("/v1/sql?sql=select * from numbers limit 10")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // Call metrics api
    let res = client.get("/metrics").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.text().await;
    // Comment in the metrics text.
    assert!(body.contains("# HELP"));
    guard.remove_all().await;
}

pub async fn test_splunk_health(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_splunk_health").await;
    let client = TestClient::new(app).await;

    // The HEC health endpoint returns 200 with the HEC health body.
    let res = client
        .get("/v1/splunk/services/collector/health")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap();
    assert_eq!(json["text"], "HEC is healthy");
    assert_eq!(json["code"], 17);

    // The versioned alias `/health/1.0` serves the same handler.
    let res = client
        .get("/v1/splunk/services/collector/health/1.0")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap();
    assert_eq!(json["text"], "HEC is healthy");
    assert_eq!(json["code"], 17);

    // Query parameters (e.g. `ack`, `token`) are tolerated and ignored rather
    // than rejected
    for path in [
        "/v1/splunk/services/collector/health?ack=true&token=xyz",
        "/v1/splunk/services/collector/health/1.0?ack=true&token=xyz",
    ] {
        let res = client.get(path).send().await;
        assert_eq!(StatusCode::OK, res.status());
    }

    guard.remove_all().await;
}

pub async fn test_splunk_health_is_public(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();

    let user_provider =
        user_provider_from_option("static_user_provider:cmd:greptime_user=greptime_pwd").unwrap();
    let (app, _guard) = setup_test_http_app_with_frontend_and_user_provider(
        store_type,
        "test_splunk_health_is_public",
        Some(user_provider),
    )
    .await;
    let client = TestClient::new(app).await;

    // The health endpoint is registered in `PUBLIC_API_PREFIX`, so it must
    // succeed without credentials even when a user provider is configured.
    let res = client
        .get("/v1/splunk/services/collector/health")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
}

pub async fn test_splunk_logs(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();

    let user_provider =
        user_provider_from_option("static_user_provider:cmd:greptime_user=greptime_pwd").unwrap();
    let (app, mut guard) = setup_test_http_app_with_frontend_and_user_provider(
        store_type,
        "test_splunk_logs",
        Some(user_provider),
    )
    .await;
    let client = TestClient::new(app).await;

    // Authenticated SQL query (the user-provider harness requires auth on /v1/sql).
    async fn query(client: &TestClient, sql: &str) -> String {
        let res = client
            .get(format!("/v1/sql?sql={sql}").as_str())
            .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK, "query failed: {sql}");
        res.text().await
    }

    // HEC `Authorization: Splunk <user:pass>` + JSON content type.
    let splunk_headers = || {
        vec![
            (
                HeaderName::from_static("authorization"),
                HeaderValue::from_static("Splunk greptime_user:greptime_pwd"),
            ),
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/json"),
            ),
        ]
    };
    let event_path = "/v1/splunk/services/collector/event";

    // 1. Ingest a HEC batch (no `index` -> default `splunk_logs` table).
    let body = concat!(
        r#"{"event":"login ok","time":1700000000,"host":"web-01","source":"auth.log","sourcetype":"syslog","fields":{"region":"us-east"}}"#,
        "\n",
        r#"{"event":"login fail","time":1700000001,"host":"web-02","source":"auth.log","sourcetype":"syslog","fields":{"region":"us-west"}}"#,
    );
    let res = send_req(
        &client,
        splunk_headers(),
        event_path,
        body.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    assert!(res.text().await.contains("\"code\":0"));

    // 2. Rows landed with the right values (proves route + auth + parse + insert,
    //    and that the default table name is `splunk_logs`).
    let rows = get_rows_from_output(
        &query(
            &client,
            "select host, region, event, greptime_timestamp from splunk_logs order by host",
        )
        .await,
    );
    // `time` (epoch seconds) maps to the nanosecond timestamp column.
    assert_eq!(
        rows,
        r#"[["web-01","us-east","login ok",1700000000000000000],["web-02","us-west","login fail",1700000001000000000]]"#
    );

    // 3. host/source/sourcetype + `fields` keys are tags (i.e. primary key).
    let create = query(&client, "show create table splunk_logs").await;
    let pk = create
        .split("PRIMARY KEY")
        .nth(1)
        .expect("splunk_logs should have a PRIMARY KEY");
    for col in ["host", "source", "sourcetype", "region"] {
        assert!(
            pk.contains(col),
            "expected `{col}` in primary key: {create}"
        );
    }
    // host/source/sourcetype lead the primary key (ahead of the `fields` tag `region`),
    let pos = |col: &str| pk.find(col).expect("column missing from primary key");
    assert!(
        pos("host") < pos("source")
            && pos("source") < pos("sourcetype")
            && pos("sourcetype") < pos("region"),
        "expected host/source/sourcetype to lead the primary key: {create}"
    );

    // 4. A brand-new `fields` key on a later write also becomes a tag (dynamic
    //    tag column added to the existing table's primary key).
    let body2 = r#"{"event":"deploy","time":1700000002,"host":"web-03","source":"deploy.log","sourcetype":"syslog","fields":{"datacenter":"dc1"}}"#;
    let res = send_req(
        &client,
        splunk_headers(),
        event_path,
        body2.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let create = query(&client, "show create table splunk_logs").await;
    let pk = create.split("PRIMARY KEY").nth(1).unwrap();
    assert!(
        pk.contains("datacenter"),
        "expected dynamically-added `datacenter` in primary key: {create}"
    );

    // 5. An invalid `?table=` override returns HEC code 7 ("incorrect index").
    let res = send_req(
        &client,
        splunk_headers(),
        "/v1/splunk/services/collector/event?table=bad%20name",
        br#"{"event":"x","time":1700000003}"#.to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":7"));

    // 6. A custom pipeline (via `x-greptime-pipeline-name`) overrides identity and
    //    owns the schema: this one keeps host/event/timestamp and drops source/sourcetype.
    let pipeline_yaml = r#"
transform:
  - field: host
    type: string
    index: tag
  - field: event
    type: string
  - field: greptime_timestamp
    type: time
    index: timestamp
"#;
    let res = client
        .post("/v1/pipelines/splunk_custom")
        .header("Content-Type", "application/x-yaml")
        .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
        .body(pipeline_yaml)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "create pipeline failed");

    let mut headers = splunk_headers();
    headers.push((
        HeaderName::from_static("x-greptime-pipeline-name"),
        HeaderValue::from_static("splunk_custom"),
    ));
    let res = send_req(
        &client,
        headers,
        "/v1/splunk/services/collector/event?table=splunk_custom_tbl",
        br#"{"event":"hi","time":1700000010,"host":"web-09","source":"s","sourcetype":"st"}"#
            .to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let create = query(&client, "show create table splunk_custom_tbl").await;
    assert!(
        create.contains("host"),
        "custom pipeline kept host: {create}"
    );
    assert!(
        !create.contains("sourcetype"),
        "custom pipeline should have dropped sourcetype (identity would keep it): {create}"
    );

    // 7. Auth failures return HEC codes: missing token -> 2 (401), bad token -> 4 (403).
    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        )],
        event_path,
        br#"{"event":"x","time":1700000020}"#.to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::UNAUTHORIZED, res.status());
    assert!(res.text().await.contains("\"code\":2"));

    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("authorization"),
                HeaderValue::from_static("Splunk baduser:badpass"),
            ),
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/json"),
            ),
        ],
        event_path,
        br#"{"event":"x","time":1700000021}"#.to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::FORBIDDEN, res.status());
    assert!(res.text().await.contains("\"code\":4"));

    guard.remove_all().await;
}

pub async fn test_splunk_raw(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();

    let user_provider =
        user_provider_from_option("static_user_provider:cmd:greptime_user=greptime_pwd").unwrap();
    let (app, mut guard) = setup_test_http_app_with_frontend_and_user_provider(
        store_type,
        "test_splunk_raw",
        Some(user_provider),
    )
    .await;
    let client = TestClient::new(app).await;

    async fn query(client: &TestClient, sql: &str) -> String {
        let res = client
            .get(format!("/v1/sql?sql={sql}").as_str())
            .header("Authorization", basic_auth("greptime_user", "greptime_pwd"))
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK, "query failed: {sql}");
        res.text().await
    }

    // HEC `Authorization: Splunk <user:pass>` + plain-text content type (raw bodies).
    let splunk_headers = || {
        vec![
            (
                HeaderName::from_static("authorization"),
                HeaderValue::from_static("Splunk greptime_user:greptime_pwd"),
            ),
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("text/plain"),
            ),
        ]
    };
    let raw_path = "/v1/splunk/services/collector/raw";

    // 1. Explicit `?linebreaker=%0A` ("\n") splits the body into one event per
    //    line, with request-level metadata; `channel` (param AND header) is
    //    accepted and ignored (ack protocol not implemented); `?time=` sets the
    //    timestamp for every event; `index` routes the table. Blank lines are
    //    skipped; indentation inside a line is preserved verbatim.
    let mut headers = splunk_headers();
    headers.push((
        HeaderName::from_static("x-splunk-request-channel"),
        HeaderValue::from_static("FE0ECFAD-13D5-401B-847D-77833BD77131"),
    ));
    let body = "line one\nline two\n\n  indented line";
    let res = send_req(
        &client,
        headers,
        &format!(
            "{raw_path}?channel=FE0ECFAD-13D5-401B-847D-77833BD77131\
             &host=web-01&source=nginx&sourcetype=access&index=raw_main&time=1700000100\
             &linebreaker=%0A"
        ),
        body.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    assert!(res.text().await.contains("\"code\":0"));

    // 2. Rows landed: `message` holds each line verbatim, metadata columns carry the
    //    query-param values, `?time=` (epoch seconds) became the timestamp.
    let rows = get_rows_from_output(
        &query(
            &client,
            "select host, source, sourcetype, message, greptime_timestamp from raw_main order by message",
        )
        .await,
    );
    assert_eq!(
        rows,
        concat!(
            r#"[["web-01","nginx","access","  indented line",1700000100000000000],"#,
            r#"["web-01","nginx","access","line one",1700000100000000000],"#,
            r#"["web-01","nginx","access","line two",1700000100000000000]]"#
        )
    );

    // 3. host/source/sourcetype are tags (primary key); `message` is not.
    let create = query(&client, "show create table raw_main").await;
    let pk = create
        .split("PRIMARY KEY")
        .nth(1)
        .expect("raw_main should have a PRIMARY KEY");
    for col in ["host", "source", "sourcetype"] {
        assert!(
            pk.contains(col),
            "expected `{col}` in primary key: {create}"
        );
    }
    assert!(
        !pk.contains("message"),
        "`message` must not be a tag: {create}"
    );

    // 4. No query params at all: default table (`splunk_logs`), only timestamp +
    //    `message` columns, ingest-time timestamp.
    let res = send_req(
        &client,
        splunk_headers(),
        raw_path,
        b"Hello World".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let rows = get_rows_from_output(&query(&client, "select message from splunk_logs").await);
    assert_eq!(rows, r#"[["Hello World"]]"#);

    // 4b. Without `?linebreaker=`, a multiline body (e.g. a stack trace) is stored
    //     as it is.
    let stack_trace = "java.lang.NullPointerException: boom\n\tat com.example.Foo.bar(Foo.java:42)\n\tat com.example.Main.main(Main.java:7)";
    let res = send_req(
        &client,
        splunk_headers(),
        &format!("{raw_path}?table=raw_multiline"),
        stack_trace.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let rows = get_rows_from_output(&query(&client, "select message from raw_multiline").await);
    assert_eq!(
        rows,
        r#"[["java.lang.NullPointerException: boom\n\tat com.example.Foo.bar(Foo.java:42)\n\tat com.example.Main.main(Main.java:7)"]]"#
    );

    // 5. gzip-compressed raw body on the versioned alias (exercises the
    //    decompression layer and the `/raw/1.0` route).
    let res = send_req(
        &client,
        splunk_headers(),
        "/v1/splunk/services/collector/raw/1.0?table=raw_gzip",
        b"compressed line".to_vec(),
        true,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let rows = get_rows_from_output(&query(&client, "select message from raw_gzip").await);
    assert_eq!(rows, r#"[["compressed line"]]"#);

    // 6. Error paths: empty body -> 5 ("No data"); unparsable `?time=` -> 6;
    //    invalid `?table=` -> 7 ("incorrect index").
    let res = send_req(
        &client,
        splunk_headers(),
        raw_path,
        b"  \n ".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":5"));

    let res = send_req(
        &client,
        splunk_headers(),
        &format!("{raw_path}?time=not-a-time"),
        b"x".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":6"));

    let res = send_req(
        &client,
        splunk_headers(),
        &format!("{raw_path}?table=bad%20name"),
        b"x".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":7"));

    // 7. Replay of a real Vector `splunk_hec_logs` (endpoint_target = "raw") request,
    //    path and headers verbatim from a wire capture (body trimmed):
    let mut headers = splunk_headers();
    headers.push((
        HeaderName::from_static("x-splunk-request-channel"),
        HeaderValue::from_static("b408271e-51af-43c1-a99f-9c21f78df0cf"),
    ));
    let res = send_req(
        &client,
        headers,
        "/v1/splunk/services/collector/raw?source=vector%2Dsrc&sourcetype=vector%5Fdemo&index=vector_raw&host=localhost",
        b"245.158.191.1 - AnthraX [13/Jul/2026:05:10:26 +0000] \"GET /wp-admin HTTP/1.0\" 300 17922".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let rows = get_rows_from_output(
        &query(
            &client,
            "select host, source, sourcetype, message from vector_raw",
        )
        .await,
    );
    // the percent-encoded params (`vector%2Dsrc` etc.) decode to the plain values.
    assert_eq!(
        rows,
        r#"[["localhost","vector-src","vector_demo","245.158.191.1 - AnthraX [13/Jul/2026:05:10:26 +0000] \"GET /wp-admin HTTP/1.0\" 300 17922"]]"#
    );

    // 8. An unknown `Content-Encoding` passes through the decompression layer
    //    still compressed (`pass_through_unaccepted(true)`); the handler must reject
    //    it (code 6) instead of ingesting compressed bytes. `identity` is fine.
    //    (Known-but-broken encodings, e.g. `zstd` over gzip bytes, already fail in
    //    the layer itself.)
    //    Invalid UTF-8 must also be rejected with code 6, not lossily replaced.
    let mut headers = splunk_headers();
    headers.push((
        HeaderName::from_static("content-encoding"),
        HeaderValue::from_static("snappy"),
    ));
    let res = send_req(
        &client,
        headers,
        &format!("{raw_path}?table=raw_encoding"),
        compress_vec_with_gzip(b"still compressed".to_vec()),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":6"));

    let mut headers = splunk_headers();
    headers.push((
        HeaderName::from_static("content-encoding"),
        HeaderValue::from_static("identity"),
    ));
    let res = send_req(
        &client,
        headers,
        &format!("{raw_path}?table=raw_encoding"),
        b"identity line".to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let rows = get_rows_from_output(&query(&client, "select message from raw_encoding").await);
    assert_eq!(rows, r#"[["identity line"]]"#);

    let res = send_req(
        &client,
        splunk_headers(),
        &format!("{raw_path}?table=raw_bad_utf8"),
        vec![b'h', b'i', 0xff, 0xfe],
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    assert!(res.text().await.contains("\"code\":6"));

    guard.remove_all().await;
}

pub async fn test_health_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, _guard) = setup_test_http_app_with_frontend(store_type, "health_api").await;
    let client = TestClient::new(app).await;

    async fn health_api(client: &TestClient, endpoint: &str) {
        // we can call health api with both `GET` and `POST` method.
        let res_post = client.post(endpoint).send().await;
        assert_eq!(res_post.status(), StatusCode::OK);
        let res_get = client.get(endpoint).send().await;
        assert_eq!(res_get.status(), StatusCode::OK);

        // both `GET` and `POST` method return same result
        let body_text = res_post.text().await;
        assert_eq!(body_text, res_get.text().await);

        // currently health api simply returns an empty json `{}`, which can be deserialized to an empty `HealthResponse`
        assert_eq!(body_text, "{}");

        let body = serde_json::from_str::<HealthResponse>(&body_text).unwrap();
        assert_eq!(body, HealthResponse {});
    }

    health_api(&client, "/health").await;
    health_api(&client, "/ready").await;
}

pub async fn test_status_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, _guard) = setup_test_http_app_with_frontend(store_type, "status_api").await;
    let client = TestClient::new(app).await;

    let res_get = client.get("/status").send().await;
    assert_eq!(res_get.status(), StatusCode::OK);

    let res_body = res_get.text().await;
    assert!(res_body.contains("\"commit\":"));
    assert!(res_body.contains("\"branch\":"));
    assert!(res_body.contains("\"rustc_version\":"));
    assert!(res_body.contains("\"hostname\":"));
    assert!(res_body.contains("\"version\":"));
}

pub async fn test_config_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, _guard) = setup_test_http_app_with_frontend(store_type, "config_api").await;
    let client = TestClient::new(app).await;

    let res_get = client.get("/config").send().await;
    assert_eq!(res_get.status(), StatusCode::OK);

    let storage = if store_type != StorageType::File {
        format!(
            r#"[storage]
type = "{}"
providers = []

[storage.http_client]
pool_max_idle_per_host = 1024
connect_timeout = "30s"
timeout = "30s"
pool_idle_timeout = "1m 30s"
skip_ssl_validation = false"#,
            store_type
        )
    } else {
        format!(
            r#"[storage]
type = "{}"
providers = []"#,
            store_type
        )
    };

    let vector_index_config = if cfg!(feature = "vector_index") {
        r#"
[region_engine.mito.vector_index]
create_on_flush = "auto"
create_on_compaction = "auto"
apply_on_query = "auto"
mem_threshold_on_create = "auto"

"#
    } else {
        "\n"
    };

    let expected_toml_str = format!(
        r#"
enable_telemetry = true
auto_create_table = true
max_in_flight_write_bytes = "0KiB"
write_bytes_exhausted_policy = "wait"
init_regions_in_background = false
init_regions_parallelism = 16
heartbeat_env_vars = []

[http]
addr = "127.0.0.1:4000"
timeout = "0s"
body_limit = "64MiB"
cors_allowed_origins = []
enable_cors = true
enable_api_server = false
api_server_addr = "127.0.0.1:4006"

[grpc]
bind_addr = "127.0.0.1:4001"
server_addr = "127.0.0.1:4001"
max_recv_message_size = "512MiB"
max_send_message_size = "512MiB"
flight_compression = "arrow_ipc"
runtime_size = 8
http2_keep_alive_interval = "10s"
http2_keep_alive_timeout = "3s"

[grpc.tls]
mode = "disable"
cert_path = ""
key_path = ""
ca_cert_path = ""
watch = false

[mysql]
enable = true
addr = "127.0.0.1:4002"
runtime_size = 2
keep_alive = "0s"
prepared_stmt_cache_size = 10000

[mysql.tls]
mode = "disable"
cert_path = ""
key_path = ""
ca_cert_path = ""
watch = false

[postgres]
enable = true
addr = "127.0.0.1:4003"
runtime_size = 2
keep_alive = "0s"

[postgres.tls]
mode = "disable"
cert_path = ""
key_path = ""
ca_cert_path = ""
watch = false

[opentsdb]
enable = true

[influxdb]
enable = true
default_merge_mode = "last_non_null"

[jaeger]
enable = true

[otlp]
enable = true
experimental_enable_exponential_histogram = false
trace_ingest_chunk_size = 512
experimental_enable_resource_info = true

[prom_store]
enable = true
with_metric_engine = true
prom_validation_mode = "strict"
experimental_enable_prometheus_native_histogram = false
pending_rows_flush_interval = "0s"
max_batch_rows = 100000
max_concurrent_flushes = 256
worker_channel_capacity = 65526
max_inflight_requests = 3000
flow_notification_queue_capacity = 1024

[wal]
provider = "raft_engine"
file_size = "128MiB"
purge_threshold = "1GiB"
purge_interval = "1m"
read_batch_size = 128
sync_write = false
enable_log_recycle = true
prefill_log_files = false

{storage}

[metadata_store]
file_size = "64MiB"
purge_threshold = "256MiB"
purge_interval = "1m"

[procedure]
max_retry_times = 3
retry_delay = "500ms"
max_running_procedures = 128

[flow]

[flow.batching_mode]
query_timeout = "10m"
slow_query_threshold = "1m"
experimental_min_refresh_duration = "5s"
grpc_conn_timeout = "5s"
experimental_flight_do_get_timeout = "10s"
experimental_grpc_max_retries = 3
experimental_frontend_scan_timeout = "30s"
experimental_max_filter_num_per_query = 20
experimental_time_window_merge_threshold = 3
experimental_enable_incremental_read = false
read_preference = "Leader"

[logging]
max_log_files = 720
append_stdout = true
enable_file_logging = true
enable_otlp_tracing = false
enable_per_region_metrics = false

[[region_engine]]

[region_engine.mito]
worker_channel_size = 128
worker_request_batch_size = 64
manifest_checkpoint_distance = 10
experimental_manifest_keep_removed_file_count = 256
experimental_manifest_keep_removed_file_ttl = "1h"
compress_manifest = false
experimental_compaction_memory_limit = "unlimited"
experimental_compaction_on_exhausted = "wait"
auto_flush_interval = "10m"
default_region_write_buffer_size = "0KiB"
enable_write_cache = false
write_cache_path = ""
write_cache_size = "5GiB"
preload_index_cache = true
index_cache_percent = 20
enable_refill_cache_on_read = true
manifest_cache_size = "256MiB"
sst_write_buffer_size = "8MiB"
max_concurrent_scan_files = 384
allow_stale_entries = false
scan_memory_on_exhausted = "fail"
min_compaction_interval = "0s"
schedule_compaction_after_edit = true
default_flat_format = true
experimental_series_scan_v2 = true

[region_engine.mito.index]
aux_path = ""
staging_size = "2GiB"
staging_ttl = "7days"
build_mode = "sync"
write_buffer_size = "8MiB"
content_cache_page_size = "64KiB"

[region_engine.mito.inverted_index]
create_on_flush = "auto"
create_on_compaction = "auto"
apply_on_query = "auto"
mem_threshold_on_create = "auto"

[region_engine.mito.fulltext_index]
create_on_flush = "auto"
create_on_compaction = "auto"
apply_on_query = "auto"
mem_threshold_on_create = "auto"
compress = true

[region_engine.mito.bloom_filter_index]
create_on_flush = "auto"
create_on_compaction = "auto"
apply_on_query = "auto"
mem_threshold_on_create = "auto"
{vector_index_config}[region_engine.mito.gc]
enable = false
lingering_time = "1h"
unknown_file_lingering_time = "1day"
max_concurrent_lister_per_gc_job = 32
max_concurrent_gc_job = 4

[[region_engine]]

[region_engine.file]

[tracing]

[slow_query]
enable = true
record_type = "system_table"
threshold = "1s"
sample_ratio = 1.0
ttl = "2months 29days 2h 52m 48s"

[query]
parallelism = 0
allow_query_fallback = false
experimental_spill_mode = "default"
experimental_spill_max_temp_directory_size = "1GiB"
experimental_spill_compression = "uncompressed"
experimental_memory_pool_policy = "greedy"

[memory]
enable_heap_profiling = true

[event_recorder]
ttl = "2months 29days 2h 52m 48s"
"#,
    )
    .trim()
    .to_string();
    let body_text = drop_lines_with_inconsistent_results(res_get.text().await);
    similar_asserts::assert_eq!(expected_toml_str, body_text);
}

fn drop_lines_with_inconsistent_results(input: String) -> String {
    let inconsistent_results = [
        "dir =",
        "log_format =",
        "data_home =",
        "bucket =",
        "root =",
        "endpoint =",
        "region =",
        "enable_virtual_host_style =",
        "disable_ec2_metadata =",
        "cache_path =",
        "cache_capacity =",
        "memory_pool_size =",
        "scan_memory_limit =",
        "sas_token =",
        "scope =",
        "num_workers =",
        "scan_parallelism =",
        "global_write_buffer_size =",
        "global_write_buffer_reject_size =",
        "sst_meta_cache_size =",
        "vector_cache_size =",
        "page_cache_size =",
        "selector_result_cache_size =",
        "range_result_cache_size =",
        "prefilter_result_cache_size =",
        "metadata_cache_size =",
        "content_cache_size =",
        "result_cache_size =",
        "name =",
        "recovery_parallelism =",
        "max_background_index_builds =",
        "max_background_flushes =",
        "max_background_compactions =",
        "max_background_purges =",
        "enable_read_cache =",
        "max_total_body_memory =",
        "max_total_message_memory =",
    ];

    input
        .lines()
        .filter(|line| {
            // ignores
            let line = line.trim();
            for prefix in inconsistent_results {
                if line.starts_with(prefix) {
                    return false;
                }
            }
            true
        })
        .collect::<Vec<&str>>()
        .join(
            "
",
        )
}

pub async fn test_dynamic_tracer_toggle(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();

    let (app, mut guard) = setup_test_http_app(store_type, "test_dynamic_tracer_toggle").await;
    let client = TestClient::new(app).await;

    let disable_resp = client
        .post("/debug/enable_trace")
        .body("false")
        .send()
        .await;
    assert_eq!(disable_resp.status(), StatusCode::OK);
    assert_eq!(disable_resp.text().await, "trace disabled");

    let enable_resp = client.post("/debug/enable_trace").body("true").send().await;
    assert_eq!(enable_resp.status(), StatusCode::OK);
    assert_eq!(enable_resp.text().await, "trace enabled");

    let cleanup_resp = client
        .post("/debug/enable_trace")
        .body("false")
        .send()
        .await;
    assert_eq!(cleanup_resp.status(), StatusCode::OK);
    assert_eq!(cleanup_resp.text().await, "trace disabled");

    guard.remove_all().await;
}

#[cfg(feature = "dashboard")]
pub async fn test_dashboard_path(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, _guard) = setup_test_http_app_with_frontend(store_type, "dashboard_path").await;
    let client = TestClient::new(app).await;

    let res_get = client.get("/dashboard").send().await;
    assert_eq!(res_get.status(), StatusCode::PERMANENT_REDIRECT);
    let res_post = client.post("/dashboard/").send().await;
    assert_eq!(res_post.status(), StatusCode::OK);
    let res_get = client.get("/dashboard/").send().await;
    assert_eq!(res_get.status(), StatusCode::OK);

    // both `GET` and `POST` method return same result
    let body_text = res_post.text().await;
    assert_eq!(body_text, res_get.text().await);
}

#[cfg(not(feature = "dashboard"))]
pub async fn test_dashboard_path(_: StorageType) {}

#[cfg(feature = "dashboard")]
pub async fn test_dashboard_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "dashboard_api").await;
    let client = TestClient::new(app).await;

    // 1. List dashboards - should be empty initially
    let res = client.get("/v1/dashboards").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert!(dashboards.is_empty());

    // 2. Save a dashboard
    let dashboard_definition = r#"{"title": "My Dashboard", "panels": []}"#;
    let res = client
        .post("/v1/dashboards/test_dashboard")
        .body(dashboard_definition)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 1);
    assert_eq!(dashboards[0].get("name").unwrap(), "test_dashboard");

    // 3. Save another dashboard
    let res = client
        .post("/v1/dashboards/another_dashboard")
        .body(r#"{"title": "Another Dashboard"}"#)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 4. List dashboards - should have 2
    let res = client.get("/v1/dashboards").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 2);

    let names: Vec<&str> = dashboards
        .iter()
        .map(|d| d.get("name").unwrap().as_str().unwrap())
        .collect();
    assert!(names.contains(&"test_dashboard"));
    assert!(names.contains(&"another_dashboard"));

    // 5. Update a dashboard by posting again with new definition
    let updated_definition = r#"{"title": "Updated Dashboard", "panels": [{"id": 1}]}"#;
    let res = client
        .post("/v1/dashboards/test_dashboard")
        .body(updated_definition)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 1);
    assert_eq!(dashboards[0].get("name").unwrap(), "test_dashboard");

    // Verify the definition was updated by listing again
    let res = client.get("/v1/dashboards").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 2);

    // Find test_dashboard and verify it has updated definition
    let test_db = dashboards
        .iter()
        .find(|d| d.get("name").unwrap() == "test_dashboard")
        .unwrap();
    assert_eq!(
        test_db.get("definition").unwrap(),
        r#"{"title": "Updated Dashboard", "panels": [{"id": 1}]}"#
    );

    // 6. Delete one dashboard
    let res = client.delete("/v1/dashboards/test_dashboard").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 1);
    assert_eq!(dashboards[0].get("name").unwrap(), "test_dashboard");

    // 7. List dashboards - should have 1
    let res = client.get("/v1/dashboards").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert_eq!(dashboards.len(), 1);
    assert_eq!(dashboards[0].get("name").unwrap(), "another_dashboard");

    // 8. Delete the remaining dashboard
    let res = client
        .delete("/v1/dashboards/another_dashboard")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 9. List dashboards - should be empty
    let res = client.get("/v1/dashboards").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let body: Value = res.json().await;
    let dashboards = body.get("dashboards").unwrap().as_array().unwrap();
    assert!(dashboards.is_empty());

    guard.remove_all().await;
}

#[cfg(not(feature = "dashboard"))]
pub async fn test_dashboard_api(_: StorageType) {}

pub async fn test_prometheus_remote_write(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "prometheus_remote_write").await;
    let client = TestClient::new(app).await;

    // write snappy encoded data
    let write_request = WriteRequest {
        timeseries: prom_store::mock_timeseries(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // The direct Prom remote-write handler creates tables without going through
    // the generic inserter. Its submitted events must retain this origin.
    wait_for_event_data(
        &client,
        "SELECT DISTINCT json_to_string(event_context) FROM greptime_private.events WHERE type IN ('create_table', 'create_logical_tables') AND json_get_string(procedure_trigger, 'type') = 'Submitted' AND json_get_string(event_context, 'protocol') = 'prometheus' AND json_get_string(event_context, 'reason') = 'auto_create'",
        r#"[["{\"protocol\":\"prometheus\",\"reason\":\"auto_create\"}"]]"#,
    )
    .await;

    let expected = "[[\"demo\"],[\"demo_metrics\"],[\"demo_metrics_with_nanos\"],[\"greptime_physical_table\"],[\"metric1\"],[\"metric2\"],[\"metric3\"],[\"mito\"],[\"multi_labels\"],[\"numbers\"],[\"phy\"],[\"phy2\"],[\"phy_ns\"]]";
    validate_data("prometheus_remote_write", &client, "show tables;", expected).await;

    let table_val = "[[1000,3.0,\"z001\",\"test_host1\"],[2000,4.0,\"z001\",\"test_host1\"]]";
    validate_data(
        "prometheus_remote_write",
        &client,
        "select * from metric2",
        table_val,
    )
    .await;

    // Prom RW tables carry the metric identity; type is inferred from naming.
    validate_data(
        "prometheus_remote_write_semantic_identity",
        &client,
        "select count(*) from information_schema.tables where table_name = 'metric2' \
         and create_options like '%greptime.semantic.signal_type=metric%' \
         and create_options like '%greptime.semantic.source=prometheus%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=inferred%';",
        "[[1]]",
    )
    .await;

    // Write snappy encoded data with new labels
    let write_request = WriteRequest {
        timeseries: mock_timeseries_new_label(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // Adding a label is handled by PendingRowsSchemaAlterer, which is another
    // direct DDL path outside the generic inserter.
    wait_for_event_data(
        &client,
        "SELECT DISTINCT json_to_string(event_context) FROM greptime_private.events WHERE type IN ('alter_table', 'alter_logical_tables') AND json_get_string(procedure_trigger, 'type') = 'Submitted' AND json_get_string(event_context, 'protocol') = 'prometheus' AND json_get_string(event_context, 'reason') = 'auto_alter'",
        r#"[["{\"protocol\":\"prometheus\",\"reason\":\"auto_alter\"}"]]"#,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_prometheus_remote_write_v2(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "prometheus_remote_write_v2").await;
    let client = TestClient::new(app).await;

    let write_request = remote_write_v2::request_with_labels_and_samples(
        vec![
            (prom_store::METRIC_NAME_LABEL, "remote_write_v2_total"),
            ("job", "api"),
            ("instance", "localhost:9090"),
        ],
        vec![
            RemoteWriteV2Sample {
                value: 42.0,
                timestamp: 1000,
                start_timestamp: 0,
            },
            RemoteWriteV2Sample {
                value: 43.0,
                timestamp: 2000,
                start_timestamp: 0,
            },
        ],
    );
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .header(
            "Content-Type",
            "application/x-protobuf;proto=io.prometheus.write.v2.Request",
        )
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);
    let headers = res.headers();
    assert_remote_write_v2_written_headers(&headers, "2");
    assert!(res.text().await.is_empty());

    validate_data(
        "prometheus_remote_write_v2_rows",
        &client,
        "select greptime_timestamp, greptime_value, job, instance from remote_write_v2_total order by greptime_timestamp;",
        "[[1000,42.0,\"api\",\"localhost:9090\"],[2000,43.0,\"api\",\"localhost:9090\"]]",
    )
        .await;

    validate_data(
        "prometheus_remote_write_v2_semantic_identity",
        &client,
        "select count(*) from information_schema.tables where table_name = 'remote_write_v2_total' \
         and create_options like '%greptime.semantic.signal_type=metric%' \
         and create_options like '%greptime.semantic.source=prometheus%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=inferred%';",
        "[[1]]",
    )
    .await;

    // A series carrying inline metadata (v2 senders SHOULD) upgrades its
    // table's quality to declared, with the metric type and the unit
    // canonicalised from the OpenMetrics word to UCUM.
    let mut write_request = remote_write_v2::request_with_labels_and_samples(
        vec![
            (prom_store::METRIC_NAME_LABEL, "remote_write_v2_typed_total"),
            ("job", "api"),
        ],
        vec![RemoteWriteV2Sample {
            value: 1.0,
            timestamp: 1000,
            start_timestamp: 0,
        }],
    );
    let unit_ref = write_request.symbols.len() as u32;
    write_request.symbols.push("seconds".to_string());
    write_request.timeseries[0].metadata = Some(RemoteWriteV2Metadata {
        r#type: RemoteWriteV2MetricType::Counter as i32,
        help_ref: 0,
        unit_ref,
    });
    let compressed_request = prom_store::snappy_compress(&write_request.encode_to_vec())
        .expect("failed to encode snappy");
    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .header(
            "Content-Type",
            "application/x-protobuf;proto=io.prometheus.write.v2.Request",
        )
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    validate_data(
        "prometheus_remote_write_v2_declared_metadata",
        &client,
        "select count(*) from information_schema.tables where table_name = 'remote_write_v2_typed_total' \
         and create_options like '%greptime.semantic.metric.type=counter%' \
         and create_options like '%greptime.semantic.metric.unit=s%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=declared%';",
        "[[1]]",
    )
    .await;

    let res = client
        .get("/v1/prometheus/api/v1/metadata?metric=remote_write_v2_typed_total")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(
        serde_json::to_string_pretty(&body).unwrap(),
        r#"{
  "status": "success",
  "data": {
    "remote_write_v2_typed_total": [
      {
        "type": "counter",
        "unit": "seconds",
        "help": ""
      }
    ]
  }
}"#
    );

    guard.remove_all().await;
}

pub async fn test_prometheus_remote_write_v2_native_histogram(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_prom_app_with_frontend_native_histogram(
        store_type,
        "prometheus_remote_write_v2_native_histogram",
    )
    .await;
    let client = TestClient::new(app).await;

    let mut write_request = remote_write_v2::request_with_labels_and_samples(
        vec![
            (
                prom_store::METRIC_NAME_LABEL,
                "remote_write_v2_sample_seconds",
            ),
            ("job", "api"),
            ("instance", "localhost:9090"),
        ],
        vec![RemoteWriteV2Sample {
            value: 5.0,
            timestamp: 2500,
            start_timestamp: 0,
        }],
    );
    let name_ref = write_request
        .symbols
        .iter()
        .position(|symbol| symbol == prom_store::METRIC_NAME_LABEL)
        .unwrap() as u32;
    let job_ref = write_request
        .symbols
        .iter()
        .position(|symbol| symbol == "job")
        .unwrap() as u32;
    let api_ref = write_request
        .symbols
        .iter()
        .position(|symbol| symbol == "api")
        .unwrap() as u32;
    let instance_ref = write_request
        .symbols
        .iter()
        .position(|symbol| symbol == "instance")
        .unwrap() as u32;
    let localhost_ref = write_request
        .symbols
        .iter()
        .position(|symbol| symbol == "localhost:9090")
        .unwrap() as u32;
    let histogram_metric_ref = write_request.symbols.len() as u32;
    write_request
        .symbols
        .push("remote_write_v2_latency_seconds".to_string());
    write_request.timeseries.push(RemoteWriteV2TimeSeries {
        labels_refs: vec![
            name_ref,
            histogram_metric_ref,
            job_ref,
            api_ref,
            instance_ref,
            localhost_ref,
        ],
        histograms: vec![
            Histogram {
                count: Some(Count::CountInt(8)),
                sum: 10.0,
                schema: 1,
                zero_threshold: 0.001,
                zero_count: Some(ZeroCount::ZeroCountInt(1)),
                negative_spans: vec![BucketSpan {
                    offset: -2,
                    length: 1,
                }],
                negative_deltas: vec![1],
                positive_spans: vec![BucketSpan {
                    offset: 0,
                    length: 3,
                }],
                positive_deltas: vec![1, 2, -1],
                reset_hint: 2,
                timestamp: 3000,
                start_timestamp: 1500,
                ..Default::default()
            },
            Histogram {
                count: Some(Count::CountFloat(6.0)),
                sum: 20.0,
                schema: 2,
                zero_threshold: 0.002,
                zero_count: Some(ZeroCount::ZeroCountFloat(0.5)),
                positive_spans: vec![BucketSpan {
                    offset: 3,
                    length: 2,
                }],
                positive_counts: vec![2.0, 3.5],
                reset_hint: 3,
                timestamp: 4000,
                start_timestamp: 2500,
                ..Default::default()
            },
        ],
        ..Default::default()
    });
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .header(
            "Content-Type",
            "application/x-protobuf;proto=io.prometheus.write.v2.Request",
        )
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);
    let headers = res.headers();
    assert_remote_write_v2_written_headers_with_histograms(&headers, "1", "2");
    assert!(res.text().await.is_empty());

    validate_data(
        "prometheus_remote_write_v2_native_histogram_sample_rows",
        &client,
        "select greptime_timestamp, greptime_value, job, instance from remote_write_v2_sample_seconds order by greptime_timestamp;",
        "[[2500,5.0,\"api\",\"localhost:9090\"]]",
    )
    .await;

    validate_data(
        "prometheus_remote_write_v2_native_histogram_rows",
        &client,
        "select greptime_timestamp, greptime_native_histogram, job, instance from remote_write_v2_latency_seconds order by greptime_timestamp;",
        "[[3000,{\"count_f64\":null,\"count_i64\":8,\"custom_values\":[],\"negative_buckets_f64\":[],\"negative_buckets_i64\":[1],\"negative_span_lengths\":[1],\"negative_span_offsets\":[-2],\"positive_buckets_f64\":[],\"positive_buckets_i64\":[1,3,2],\"positive_span_lengths\":[3],\"positive_span_offsets\":[0],\"reset_hint\":2,\"schema\":1,\"start_timestamp\":1500,\"sum\":10.0,\"zero_count_f64\":null,\"zero_count_i64\":1,\"zero_threshold\":0.001},\"api\",\"localhost:9090\"],[4000,{\"count_f64\":6.0,\"count_i64\":null,\"custom_values\":[],\"negative_buckets_f64\":[],\"negative_buckets_i64\":[],\"negative_span_lengths\":[],\"negative_span_offsets\":[],\"positive_buckets_f64\":[2.0,3.5],\"positive_buckets_i64\":[],\"positive_span_lengths\":[2],\"positive_span_offsets\":[3],\"reset_hint\":3,\"schema\":2,\"start_timestamp\":2500,\"sum\":20.0,\"zero_count_f64\":0.5,\"zero_count_i64\":null,\"zero_threshold\":0.002},\"api\",\"localhost:9090\"]]",
    )
    .await;

    let res = client
        .get("/v1/prometheus/api/v1/metadata?metric=remote_write_v2_latency_seconds")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(
        serde_json::to_string_pretty(&body).unwrap(),
        r#"{
  "status": "success",
  "data": {
    "remote_write_v2_latency_seconds": [
      {
        "type": "histogram",
        "unit": "",
        "help": ""
      }
    ]
  }
}"#
    );

    let res = client
        .get("/v1/prometheus/api/v1/query?query=histogram_count(remote_write_v2_latency_seconds)&time=4")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    // Finite vector sample values use the HTTP response's ryu contract, so
    // integral f64 values retain their explicit `.0`; timestamps remain JSON numbers.
    assert_eq!(
        serde_json::to_string_pretty(&body).unwrap(),
        r#"{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {
        "metric": {
          "__name__": "remote_write_v2_latency_seconds",
          "instance": "localhost:9090",
          "job": "api"
        },
        "value": [
          4.0,
          "6.0"
        ]
      }
    ]
  }
}"#
    );

    let res = client
        .get("/v1/prometheus/api/v1/query?query=remote_write_v2_latency_seconds&time=4")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(
        serde_json::to_string_pretty(&body).unwrap(),
        r#"{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {
        "metric": {
          "__name__": "remote_write_v2_latency_seconds",
          "instance": "localhost:9090",
          "job": "api"
        },
        "histogram": [
          4.0,
          {
            "count": "6",
            "sum": "20",
            "buckets": [
              [
                3,
                "-0.002",
                "0.002",
                "0.5"
              ],
              [
                0,
                "1.4142135623730951",
                "1.681792830507429",
                "2"
              ],
              [
                0,
                "1.681792830507429",
                "2",
                "3.5"
              ]
            ]
          }
        ]
      }
    ]
  }
}"#
    );

    let res = client
        .get("/v1/prometheus/api/v1/query_range?query=remote_write_v2_latency_seconds&start=3&end=4&step=1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let body = res.json::<PrometheusJsonResponse>().await;
    assert_eq!(
        serde_json::to_string_pretty(&body).unwrap(),
        r#"{
  "status": "success",
  "data": {
    "resultType": "matrix",
    "result": [
      {
        "metric": {
          "__name__": "remote_write_v2_latency_seconds",
          "instance": "localhost:9090",
          "job": "api"
        },
        "histograms": [
          [
            3.0,
            {
              "count": "8",
              "sum": "10",
              "buckets": [
                [
                  1,
                  "-0.5",
                  "-0.3535533905932738",
                  "1"
                ],
                [
                  3,
                  "-0.001",
                  "0.001",
                  "1"
                ],
                [
                  0,
                  "0.7071067811865476",
                  "1",
                  "1"
                ],
                [
                  0,
                  "1",
                  "1.4142135623730951",
                  "3"
                ],
                [
                  0,
                  "1.4142135623730951",
                  "2",
                  "2"
                ]
              ]
            }
          ],
          [
            4.0,
            {
              "count": "6",
              "sum": "20",
              "buckets": [
                [
                  3,
                  "-0.002",
                  "0.002",
                  "0.5"
                ],
                [
                  0,
                  "1.4142135623730951",
                  "1.681792830507429",
                  "2"
                ],
                [
                  0,
                  "1.681792830507429",
                  "2",
                  "3.5"
                ]
              ]
            }
          ]
        ]
      }
    ]
  }
}"#
    );

    validate_data(
        "prometheus_remote_write_v2_native_histogram_table_created",
        &client,
        "select count(*) from information_schema.tables where table_name = 'remote_write_v2_latency_seconds';",
        "[[1]]",
    )
    .await;

    validate_data(
        "prometheus_remote_write_v2_native_histogram_suffix_table_not_created",
        &client,
        "select count(*) from information_schema.tables where table_name = 'remote_write_v2_latency_seconds_native_histogram';",
        "[[0]]",
    )
    .await;

    guard.remove_all().await;
}

/// Covers the batched (pending-rows-batcher) Prometheus remote write path, which
/// bypasses `PromStoreProtocolHandler::write`. Verifies the metric table is created
/// asynchronously and still carries the Prometheus semantic identity stamped on the
/// shared request context.
pub async fn test_prometheus_remote_write_batched(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend_batched(store_type, "prometheus_remote_write_batched")
            .await;
    let client = TestClient::new(app).await;

    let write_request = WriteRequest {
        timeseries: prom_store::mock_timeseries(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // The batcher flushes asynchronously, so poll until the table exists and carries
    // the semantic identity (signal_type/source/metadata_quality).
    wait_for_data(
        &client,
        "select count(*) from information_schema.tables where table_name = 'metric2' \
         and create_options like '%greptime.semantic.signal_type=metric%' \
         and create_options like '%greptime.semantic.source=prometheus%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=inferred%'",
        "[[1]]",
    )
    .await;

    // The batched path bypasses the operator's auto-create: v2 per-series
    // metadata must reach its table options through the batch create too.
    let mut write_request = remote_write_v2::request_with_labels_and_samples(
        vec![
            (
                prom_store::METRIC_NAME_LABEL,
                "remote_write_batched_typed_total",
            ),
            ("job", "api"),
        ],
        vec![RemoteWriteV2Sample {
            value: 1.0,
            timestamp: 1000,
            start_timestamp: 0,
        }],
    );
    let unit_ref = write_request.symbols.len() as u32;
    write_request.symbols.push("bytes".to_string());
    write_request.timeseries[0].metadata = Some(RemoteWriteV2Metadata {
        r#type: RemoteWriteV2MetricType::Gauge as i32,
        help_ref: 0,
        unit_ref,
    });
    let compressed_request = prom_store::snappy_compress(&write_request.encode_to_vec())
        .expect("failed to encode snappy");
    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .header(
            "Content-Type",
            "application/x-protobuf;proto=io.prometheus.write.v2.Request",
        )
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    wait_for_data(
        &client,
        "select count(*) from information_schema.tables where table_name = 'remote_write_batched_typed_total' \
         and create_options like '%greptime.semantic.metric.type=gauge%' \
         and create_options like '%greptime.semantic.metric.unit=By%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=declared%'",
        "[[1]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_prometheus_remote_special_labels(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "test_prometheus_remote_special_labels")
            .await;
    let client = TestClient::new(app).await;

    // write snappy encoded data
    let write_request = WriteRequest {
        timeseries: prom_store::mock_timeseries_special_labels(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    // create databases
    let res = client
        .post("/v1/sql?sql=create database idc3")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .post("/v1/sql?sql=create database idc4")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // write data
    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // test idc3
    let expected = "[[\"f1\"],[\"idc3_lo_table\"]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc3",
        &client,
        "show tables from idc3;",
        expected,
    )
    .await;
    let expected = "[[\"idc3_lo_table\",\"CREATE TABLE IF NOT EXISTS \\\"idc3_lo_table\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"greptime_value\\\" DOUBLE NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n  \'comment\' = 'Created on insertion',\\n  'greptime.semantic.metric.metadata_quality' = 'inferred',\\n  'greptime.semantic.signal_type' = 'metric',\\n  'greptime.semantic.source' = 'prometheus',\\n  'greptime.semantic.source_version' = '1.0',\\n  on_physical_table = 'f1'\\n)\"]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc3_show_create_table",
        &client,
        "show create table idc3.idc3_lo_table",
        expected,
    )
    .await;
    let expected = "[[3000,42.0]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc3_select",
        &client,
        "select * from idc3.idc3_lo_table",
        expected,
    )
    .await;

    // test idc4
    let expected = "[[\"f2\"],[\"idc4_local_table\"]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc4",
        &client,
        "show tables from idc4;",
        expected,
    )
    .await;
    let expected = "[[\"idc4_local_table\",\"CREATE TABLE IF NOT EXISTS \\\"idc4_local_table\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"greptime_value\\\" DOUBLE NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n  \'comment\' = 'Created on insertion',\\n  'greptime.semantic.metric.metadata_quality' = 'inferred',\\n  'greptime.semantic.signal_type' = 'metric',\\n  'greptime.semantic.source' = 'prometheus',\\n  'greptime.semantic.source_version' = '1.0',\\n  on_physical_table = 'f2'\\n)\"]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc4_show_create_table",
        &client,
        "show create table idc4.idc4_local_table",
        expected,
    )
    .await;
    let expected = "[[4000,99.0]]";
    validate_data(
        "test_prometheus_remote_special_labels_idc4_select",
        &client,
        "select * from idc4.idc4_local_table",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_prometheus_remote_write_with_pipeline(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "prometheus_remote_write_with_pipeline")
            .await;
    let client = TestClient::new(app).await;

    // write snappy encoded data
    let write_request = WriteRequest {
        timeseries: prom_store::mock_timeseries(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        prom_store::snappy_compress(&serialized_request).expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .header("x-greptime-log-pipeline-name", "greptime_identity")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    let expected = "[[\"demo\"],[\"demo_metrics\"],[\"demo_metrics_with_nanos\"],[\"greptime_physical_table\"],[\"metric1\"],[\"metric2\"],[\"metric3\"],[\"mito\"],[\"multi_labels\"],[\"numbers\"],[\"phy\"],[\"phy2\"],[\"phy_ns\"]]";
    validate_data(
        "prometheus_remote_write_pipeline",
        &client,
        "show tables;",
        expected,
    )
    .await;

    let table_val = "[[1000,3.0,\"z001\",\"test_host1\"],[2000,4.0,\"z001\",\"test_host1\"]]";
    validate_data(
        "prometheus_remote_write_pipeline",
        &client,
        "select * from metric2",
        table_val,
    )
    .await;

    guard.remove_all().await;
}

fn schema_series(schema: &str, value: f64) -> TimeSeries {
    TimeSeries {
        labels: vec![
            Label {
                name: "__name__".to_string(),
                value: "schema_metric".to_string(),
            },
            Label {
                name: "__schema__".to_string(),
                value: schema.to_string(),
            },
            Label {
                name: "instance".to_string(),
                value: "host1".to_string(),
            },
        ],
        samples: vec![Sample {
            value,
            timestamp: 1000,
        }],
        ..Default::default()
    }
}

fn schema_query(schema: Option<&str>) -> Query {
    let mut matchers = vec![LabelMatcher {
        name: "__name__".to_string(),
        value: "schema_metric".to_string(),
        r#type: MatcherType::Eq as i32,
    }];
    if let Some(schema) = schema {
        matchers.push(LabelMatcher {
            name: "__schema__".to_string(),
            value: schema.to_string(),
            r#type: MatcherType::Eq as i32,
        });
    }

    Query {
        start_timestamp_ms: 500,
        end_timestamp_ms: 1500,
        matchers,
        ..Default::default()
    }
}

async fn post_remote_read(client: &TestClient, queries: Vec<Query>) -> ReadResponse {
    let read_request = ReadRequest {
        queries,
        ..Default::default()
    };
    let compressed_request = prom_store::snappy_compress(&read_request.encode_to_vec())
        .expect("failed to encode snappy");

    let response = client
        .post("/v1/prometheus/read")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);

    let response_body = response.bytes().await;
    let decompressed_response = prom_store::snappy_decompress(&response_body).unwrap();
    ReadResponse::decode(decompressed_response.as_slice()).unwrap()
}

async fn post_remote_read_from_schema_a(client: &TestClient, queries: Vec<Query>) -> ReadResponse {
    let read_request = ReadRequest {
        queries,
        ..Default::default()
    };
    let compressed_request = prom_store::snappy_compress(&read_request.encode_to_vec())
        .expect("failed to encode snappy");

    let response = client
        .post("/v1/prometheus/read")
        .header(GREPTIME_DB_HEADER_NAME.clone(), "schema_a")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);

    let response_body = response.bytes().await;
    let decompressed_response = prom_store::snappy_decompress(&response_body).unwrap();
    ReadResponse::decode(decompressed_response.as_slice()).unwrap()
}

fn read_sample_values(read_response: ReadResponse) -> Vec<f64> {
    read_response
        .results
        .into_iter()
        .map(|result| {
            assert_eq!(result.timeseries.len(), 1);
            let timeseries = &result.timeseries[0];
            assert_eq!(timeseries.samples.len(), 1);
            assert_eq!(timeseries.samples[0].timestamp, 1000);
            timeseries.samples[0].value
        })
        .collect()
}

pub async fn test_prometheus_remote_schema_labels(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "test_prometheus_remote_schema_labels").await;
    let client = TestClient::new(app).await;

    const SCHEMA_A: &str = "schema_a";
    const SCHEMA_B: &str = "schema_b";

    for schema in [SCHEMA_A, SCHEMA_B] {
        let res = client
            .post(&format!("/v1/sql?sql=create database {schema}"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
    }

    // Both series have the same metric, non-schema labels, and timestamp.
    let write_request = WriteRequest {
        timeseries: vec![
            schema_series(SCHEMA_A, 101.0),
            schema_series(SCHEMA_B, 202.0),
        ],
        ..Default::default()
    };
    let compressed_request = prom_store::snappy_compress(&write_request.encode_to_vec())
        .expect("failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // Independent controls establish that each selector resolves its own schema.
    assert_eq!(
        read_sample_values(post_remote_read(&client, vec![schema_query(Some(SCHEMA_A))]).await,),
        vec![101.0]
    );
    assert_eq!(
        read_sample_values(post_remote_read(&client, vec![schema_query(Some(SCHEMA_B))]).await,),
        vec![202.0]
    );

    // Results must stay aligned with the request query order.
    let forward_values = read_sample_values(
        post_remote_read(
            &client,
            vec![schema_query(Some(SCHEMA_A)), schema_query(Some(SCHEMA_B))],
        )
        .await,
    );
    let reverse_values = read_sample_values(
        post_remote_read(
            &client,
            vec![schema_query(Some(SCHEMA_B)), schema_query(Some(SCHEMA_A))],
        )
        .await,
    );
    assert_eq!(
        (forward_values, reverse_values),
        (vec![101.0, 202.0], vec![202.0, 101.0])
    );

    // With request database A, unqualified queries must resolve to A independently.
    let explicit_then_default_values = read_sample_values(
        post_remote_read_from_schema_a(
            &client,
            vec![schema_query(Some(SCHEMA_B)), schema_query(None)],
        )
        .await,
    );
    let default_then_explicit_values = read_sample_values(
        post_remote_read_from_schema_a(
            &client,
            vec![schema_query(None), schema_query(Some(SCHEMA_B))],
        )
        .await,
    );
    assert_eq!(
        (explicit_then_default_values, default_then_explicit_values),
        (vec![202.0, 101.0], vec![101.0, 202.0])
    );

    // write data to unknown schema
    let unknown_schema_series = TimeSeries {
        labels: vec![
            Label {
                name: "__name__".to_string(),
                value: "metric_unknown_schema".to_string(),
            },
            Label {
                name: "__schema__".to_string(),
                value: "unknown_schema".to_string(),
            },
            Label {
                name: "instance".to_string(),
                value: "host2".to_string(),
            },
        ],
        samples: vec![Sample {
            value: 200.0,
            timestamp: 2000,
        }],
        ..Default::default()
    };

    let unknown_write_request = WriteRequest {
        timeseries: vec![unknown_schema_series],
        ..Default::default()
    };
    let serialized_unknown_request = unknown_write_request.encode_to_vec();
    let compressed_unknown_request =
        prom_store::snappy_compress(&serialized_unknown_request).expect("failed to encode snappy");

    // Write data to unknown schema
    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_unknown_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    // Read data from unknown schema
    let unknown_read_request = ReadRequest {
        queries: vec![Query {
            start_timestamp_ms: 1500,
            end_timestamp_ms: 2500,
            matchers: vec![
                LabelMatcher {
                    name: "__name__".to_string(),
                    value: "metric_unknown_schema".to_string(),
                    r#type: MatcherType::Eq as i32,
                },
                LabelMatcher {
                    name: "__schema__".to_string(),
                    value: "unknown_schema".to_string(),
                    r#type: MatcherType::Eq as i32,
                },
            ],
            ..Default::default()
        }],
        ..Default::default()
    };

    let serialized_unknown_read_request = unknown_read_request.encode_to_vec();
    let compressed_unknown_read_request =
        prom_store::snappy_compress(&serialized_unknown_read_request)
            .expect("failed to encode snappy");

    let unknown_result = client
        .post("/v1/prometheus/read")
        .body(compressed_unknown_read_request)
        .send()
        .await;
    assert_eq!(unknown_result.status(), StatusCode::BAD_REQUEST);

    guard.remove_all().await;
}

pub async fn test_vm_proto_remote_write(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_prom_app_with_frontend(store_type, "vm_proto_remote_write").await;

    // handshake
    let client = TestClient::new(app).await;
    let res = client
        .post("/v1/prometheus/write?get_vm_proto_version=1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    assert_eq!(res.text().await, "1");

    // write zstd encoded data
    let write_request = WriteRequest {
        timeseries: prom_store::mock_timeseries(),
        ..Default::default()
    };
    let serialized_request = write_request.encode_to_vec();
    let compressed_request =
        zstd::stream::encode_all(&serialized_request[..], 1).expect("Failed to encode zstd");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "zstd")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // also test fallback logic, vmagent could sent data in wrong content-type
    // we encode it as zstd but send it as snappy
    let compressed_request =
        zstd::stream::encode_all(&serialized_request[..], 1).expect("Failed to encode zstd");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "snappy")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // reversed
    let compressed_request =
        prom_store::snappy_compress(&serialized_request[..]).expect("Failed to encode snappy");

    let res = client
        .post("/v1/prometheus/write")
        .header("Content-Encoding", "zstd")
        .body(compressed_request)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    guard.remove_all().await;
}

pub async fn test_pipeline_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_pipeline_api").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - fields:
      - id1
      - id2
    type: int32
    index: inverted
  - fields:
      - logger
    type: string
  - field: type
    type: string
    index: skipping
    tag: true
  - field: log
    type: string
    index: fulltext
    tag: true
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/pipelines/greptime_guagua")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    assert_eq!(
        res.json::<serde_json::Value>().await["error"]
            .as_str()
            .unwrap(),
        "Invalid request parameter: pipeline_name cannot start with greptime_"
    );

    let res = client
        .post("/v1/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let content = res.text().await;

    let content = serde_json::from_str(&content);
    assert!(content.is_ok());
    //  {"execution_time_ms":13,"pipelines":[{"name":"test","version":"2024-07-04 08:31:00.987136"}]}
    let content: Value = content.unwrap();

    let execution_time = content.get("execution_time_ms");
    assert!(execution_time.unwrap().is_number());
    let pipelines = content.get("pipelines");
    let pipelines = pipelines.unwrap().as_array().unwrap();
    assert_eq!(pipelines.len(), 1);
    let pipeline = pipelines.first().unwrap();
    assert_eq!(pipeline.get("name").unwrap(), "test");

    let version_str = pipeline
        .get("version")
        .unwrap()
        .as_str()
        .unwrap()
        .to_string();
    let encoded_ver_str: String =
        url::form_urlencoded::byte_serialize(version_str.as_bytes()).collect();

    // get pipeline
    let res = client
        .get(format!("/v1/pipelines/test?version={}", encoded_ver_str).as_str())
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let content = res.text().await;
    let content = serde_json::from_str(&content);
    let content: Value = content.unwrap();
    let pipeline_yaml = content
        .get("pipelines")
        .unwrap()
        .as_array()
        .unwrap()
        .first()
        .unwrap()
        .get("pipeline")
        .unwrap()
        .as_str()
        .unwrap();
    let docs = YamlLoader::load_from_str(pipeline_yaml).unwrap();
    let body_yaml = YamlLoader::load_from_str(pipeline_body).unwrap();
    assert_eq!(docs, body_yaml);

    // Do not specify version, get the latest version
    let res = client.get("/v1/pipelines/test").send().await;
    assert_eq!(res.status(), StatusCode::OK);

    let content = res.text().await;
    let content = serde_json::from_str(&content);
    let content: Value = content.unwrap();
    let pipeline_yaml = content
        .get("pipelines")
        .unwrap()
        .as_array()
        .unwrap()
        .first()
        .unwrap()
        .get("pipeline")
        .unwrap()
        .as_str()
        .unwrap();
    let docs = YamlLoader::load_from_str(pipeline_yaml).unwrap();
    assert_eq!(docs, body_yaml);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "I",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/ingest?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. check schema
    let expected_schema = "[[\"logs1\",\"CREATE TABLE IF NOT EXISTS \\\"logs1\\\" (\\n  \\\"id1\\\" INT NULL INVERTED INDEX,\\n  \\\"id2\\\" INT NULL INVERTED INDEX,\\n  \\\"logger\\\" STRING NULL,\\n  \\\"type\\\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\\n  \\\"log\\\" STRING NULL FULLTEXT INDEX WITH(analyzer = 'English', backend = 'bloom', case_sensitive = 'false', false_positive_rate = '0.01', granularity = '10240'),\\n  \\\"time\\\" TIMESTAMP(9) NOT NULL,\\n  TIME INDEX (\\\"time\\\"),\\n  PRIMARY KEY (\\\"type\\\", \\\"log\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  \'comment\' = 'Created on insertion',\\n  append_mode = 'true'\\n)\"]]";
    validate_data(
        "pipeline_schema",
        &client,
        "show create table logs1",
        expected_schema,
    )
    .await;

    // 4. cross-ref pipeline
    // create database test_db
    let res = client
        .post("/v1/sql?sql=create database test_db")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // check test_db created
    validate_data(
        "pipeline_db_schema",
        &client,
        "show databases",
        "[[\"greptime_private\"],[\"information_schema\"],[\"public\"],[\"test_db\"]]",
    )
    .await;

    // cross ref using public's pipeline
    let res = client
        .post("/v1/ingest?db=test_db&table=logs1&pipeline_name=test")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // check write success
    validate_data(
        "pipeline_db_schema",
        &client,
        "select * from test_db.logs1",
        "[[2436,2528,\"INTERACT.MANAGER\",\"I\",\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",1716668197217000000]]",
    )
    .await;

    // 5. remove pipeline
    let encoded_ver_str: String =
        url::form_urlencoded::byte_serialize(version_str.as_bytes()).collect();
    let res = client
        .delete(format!("/v1/pipelines/test?version={}", encoded_ver_str).as_str())
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // {"pipelines":[{"name":"test","version":"2024-07-04 08:55:29.038347"}],"execution_time_ms":22}
    let content = res.text().await;
    let content: Value = serde_json::from_str(&content).unwrap();
    assert!(content.get("execution_time_ms").unwrap().is_number());

    assert_eq!(
        content.get("pipelines").unwrap().to_string(),
        format!(r#"[{{"name":"test","version":"{}"}}]"#, version_str).as_str()
    );

    // 6. write data failed
    let res = client
        .post("/v1/ingest?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    // todo(shuiyisong): refactor http error handling
    assert_ne!(res.status(), StatusCode::OK);

    guard.remove_all().await;
}

pub async fn test_pipeline_name_in_header(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_name_in_header").await;

    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true
transform:
  - field: message
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // create pipeline named `test`
    let res = client
        .post("/v1/events/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let data_body = r#"
[
  {
    "time": "2024-05-25 20:16:37.217",
    "message": "hello header"
  }
]
"#;

    // 1. pipeline name provided via the `x-greptime-pipeline-name` header,
    //    without the `pipeline_name` query parameter.
    let res = client
        .post("/v1/events/logs?db=public&table=logs1")
        .header("Content-Type", "application/json")
        .header("x-greptime-pipeline-name", "test")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    validate_data(
        "pipeline_name_header",
        &client,
        "select message from logs1",
        "[[\"hello header\"]]",
    )
    .await;

    // 2. the deprecated `x-greptime-log-pipeline-name` header is also accepted.
    let res = client
        .post("/v1/events/logs?db=public&table=logs2")
        .header("Content-Type", "application/json")
        .header("x-greptime-log-pipeline-name", "test")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    validate_data(
        "pipeline_name_log_header",
        &client,
        "select message from logs2",
        "[[\"hello header\"]]",
    )
    .await;

    // 3. the header takes precedence over the query parameter. A bogus
    //    `pipeline_name` query param is overridden by the valid header.
    let res = client
        .post("/v1/events/logs?db=public&table=logs3&pipeline_name=does_not_exist")
        .header("Content-Type", "application/json")
        .header("x-greptime-pipeline-name", "test")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    validate_data(
        "pipeline_name_header_priority",
        &client,
        "select message from logs3",
        "[[\"hello header\"]]",
    )
    .await;

    // 4. when both headers are present, the non-deprecated
    //    `x-greptime-pipeline-name` wins over the deprecated
    //    `x-greptime-log-pipeline-name`.
    let res = client
        .post("/v1/events/logs?db=public&table=logs4")
        .header("Content-Type", "application/json")
        .header("x-greptime-pipeline-name", "test")
        .header("x-greptime-log-pipeline-name", "does_not_exist")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    validate_data(
        "pipeline_name_header_non_deprecated_priority",
        &client,
        "select message from logs4",
        "[[\"hello header\"]]",
    )
    .await;

    // 5. missing pipeline name in both header and query param is rejected.
    let res = client
        .post("/v1/events/logs?db=public&table=logs5")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    guard.remove_all().await;
}

pub async fn test_identity_pipeline(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_identity_pipeline").await;

    // handshake
    let client = TestClient::new(app).await;
    let body = r#"{"__time__":1453809242,"__topic__":"","__source__":"10.170.***.***","ip":"10.200.**.***","time":"26/Jan/2016:19:54:02 +0800","url":"POST/PutData?Category=YunOsAccountOpLog&AccessKeyId=<yourAccessKeyId>&Date=Fri%2C%2028%20Jun%202013%2006%3A53%3A30%20GMT&Topic=raw&Signature=<yourSignature>HTTP/1.1","status":"200","user-agent":"aliyun-sdk-java", "json_object": {"a":1,"b":2}, "json_array":[1,2,3]}
{"__time__":1453809242,"__topic__":"","__source__":"10.170.***.***","ip":"10.200.**.***","time":"26/Jan/2016:19:54:02 +0800","url":"POST/PutData?Category=YunOsAccountOpLog&AccessKeyId=<yourAccessKeyId>&Date=Fri%2C%2028%20Jun%202013%2006%3A53%3A30%20GMT&Topic=raw&Signature=<yourSignature>HTTP/1.1","status":"200","user-agent":"aliyun-sdk-java","hasagei":"hasagei","dongdongdong":"guaguagua", "json_object": {"a":1,"b":2}, "json_array":[1,2,3]}"#;
    let res = client
        .post("/v1/ingest?db=public&table=logs&pipeline_name=greptime_identity")
        .header("Content-Type", "application/json")
        .body(body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let body: serde_json::Value = res.json().await;

    assert!(body.get("execution_time_ms").unwrap().is_number());
    assert_eq!(body["output"][0]["affectedrows"], 2);

    let res = client.get("/v1/sql?sql=select * from logs").send().await;

    assert_eq!(res.status(), StatusCode::OK);

    let line1_expected = r#"[null,"10.170.***.***",1453809242,"","10.200.**.***","[1,2,3]",1,2,"200","26/Jan/2016:19:54:02 +0800","POST/PutData?Category=YunOsAccountOpLog&AccessKeyId=<yourAccessKeyId>&Date=Fri%2C%2028%20Jun%202013%2006%3A53%3A30%20GMT&Topic=raw&Signature=<yourSignature>HTTP/1.1","aliyun-sdk-java",null,null]"#;
    let line2_expected = r#"[null,"10.170.***.***",1453809242,"","10.200.**.***","[1,2,3]",1,2,"200","26/Jan/2016:19:54:02 +0800","POST/PutData?Category=YunOsAccountOpLog&AccessKeyId=<yourAccessKeyId>&Date=Fri%2C%2028%20Jun%202013%2006%3A53%3A30%20GMT&Topic=raw&Signature=<yourSignature>HTTP/1.1","aliyun-sdk-java","guaguagua","hasagei"]"#;
    let res = client.get("/v1/sql?sql=select * from logs").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp: serde_json::Value = res.json().await;
    let result = resp["output"][0]["records"]["rows"].as_array().unwrap();
    assert_eq!(result.len(), 2);
    let mut line1 = result[0].as_array().unwrap().clone();
    let mut line2 = result[1].as_array().unwrap().clone();
    assert!(line1.first().unwrap().is_i64());
    assert!(line2.first().unwrap().is_i64());
    // set time index to null for assertion
    *line1.first_mut().unwrap() = serde_json::Value::Null;
    *line2.first_mut().unwrap() = serde_json::Value::Null;

    assert_eq!(
        line1,
        serde_json::from_str::<Vec<Value>>(line1_expected).unwrap()
    );
    assert_eq!(
        line2,
        serde_json::from_str::<Vec<Value>>(line2_expected).unwrap()
    );

    let expected = r#"[["greptime_timestamp","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["__source__","String","","YES","","FIELD"],["__time__","Int64","","YES","","FIELD"],["__topic__","String","","YES","","FIELD"],["ip","String","","YES","","FIELD"],["json_array","String","","YES","","FIELD"],["json_object.a","Int64","","YES","","FIELD"],["json_object.b","Int64","","YES","","FIELD"],["status","String","","YES","","FIELD"],["time","String","","YES","","FIELD"],["url","String","","YES","","FIELD"],["user-agent","String","","YES","","FIELD"],["dongdongdong","String","","YES","","FIELD"],["hasagei","String","","YES","","FIELD"]]"#;
    validate_data("identity_schema", &client, "desc logs", expected).await;

    guard.remove_all().await;
}

/// Test for identity pipeline with null values for new columns.
/// This test verifies that null values for columns not yet in the schema
/// are handled correctly - they should NOT push to the row without updating schema.
/// Regression test for: https://github.com/GreptimeTeam/greptimedb/issues/7654
///
/// The bug was that when a null value appeared for a column not in schema:
/// 1. The schema was not updated (correct)
/// 2. But row.push() was still called (BUG!)
///
/// This caused row.len() > schema.len(), leading to integer underflow in the
/// padding loop: `let diff = column_count - row.values.len()` where column_count < row.values.len()
/// causes usize underflow, making the loop try to push billions of elements → OOM.
///
/// To trigger this bug:
/// - Row 1 must have ONLY null values for NEW columns (pushes to row without schema update)
/// - Row 2 must have FEWER new non-null columns than Row 1's null columns
///
/// Example that triggers the bug:
/// - Row 1: {"a": null, "b": null} → row has [ts, null, null] (3 elements), schema has [ts] (1 col)
/// - Row 2: {"c": "value"} → row has [ts, "value"] (2 elements), schema has [ts, c] (2 cols)
/// - Padding: column_count=2, Row1.len()=3 → diff = 2-3 = underflow!
///
/// This test also covers resolve_value function branches:
/// - VrlValue::Null (null values)
/// - VrlValue::Integer (integer values)
/// - VrlValue::Float (floating point values)
/// - VrlValue::Boolean (boolean values)
/// - VrlValue::Bytes (string values)
///
/// Note: VrlValue::Array and VrlValue::Object branches are NOT covered because
/// the identity pipeline flattens nested objects and stringifies arrays before
/// resolve_value is called. Testing those branches would require a custom pipeline.
pub async fn test_identity_pipeline_with_null_column(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_identity_pipeline_with_null_column")
            .await;

    let client = TestClient::new(app).await;

    // Row 1 has 2 null values for new columns (a, b) - these push to row but don't add to schema
    // Row 2 has 1 non-null value for new column (c) - this adds to schema
    // Result: Row 1 has 3 elements [ts, null, null], schema has 2 columns [ts, c]
    // The padding loop: diff = 2 - 3 = usize underflow → OOM
    let body = r#"{"a":null,"b":null}
{"c":"value"}"#;

    let res = client
        .post("/v1/ingest?db=public&table=null_test&pipeline_name=greptime_identity")
        .header("Content-Type", "application/json")
        .body(body)
        .send()
        .await;

    // The buggy code would OOM or panic here due to the integer underflow causing
    // billions of push operations. If we get here, the bug is fixed.
    assert_eq!(res.status(), StatusCode::OK);

    let body: serde_json::Value = res.json().await;
    assert_eq!(body["output"][0]["affectedrows"], 2);

    // Verify the schema only contains columns that had non-null values
    // a and b should NOT be in the schema since they only had null values
    let expected = r#"[["greptime_timestamp","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["c","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_null_column_schema",
        &client,
        "desc null_test",
        expected,
    )
    .await;

    // Verify the data is correct
    let res = client
        .get(
            "/v1/sql?sql=select c from null_test order by case when c is null then 0 else 1 end, c",
        )
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp: serde_json::Value = res.json().await;
    let rows = resp["output"][0]["records"]["rows"].as_array().unwrap();
    assert_eq!(rows.len(), 2);

    // Row 1 has null for c (column didn't exist when row 1 was processed)
    assert!(rows[0][0].is_null());

    // Row 2 has "value" for c
    assert_eq!(rows[1][0], "value");

    // Test resolve_value function with various data types
    // Covers: VrlValue::Null, Integer, Float, Boolean, Bytes (string)
    // Note: Array and Object are flattened/stringified by identity pipeline before resolve_value
    let body_all_types = r#"{"null_col":null,"int_col":42,"float_col":3.10,"bool_col":true,"str_col":"hello","array_col":[1,2,3],"obj_col":{"nested":"value"}}"#;

    let res = client
        .post("/v1/ingest?db=public&table=all_types_test&pipeline_name=greptime_identity")
        .header("Content-Type", "application/json")
        .body(body_all_types)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body: serde_json::Value = res.json().await;
    assert_eq!(body["output"][0]["affectedrows"], 1);

    // Verify the schema contains all non-null column types
    // null_col should NOT be in schema since it only has null value
    // Note: arrays are converted to strings, nested objects are flattened (obj_col.nested)
    let expected = r#"[["greptime_timestamp","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["array_col","String","","YES","","FIELD"],["bool_col","Boolean","","YES","","FIELD"],["float_col","Float64","","YES","","FIELD"],["int_col","Int64","","YES","","FIELD"],["obj_col.nested","String","","YES","","FIELD"],["str_col","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_all_types_schema",
        &client,
        "desc all_types_test",
        expected,
    )
    .await;

    // Verify the data values are correct
    let res = client
        .get("/v1/sql?sql=select int_col, float_col, bool_col, str_col from all_types_test")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp: serde_json::Value = res.json().await;
    let rows = resp["output"][0]["records"]["rows"].as_array().unwrap();
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0][0], 42); // int_col
    assert_eq!(rows[0][1], 3.10); // float_col
    assert_eq!(rows[0][2], true); // bool_col
    assert_eq!(rows[0][3], "hello"); // str_col

    // Test with multiple rows containing different combinations of types
    // This ensures the schema evolution works correctly across rows
    let body_multi_rows = r#"{"int_val":100,"str_val":"first"}
{"int_val":200,"float_val":2.5,"bool_val":false}
{"str_val":"third","array_val":[4,5,6],"nested_obj":{"key":"val"}}"#;

    let res = client
        .post("/v1/ingest?db=public&table=multi_types_test&pipeline_name=greptime_identity")
        .header("Content-Type", "application/json")
        .body(body_multi_rows)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let body: serde_json::Value = res.json().await;
    assert_eq!(body["output"][0]["affectedrows"], 3);

    // Verify all columns from all rows are in the schema
    // Note: arrays are converted to strings, nested objects are flattened (nested_obj.key)
    // Columns appear in order they were first encountered: Row1(int_val,str_val), Row2(float_val,bool_val), Row3(array_val,nested_obj.key)
    let expected = r#"[["greptime_timestamp","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["int_val","Int64","","YES","","FIELD"],["str_val","String","","YES","","FIELD"],["bool_val","Boolean","","YES","","FIELD"],["float_val","Float64","","YES","","FIELD"],["array_val","String","","YES","","FIELD"],["nested_obj.key","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_multi_types_schema",
        &client,
        "desc multi_types_test",
        expected,
    )
    .await;

    // Verify data with nulls for columns not present in each row
    // ORDER BY with explicit NULLS LAST
    let res = client
        .get("/v1/sql?sql=select int_val, str_val, float_val, bool_val from multi_types_test order by case when int_val is null then 1 else 0 end, int_val")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp: serde_json::Value = res.json().await;
    let rows = resp["output"][0]["records"]["rows"].as_array().unwrap();
    assert_eq!(rows.len(), 3);

    // Row 1: int_val=100, str_val="first", float_val=null, bool_val=null
    assert_eq!(rows[0][0], 100);
    assert_eq!(rows[0][1], "first");
    assert!(rows[0][2].is_null());
    assert!(rows[0][3].is_null());

    // Row 2: int_val=200, str_val=null, float_val=2.5, bool_val=false
    assert_eq!(rows[1][0], 200);
    assert!(rows[1][1].is_null());
    assert_eq!(rows[1][2], 2.5);
    assert_eq!(rows[1][3], false);

    // Row 3: int_val=null, str_val="third", float_val=null, bool_val=null (nulls sorted last)
    assert!(rows[2][0].is_null());
    assert_eq!(rows[2][1], "third");
    assert!(rows[2][2].is_null());
    assert!(rows[2][3].is_null());

    guard.remove_all().await;
}

pub async fn test_pipeline_skip_error(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_skip_error").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true
transform:
  - field: message
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    // 2. write data
    let data_body = r#"
[
  {
    "time": "2024-05-25 20:16:37.217",
      "message": "good message"
  },
  {
    "time": "2024-05-25",
    "message": "bad message"
  },
  {
    "message": "another bad message"
  }
]
"#;

    // write data without skip error
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    // 3. check data
    // without skip error, all data should be dropped
    validate_data(
        "pipeline_skip_error",
        &client,
        "show tables",
        "[[\"demo\"],[\"numbers\"]]",
    )
    .await;

    // write data with skip error
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test&skip_error=true")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 4. check data
    // with skip error, only the first row should be written
    validate_data(
        "pipeline_skip_error",
        &client,
        "select message, time from logs1",
        "[[\"good message\",1716668197217000000]]",
    )
    .await;

    // write data with skip error on header
    // header has a higher priority than params
    // write one row
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test&skip_error=false")
        .header("Content-Type", "application/json")
        .header("x-greptime-pipeline-params", "skip_error=true")
        .body(data_body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // write one row
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test&skip_error=true")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // write zero row
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test&skip_error=true")
        .header("Content-Type", "application/json")
        .header("x-greptime-pipeline-params", "skip_error=false")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    // 5. check data
    // it will be three rows with the same ts
    validate_data(
        "pipeline_skip_error",
        &client,
        "select message, time from logs1",
        "[[\"good message\",1716668197217000000],[\"good message\",1716668197217000000],[\"good message\",1716668197217000000]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_filter(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_filter").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
  - filter:
      field: name
      targets:
        - John
transform:
  - field: name
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "time": "2024-05-25 20:16:37.217",
    "name": "John"
  },
  {
    "time": "2024-05-25 20:16:37.218",
    "name": "JoHN"
  },
  {
    "time": "2024-05-25 20:16:37.328",
    "name": "Jane"
  }
]
"#;

    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    validate_data(
        "pipeline_filter",
        &client,
        "select * from logs1",
        "[[\"Jane\",1716668197328000000]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_create_table(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_create_table").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
- dissect:
    fields:
      - message
    patterns:
      - '%{ip_address} - %{username} [%{timestamp}] "%{http_method} %{request_line} %{protocol}" %{status_code} %{response_size}'
    ignore_missing: true
- date:
    fields:
      - timestamp
    formats:
      - "%d/%b/%Y:%H:%M:%S %z"

transform:
  - fields:
      - timestamp
    type: time
    index: timestamp
  - fields:
      - ip_address
    type: string
    index: skipping
  - fields:
      - username
    type: string
    tag: true
  - fields:
      - http_method
    type: string
    index: inverted
  - fields:
      - request_line
    type: string
    index: fulltext
  - fields:
      - protocol
    type: string
  - fields:
      - status_code
    type: int32
    index: inverted
    tag: true
  - fields:
      - response_size
    type: int64
    on_failure: default
    default: 0
  - fields:
      - message
    type: string
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let res = client
        .get("/v1/pipelines/test/ddl?table=logs1")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp: serde_json::Value = res.json().await;
    let sql = resp
        .get("sql")
        .unwrap()
        .get("sql")
        .unwrap()
        .as_str()
        .unwrap();

    assert_eq!(
        "CREATE TABLE IF NOT EXISTS `logs1` (\n  `timestamp` TIMESTAMP(9) NOT NULL,\n  `ip_address` STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  `username` STRING NULL,\n  `http_method` STRING NULL INVERTED INDEX,\n  `request_line` STRING NULL FULLTEXT INDEX WITH(analyzer = 'English', backend = 'bloom', case_sensitive = 'false', false_positive_rate = '0.01', granularity = '10240'),\n  `protocol` STRING NULL,\n  `status_code` INT NULL INVERTED INDEX,\n  `response_size` BIGINT NULL,\n  `message` STRING NULL,\n  TIME INDEX (`timestamp`),\n  PRIMARY KEY (`username`, `status_code`)\n)\nENGINE=mito\nWITH(\n  append_mode = 'true'\n)",
        sql
    );

    guard.remove_all().await;
}

pub async fn test_pipeline_index_options(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_index_options").await;

    let client = TestClient::new(app).await;

    let pipeline_body = r#"
processors:
  - date:
      field: ts
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - field: message
    type: string
    index:
      type: fulltext
      options:
        analyzer: Chinese
        case_sensitive: true
        backend: bloom
        granularity: 2048
        false_positive_rate: 0.02
  - field: trace_id
    type: int64
    index:
      type: skipping
      options:
        granularity: 1024
        false_positive_rate: 0.05
        type: BLOOM
  - field: ts
    type: time
    index: timestamp
"#;

    let res = client
        .post("/v1/pipelines/index_options")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let data_body = r#"
[
  {
    "message": "hello greptime",
    "trace_id": 42,
    "ts": "2024-05-25 20:16:37.217"
  }
]
"#;
    let res = client
        .post("/v1/ingest?db=public&table=pipeline_index_options&pipeline_name=index_options")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let expected_schema = "[[\"pipeline_index_options\",\"CREATE TABLE IF NOT EXISTS \\\"pipeline_index_options\\\" (\\n  \\\"message\\\" STRING NULL FULLTEXT INDEX WITH(analyzer = 'Chinese', backend = 'bloom', case_sensitive = 'true', false_positive_rate = '0.02', granularity = '2048'),\\n  \\\"trace_id\\\" BIGINT NULL SKIPPING INDEX WITH(false_positive_rate = '0.05', granularity = '1024', type = 'BLOOM'),\\n  \\\"ts\\\" TIMESTAMP(9) NOT NULL,\\n  TIME INDEX (\\\"ts\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true'\\n)\"]]";
    validate_data(
        "pipeline_index_options_schema",
        &client,
        "show create table pipeline_index_options",
        expected_schema,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_dispatcher(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_dispatcher").await;

    // handshake
    let client = TestClient::new(app).await;

    let root_pipeline = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

dispatcher:
  field: type
  rules:
    - value: http
      table_suffix: _http
      pipeline: http
    - value: db
      table_suffix: _db
    - value: not_found
      table_suffix: _not_found
      pipeline: not_found

transform:
  - fields:
      - id1, id1_root
      - id2, id2_root
    type: int32
  - fields:
      - type
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    let http_pipeline = r#"
processors:

transform:
  - fields:
      - id1, id1_http
      - id2, id2_http
    type: int32
  - fields:
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(root_pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let res = client
        .post("/v1/events/pipelines/http")
        .header("Content-Type", "application/x-yaml")
        .body(http_pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "http",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "db",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "api",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "not_found",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=logs1&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);

    // 3. verify data
    let expected = "[[2436]]";
    validate_data(
        "test_dispatcher_pipeline default table",
        &client,
        "select id1_root from logs1",
        expected,
    )
    .await;

    let expected = "[[2436]]";
    validate_data(
        "test_dispatcher_pipeline http table",
        &client,
        "select id1_http from logs1_http",
        expected,
    )
    .await;

    let expected = "[[\"2436\"]]";
    validate_data(
        "test_dispatcher_pipeline db table",
        &client,
        "select id1 from logs1_db",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_suffix_template(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_suffix_template").await;

    // handshake
    let client = TestClient::new(app).await;

    let root_pipeline = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - fields:
      - id1, id1_root
      - id2, id2_root
    type: int32
  - fields:
      - type
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
table_suffix: _${type}
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(root_pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "http",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  },
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "http",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  },
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "db",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  },
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "http",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  },
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=d_table&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. check table list
    validate_data(
        "test_pipeline_suffix_template_table_list",
        &client,
        "show tables",
        "[[\"d_table\"],[\"d_table_db\"],[\"d_table_http\"],[\"demo\"],[\"numbers\"]]",
    )
    .await;

    // 4. check each table's data
    validate_data(
        "test_pipeline_suffix_template_default",
        &client,
        "select * from d_table",
        "[[2436,2528,null,\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",\"INTERACT.MANAGER\",1716668197217000000]]",
    )
    .await;

    validate_data(
        "test_pipeline_name_template_db",
        &client,
        "select * from d_table_db",
        "[[2436,2528,\"db\",\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",\"INTERACT.MANAGER\",1716668197217000000]]",
    )
    .await;

    validate_data(
        "test_pipeline_name_template_http",
        &client,
        "select * from d_table_http",
        "[[2436,2528,\"http\",\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",\"INTERACT.MANAGER\",1716668197217000000],[2436,2528,\"http\",\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",\"INTERACT.MANAGER\",1716668197217000000],[2436,2528,\"http\",\"ClusterAdapter:enter sendTextDataToCluster\\\\n\",\"INTERACT.MANAGER\",1716668197217000000]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_context(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_context").await;

    // handshake
    let client = TestClient::new(app).await;

    let root_pipeline = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - fields:
      - id1, id1_root
      - id2, id2_root
    type: int32
  - fields:
      - type
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
table_suffix: _${type}
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(root_pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "http",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n",
    "greptime_ttl": "1d",
    "greptime_skip_wal": "true"
  },
  {
    "id1": "2436",
    "id2": "2528",
    "logger": "INTERACT.MANAGER",
    "type": "db",
    "time": "2024-05-25 20:16:37.217",
    "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=d_table&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. check table list
    validate_data(
        "test_pipeline_context_table_list",
        &client,
        "show tables",
        "[[\"d_table_db\"],[\"d_table_http\"],[\"demo\"],[\"numbers\"]]",
    )
    .await;

    // 4. check each table's data
    // CREATE TABLE IF NOT EXISTS "d_table_db" (
    //     ... ignore
    //     )
    //   ENGINE=mito
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     append_mode = 'true'
    //     )
    let expected = "[[\"d_table_db\",\"CREATE TABLE IF NOT EXISTS \\\"d_table_db\\\" (\\n  \\\"id1_root\\\" INT NULL,\\n  \\\"id2_root\\\" INT NULL,\\n  \\\"type\\\" STRING NULL,\\n  \\\"log\\\" STRING NULL,\\n  \\\"logger\\\" STRING NULL,\\n  \\\"time\\\" TIMESTAMP(9) NOT NULL,\\n  TIME INDEX (\\\"time\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true'\\n)\"]]";

    validate_data(
        "test_pipeline_context_db",
        &client,
        "show create table d_table_db",
        expected,
    )
    .await;

    // CREATE TABLE IF NOT EXISTS "d_table_http" (
    //     ... ignore
    //     )
    //     ENGINE=mito
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     append_mode = 'true',
    //     skip_wal = 'true',
    //     ttl = '1day'
    //     )
    let expected = "[[\"d_table_http\",\"CREATE TABLE IF NOT EXISTS \\\"d_table_http\\\" (\\n  \\\"id1_root\\\" INT NULL,\\n  \\\"id2_root\\\" INT NULL,\\n  \\\"type\\\" STRING NULL,\\n  \\\"log\\\" STRING NULL,\\n  \\\"logger\\\" STRING NULL,\\n  \\\"time\\\" TIMESTAMP(9) NOT NULL,\\n  TIME INDEX (\\\"time\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  \'comment\' = 'Created on insertion',\\n  append_mode = 'true',\\n  skip_wal = 'true',\\n  ttl = '1day'\\n)\"]]";
    validate_data(
        "test_pipeline_context_http",
        &client,
        "show create table d_table_http",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_with_vrl(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_with_vrl").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true
  - vrl:
      source: |
        .from_source = "channel_2"
        cond, err = .id1 > .id2
        if (cond) {
            .from_source = "channel_1"
        }
        del(.id1)
        del(.id2)
        .

transform:
  - fields:
      - from_source
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": 2436,
    "id2": 123,
    "time": "2024-05-25 20:16:37.217"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=d_table&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    validate_data(
        "test_pipeline_with_vrl",
        &client,
        "select * from d_table",
        "[[\"channel_1\",1716668197217000000]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_with_hint_vrl(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_with_hint_vrl").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true
  - vrl:
      source: |
        .greptime_table_suffix, err = "_" + .id
        .

transform:
  - fields:
      - id
    type: int32
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id": "2436",
    "time": "2024-05-25 20:16:37.217"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=d_table&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    validate_data(
        "test_pipeline_with_hint_vrl",
        &client,
        "show tables",
        "[[\"d_table_2436\"],[\"demo\"],[\"numbers\"]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_influxdb_write_with_hints(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_influxdb_write_with_hints").await;

    let client = TestClient::new(app).await;

    let result = client
        .post("/v1/influxdb/write?db=public")
        .header("x-greptime-hints", "sst_format=flat,ttl=30d,skip_wal=true")
        .body("sst_fmt_table,host=host1 cpu=1.2 1664370459457010101")
        .send()
        .await;
    assert_eq!(result.status(), 204);

    let res = client
        .get("/v1/sql?sql=show create table sst_fmt_table")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp = res.text().await;
    assert!(
        resp.contains("sst_format = 'flat'"),
        "expected sst_format = 'flat' in SHOW CREATE TABLE output, got: {resp}"
    );
    assert!(
        resp.contains("ttl = '30days'"),
        "expected ttl = '30days' in SHOW CREATE TABLE output, got: {resp}"
    );
    assert!(
        resp.contains("skip_wal = 'true'"),
        "expected skip_wal = 'true' in SHOW CREATE TABLE output, got: {resp}"
    );

    guard.remove_all().await;
}

pub async fn test_influxdb_write_with_append_mode_hint(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(
        storage_type,
        "test_influxdb_write_with_append_mode_hint",
    )
    .await;

    let client = TestClient::new(app).await;

    let result = client
        .post("/v1/influxdb/write?db=public")
        .header("x-greptime-hint-append_mode", "true")
        .body("append_mode_table,host=host1 cpu=1.2 1664370459457010101")
        .send()
        .await;
    assert_eq!(result.status(), 204);

    let res = client
        .get("/v1/sql?sql=show create table append_mode_table")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp = res.text().await;
    assert!(
        resp.contains("append_mode = 'true'"),
        "expected append_mode = 'true' in SHOW CREATE TABLE output, got: {resp}"
    );
    assert!(
        resp.contains("merge_mode = 'last_row'"),
        "expected merge_mode = 'last_row' in SHOW CREATE TABLE output, got: {resp}"
    );

    guard.remove_all().await;
}

/// Test one-to-many VRL pipeline expansion.
/// This test verifies that a VRL processor can return an array, which results in
/// multiple output rows from a single input row.
pub async fn test_pipeline_one_to_many_vrl(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(storage_type, "test_pipeline_one_to_many_vrl").await;

    let client = TestClient::new(app).await;

    // Pipeline that expands events array into multiple rows
    let pipeline = r#"
processors:
  - date:
      field: timestamp
      formats:
        - "%Y-%m-%d %H:%M:%S"
      ignore_missing: true
  - vrl:
      source: |
        # Extract events array and expand each event into a separate row
        events = del(.events)
        base_host = del(.host)
        base_timestamp = del(.timestamp)

        # Map each event to a complete row object
        map_values(array!(events)) -> |event| {
            {
                "host": base_host,
                "event_type": event.type,
                "event_value": event.value,
                "timestamp": base_timestamp
            }
        }

transform:
  - field: host
    type: string
  - field: event_type
    type: string
  - field: event_value
    type: int32
  - field: timestamp
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/one_to_many")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data - single input with multiple events
    let data_body = r#"
[
  {
    "host": "server1",
    "timestamp": "2024-05-25 20:16:37",
    "events": [
      {"type": "cpu", "value": 80},
      {"type": "memory", "value": 60},
      {"type": "disk", "value": 45}
    ]
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=metrics&pipeline_name=one_to_many")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. verify: one input row should produce three output rows
    validate_data(
        "test_pipeline_one_to_many_vrl_count",
        &client,
        "select count(*) from metrics",
        "[[3]]",
    )
    .await;

    // 4. verify the actual data
    validate_data(
        "test_pipeline_one_to_many_vrl_data",
        &client,
        "select host, event_type, event_value from metrics order by event_type",
        "[[\"server1\",\"cpu\",80],[\"server1\",\"disk\",45],[\"server1\",\"memory\",60]]",
    )
    .await;

    // 5. Test with multiple input rows, each producing multiple output rows
    let data_body2 = r#"
[
  {
    "host": "server2",
    "timestamp": "2024-05-25 20:17:00",
    "events": [
      {"type": "cpu", "value": 90},
      {"type": "memory", "value": 70}
    ]
  },
  {
    "host": "server3",
    "timestamp": "2024-05-25 20:18:00",
    "events": [
      {"type": "cpu", "value": 50}
    ]
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=metrics&pipeline_name=one_to_many")
        .header("Content-Type", "application/json")
        .body(data_body2)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 6. verify total count: 3 (from first batch) + 2 + 1 = 6 rows
    validate_data(
        "test_pipeline_one_to_many_vrl_total_count",
        &client,
        "select count(*) from metrics",
        "[[6]]",
    )
    .await;

    // 7. verify rows per host
    validate_data(
        "test_pipeline_one_to_many_vrl_per_host",
        &client,
        "select host, count(*) as cnt from metrics group by host order by host",
        "[[\"server1\",3],[\"server2\",2],[\"server3\",1]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_2(storage_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(storage_type, "test_pipeline_2").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline = r#"
version: 2
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"

transform:
  - field: id1
    type: int32
    index: inverted
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/events/pipelines/root")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
[
  {
    "id1": "123",
    "id2": "2436",
    "time": "2024-05-25 20:16:37.217"
  }
]
"#;
    let res = client
        .post("/v1/events/logs?db=public&table=d_table&pipeline_name=root")
        .header("Content-Type", "application/json")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // CREATE TABLE IF NOT EXISTS "d_table" (
    //     "id1" INT NULL INVERTED INDEX,
    //     "time" TIMESTAMP(9) NOT NULL,
    //     "id2" STRING NULL,
    //     TIME INDEX ("time")
    //   )
    //   ENGINE=mito
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     append_mode = 'true'
    //   )
    validate_data(
        "test_pipeline_2_schema",
        &client,
        "show create table d_table",
        "[[\"d_table\",\"CREATE TABLE IF NOT EXISTS \\\"d_table\\\" (\\n  \\\"id1\\\" INT NULL INVERTED INDEX,\\n  \\\"time\\\" TIMESTAMP(9) NOT NULL,\\n  \\\"id2\\\" STRING NULL,\\n  TIME INDEX (\\\"time\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true'\\n)\"]]",
    )
    .await;

    validate_data(
        "test_pipeline_2_data",
        &client,
        "select * from d_table",
        "[[123,1716668197217000000,\"2436\"]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_identity_pipeline_with_flatten(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_identity_pipeline_with_flatten").await;

    let client = TestClient::new(app).await;
    let body = r#"{"__time__":1453809242,"__topic__":"","__source__":"10.170.***.***","ip":"10.200.**.***","time":"26/Jan/2016:19:54:02 +0800","url":"POST/PutData?Category=YunOsAccountOpLog&AccessKeyId=<yourAccessKeyId>&Date=Fri%2C%2028%20Jun%202013%2006%3A53%3A30%20GMT&Topic=raw&Signature=<yourSignature>HTTP/1.1","status":"200","user-agent":"aliyun-sdk-java","custom_map":{"value_a":["a","b","c"],"value_b":"b"}}"#;

    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/json"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-params"),
                HeaderValue::from_static("flatten_json_object=true"),
            ),
        ],
        "/v1/ingest?table=logs&pipeline_name=greptime_identity",
        body.as_bytes().to_vec(),
        false,
    )
    .await;

    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["greptime_timestamp","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["__source__","String","","YES","","FIELD"],["__time__","Int64","","YES","","FIELD"],["__topic__","String","","YES","","FIELD"],["custom_map.value_a","String","","YES","","FIELD"],["custom_map.value_b","String","","YES","","FIELD"],["ip","String","","YES","","FIELD"],["status","String","","YES","","FIELD"],["time","String","","YES","","FIELD"],["url","String","","YES","","FIELD"],["user-agent","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_with_flatten_desc_logs",
        &client,
        "desc logs",
        expected,
    )
    .await;

    let expected = "[[\"[\\\"a\\\",\\\"b\\\",\\\"c\\\"]\"]]";
    validate_data(
        "test_identity_pipeline_with_flatten_select_json",
        &client,
        "select `custom_map.value_a` from logs",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_identity_pipeline_with_custom_ts(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_identity_pipeline_with_custom_ts")
            .await;

    let client = TestClient::new(app).await;
    let body = r#"
    [{"__time__":1453809242,"__source__":"10.170.***.***", "__name__":"hello"},
    {"__time__":1453809252,"__source__":"10.170.***.***"}]
    "#;

    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        )],
        "/v1/ingest?table=logs&pipeline_name=greptime_identity&custom_time_index=__time__;epoch;s",
        body.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["__time__","TimestampSecond","PRI","NO","","TIMESTAMP"],["__name__","String","","YES","","FIELD"],["__source__","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_with_custom_ts_desc_logs",
        &client,
        "desc logs",
        expected,
    )
    .await;

    let expected = r#"[[1453809242,"hello","10.170.***.***"],[1453809252,null,"10.170.***.***"]]"#;
    validate_data(
        "test_identity_pipeline_with_custom_ts_data",
        &client,
        "select * from logs",
        expected,
    )
    .await;

    // drop table
    let res = client.get("/v1/sql?sql=drop table logs").send().await;
    assert_eq!(res.status(), StatusCode::OK);

    let body = r#"
    [{"__time__":"2019-01-16 02:42:01+08:00","__source__":"10.170.***.***"},
    {"__time__":"2019-01-16 02:42:04+08:00","__source__":"10.170.***.***", "__name__":"hello"}]
    "#;

    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        )],
        "/v1/ingest?table=logs&pipeline_name=greptime_identity&custom_time_index=__time__;datestr;%Y-%m-%d %H:%M:%S%z",
        body.as_bytes().to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["__time__","TimestampNanosecond","PRI","NO","","TIMESTAMP"],["__source__","String","","YES","","FIELD"],["__name__","String","","YES","","FIELD"]]"#;
    validate_data(
        "test_identity_pipeline_with_custom_ts_desc_logs",
        &client,
        "desc logs",
        expected,
    )
    .await;

    let expected = r#"[[1547577721000000000,"10.170.***.***",null],[1547577724000000000,"10.170.***.***","hello"]]"#;
    validate_data(
        "test_identity_pipeline_with_custom_ts_data",
        &client,
        "select * from logs",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_test_pipeline_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_pipeline_api").await;

    // handshake
    let client = TestClient::new(app).await;

    let pipeline_content = r#"
processors:
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - fields:
      - id1
      - id2
    type: int32
  - fields:
      - type
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(pipeline_content)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let content = res.text().await;

    let content = serde_json::from_str(&content);
    assert!(content.is_ok());
    //  {"execution_time_ms":13,"pipelines":[{"name":"test","version":"2024-07-04 08:31:00.987136"}]}
    let content: Value = content.unwrap();

    let execution_time = content.get("execution_time_ms");
    assert!(execution_time.unwrap().is_number());
    let pipelines = content.get("pipelines");
    let pipelines = pipelines.unwrap().as_array().unwrap();
    assert_eq!(pipelines.len(), 1);
    let pipeline = pipelines.first().unwrap();
    assert_eq!(pipeline.get("name").unwrap(), "test");

    let dryrun_schema = json!([
        {
            "column_type": "FIELD",
            "data_type": "INT32",
            "fulltext": false,
            "name": "id1"
        },
        {
            "column_type": "FIELD",
            "data_type": "INT32",
            "fulltext": false,
            "name": "id2"
        },
        {
            "column_type": "FIELD",
            "data_type": "STRING",
            "fulltext": false,
            "name": "type"
        },
        {
            "column_type": "FIELD",
            "data_type": "STRING",
            "fulltext": false,
            "name": "log"
        },
        {
            "column_type": "FIELD",
            "data_type": "STRING",
            "fulltext": false,
            "name": "logger"
        },
        {
            "column_type": "TIMESTAMP",
            "data_type": "TIMESTAMP_NANOSECOND",
            "fulltext": false,
            "name": "time"
        }
    ]);
    let dryrun_rows = json!([
        [
            {
                "data_type": "INT32",
                "key": "id1",
                "semantic_type": "FIELD",
                "value": 2436
            },
            {
                "data_type": "INT32",
                "key": "id2",
                "semantic_type": "FIELD",
                "value": 2528
            },
            {
                "data_type": "STRING",
                "key": "type",
                "semantic_type": "FIELD",
                "value": "I"
            },
            {
                "data_type": "STRING",
                "key": "log",
                "semantic_type": "FIELD",
                "value": "ClusterAdapter:enter sendTextDataToCluster"
            },
            {
                "data_type": "STRING",
                "key": "logger",
                "semantic_type": "FIELD",
                "value": "INTERACT.MANAGER"
            },
            {
                "data_type": "TIMESTAMP_NANOSECOND",
                "key": "time",
                "semantic_type": "TIMESTAMP",
                "value": 1716668197217000000i64
            }
        ],
        [
            {
                "data_type": "INT32",
                "key": "id1",
                "semantic_type": "FIELD",
                "value": 1111
            },
            {
                "data_type": "INT32",
                "key": "id2",
                "semantic_type": "FIELD",
                "value": 2222
            },
            {
                "data_type": "STRING",
                "key": "type",
                "semantic_type": "FIELD",
                "value": "D"
            },
            {
                "data_type": "STRING",
                "key": "log",
                "semantic_type": "FIELD",
                "value": "ClusterAdapter:enter sendTextDataToCluster ggg"
            },
            {
                "data_type": "STRING",
                "key": "logger",
                "semantic_type": "FIELD",
                "value": "INTERACT.MANAGER"
            },
            {
                "data_type": "TIMESTAMP_NANOSECOND",
                "key": "time",
                "semantic_type": "TIMESTAMP",
                "value": 1716668198217000000i64
            }
        ]
    ]);
    {
        // test original api
        let data_body = r#"
        [
          {
            "id1": "2436",
            "id2": "2528",
            "logger": "INTERACT.MANAGER",
            "type": "I",
            "time": "2024-05-25 20:16:37.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster"
          },
         {
            "id1": "1111",
            "id2": "2222",
            "logger": "INTERACT.MANAGER",
            "type": "D",
            "time": "2024-05-25 20:16:38.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster ggg"
          }
        ]
        "#;
        let res = client
            .post("/v1/pipelines/_dryrun?pipeline_name=test")
            .header("Content-Type", "application/json")
            .body(data_body)
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
        let body: Value = res.json().await;
        let schema = &body[0]["schema"];
        let rows = &body[0]["rows"];
        assert_eq!(schema, &dryrun_schema);
        assert_eq!(rows, &dryrun_rows);
    }
    {
        // test new api specify pipeline via pipeline_name
        let data = r#"[
                {
                "id1": "2436",
                "id2": "2528",
                "logger": "INTERACT.MANAGER",
                "type": "I",
                "time": "2024-05-25 20:16:37.217",
                "log": "ClusterAdapter:enter sendTextDataToCluster"
                },
                {
                "id1": "1111",
                "id2": "2222",
                "logger": "INTERACT.MANAGER",
                "type": "D",
                "time": "2024-05-25 20:16:38.217",
                "log": "ClusterAdapter:enter sendTextDataToCluster ggg"
                }
            ]"#;
        let body = json!({"pipeline_name":"test","data":data});
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
        let body: Value = res.json().await;
        let schema = &body[0]["schema"];
        let rows = &body[0]["rows"];
        assert_eq!(schema, &dryrun_schema);
        assert_eq!(rows, &dryrun_rows);
    }
    {
        let pipeline_content_for_text = r#"
processors:
  - dissect:
      fields:
        - message
      patterns:
        - "%{id1} %{id2} %{logger} %{type} \"%{time}\" \"%{log}\""
  - date:
      field: time
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
      ignore_missing: true

transform:
  - fields:
      - id1
      - id2
    type: int32
  - fields:
      - type
      - log
      - logger
    type: string
  - field: time
    type: time
    index: timestamp
"#;

        // test new api specify pipeline via pipeline raw data
        let data = r#"[
            {
            "id1": "2436",
            "id2": "2528",
            "logger": "INTERACT.MANAGER",
            "type": "I",
            "time": "2024-05-25 20:16:37.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster"
            },
            {
            "id1": "1111",
            "id2": "2222",
            "logger": "INTERACT.MANAGER",
            "type": "D",
            "time": "2024-05-25 20:16:38.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster ggg"
            }
        ]"#;
        let mut body = json!({
        "data": data
        });
        body["pipeline"] = json!(pipeline_content);
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
        let body: Value = res.json().await;
        let schema = &body[0]["schema"];
        let rows = &body[0]["rows"];
        assert_eq!(schema, &dryrun_schema);
        assert_eq!(rows, &dryrun_rows);
        let mut body_for_text = json!({
            "data": r#"2436 2528 INTERACT.MANAGER I "2024-05-25 20:16:37.217" "ClusterAdapter:enter sendTextDataToCluster"
1111 2222 INTERACT.MANAGER D "2024-05-25 20:16:38.217" "ClusterAdapter:enter sendTextDataToCluster ggg"
"#,
        });
        body_for_text["pipeline"] = json!(pipeline_content_for_text);
        body_for_text["data_type"] = json!("text/plain");
        let ndjson_content = r#"{"id1":"2436","id2":"2528","logger":"INTERACT.MANAGER","type":"I","time":"2024-05-25 20:16:37.217","log":"ClusterAdapter:enter sendTextDataToCluster"}
{"id1":"1111","id2":"2222","logger":"INTERACT.MANAGER","type":"D","time":"2024-05-25 20:16:38.217","log":"ClusterAdapter:enter sendTextDataToCluster ggg"}
"#;
        let body_for_ndjson = json!({
            "pipeline":pipeline_content,
            "data_type": "application/x-ndjson",
            "data": ndjson_content,
        });
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body_for_ndjson.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
        let body: Value = res.json().await;
        let schema = &body[0]["schema"];
        let rows = &body[0]["rows"];
        assert_eq!(schema, &dryrun_schema);
        assert_eq!(rows, &dryrun_rows);

        body_for_text["data_type"] = json!("application/yaml");
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body_for_text.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
        let body: Value = res.json().await;
        assert_eq!(
            body["error"],
            json!(
                "Invalid request parameter: invalid content type: application/yaml, expected: one of application/json, application/x-ndjson, text/plain"
            )
        );

        body_for_text["data_type"] = json!("application/json");
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body_for_text.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
        let body: Value = res.json().await;
        assert_eq!(
            body["error"],
            json!(
                "Invalid request parameter: json format error, please check the date is valid JSON."
            )
        );

        body_for_text["data_type"] = json!("text/plain");
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body_for_text.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);
        let body: Value = res.json().await;

        let schema = &body[0]["schema"];
        let rows = &body[0]["rows"];
        assert_eq!(schema, &dryrun_schema);
        assert_eq!(rows, &dryrun_rows);
    }
    {
        // failback to old version api
        // not pipeline and pipeline_name in the body
        let body = json!({
        "data": [
            {
            "id1": "2436",
            "id2": "2528",
            "logger": "INTERACT.MANAGER",
            "type": "I",
            "time": "2024-05-25 20:16:37.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster\\n"
            },
            {
            "id1": "1111",
            "id2": "2222",
            "logger": "INTERACT.MANAGER",
            "type": "D",
            "time": "2024-05-25 20:16:38.217",
            "log": "ClusterAdapter:enter sendTextDataToCluster ggg"
            }
        ]
        });
        let res = client
            .post("/v1/pipelines/_dryrun")
            .header("Content-Type", "application/json")
            .body(body.to_string())
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    }
    guard.remove_all().await;
}

pub async fn test_plain_text_ingestion(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_pipeline_api").await;

    // handshake
    let client = TestClient::new(app).await;

    let body = r#"
processors:
  - dissect:
      fields:
        - message
      patterns:
        - "%{+ts} %{+ts} %{content}"
  - date:
      fields:
        - ts
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"

transform:
  - fields:
      - content
    type: string
  - field: ts
    type: time
    index: timestamp
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    let content = res.text().await;

    let content = serde_json::from_str(&content);
    assert!(content.is_ok());
    //  {"execution_time_ms":13,"pipelines":[{"name":"test","version":"2024-07-04 08:31:00.987136"}]}
    let content: Value = content.unwrap();

    let version_str = content
        .get("pipelines")
        .unwrap()
        .as_array()
        .unwrap()
        .first()
        .unwrap()
        .get("version")
        .unwrap()
        .as_str()
        .unwrap()
        .to_string();
    assert!(!version_str.is_empty());

    // 2. write data
    let data_body = r#"
2024-05-25 20:16:37.217 hello
2024-05-25 20:16:37.218 hello world
"#;
    let res = client
        .post("/v1/ingest?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "text/plain")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. select data
    let res = client.get("/v1/sql?sql=select * from logs1").send().await;
    assert_eq!(res.status(), StatusCode::OK);
    let resp = res.text().await;

    let v = get_rows_from_output(&resp);

    assert_eq!(
        v,
        r#"[["hello",1716668197217000000],["hello world",1716668197218000000]]"#,
    );

    guard.remove_all().await;
}

pub async fn test_pipeline_auto_transform(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_auto_transform").await;

    // handshake
    let client = TestClient::new(app).await;

    let body = r#"
processors:
  - dissect:
      fields:
        - message
      patterns:
        - "%{+ts} %{+ts} %{http_status_code} %{content}"
  - date:
      fields:
        - ts
      formats:
        - "%Y-%m-%d %H:%M:%S%.3f"
"#;

    // 1. create pipeline
    let res = client
        .post("/v1/pipelines/test")
        .header("Content-Type", "application/x-yaml")
        .body(body)
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK);

    // 2. write data
    let data_body = r#"
2024-05-25 20:16:37.217 404 hello
2024-05-25 20:16:37.218 200 hello world
"#;
    let res = client
        .post("/v1/ingest?db=public&table=logs1&pipeline_name=test")
        .header("Content-Type", "text/plain")
        .body(data_body)
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // 3. select data
    let expected = "[[1716668197217000000,\"hello\",\"404\",\"2024-05-25 20:16:37.217 404 hello\"],[1716668197218000000,\"hello world\",\"200\",\"2024-05-25 20:16:37.218 200 hello world\"]]";
    validate_data(
        "test_pipeline_auto_transform",
        &client,
        "select * from logs1",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_pipeline_auto_transform_with_select(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_pipeline_auto_transform_with_select")
            .await;

    // handshake
    let client = TestClient::new(app).await;
    let data_body = r#"
2024-05-25 20:16:37.217 404 hello
2024-05-25 20:16:37.218 200 hello world"#;

    // select include
    {
        let body = r#"
        processors:
          - dissect:
              fields:
                - message
              patterns:
                - "%{+ts} %{+ts} %{http_status_code} %{content}"
          - date:
              fields:
                - ts
              formats:
                - "%Y-%m-%d %H:%M:%S%.3f"
          - select:
              fields:
                - ts
                - http_status_code
        "#;

        // 1. create pipeline
        let res = client
            .post("/v1/pipelines/test")
            .header("Content-Type", "application/x-yaml")
            .body(body)
            .send()
            .await;

        assert_eq!(res.status(), StatusCode::OK);

        // 2. write data
        let res = client
            .post("/v1/ingest?db=public&table=logs1&pipeline_name=test")
            .header("Content-Type", "text/plain")
            .body(data_body)
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);

        // 3. select data
        let expected = "[[1716668197217000000,\"404\"],[1716668197218000000,\"200\"]]";
        validate_data(
            "test_pipeline_auto_transform_with_select",
            &client,
            "select * from logs1",
            expected,
        )
        .await;
    }

    // select include rename
    {
        let body = r#"
        processors:
          - dissect:
              fields:
                - message
              patterns:
                - "%{+ts} %{+ts} %{http_status_code} %{content}"
          - date:
              fields:
                - ts
              formats:
                - "%Y-%m-%d %H:%M:%S%.3f"
          - select:
              fields:
                - ts
                - key: http_status_code
                  rename_to: s_code
        "#;

        // 1. create pipeline
        let res = client
            .post("/v1/pipelines/test2")
            .header("Content-Type", "application/x-yaml")
            .body(body)
            .send()
            .await;

        assert_eq!(res.status(), StatusCode::OK);

        // 2. write data
        let res = client
            .post("/v1/ingest?db=public&table=logs2&pipeline_name=test2")
            .header("Content-Type", "text/plain")
            .body(data_body)
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);

        // 3. check schema
        let expected = "[[\"ts\",\"TimestampNanosecond\",\"PRI\",\"NO\",\"\",\"TIMESTAMP\"],[\"s_code\",\"String\",\"\",\"YES\",\"\",\"FIELD\"]]";
        validate_data(
            "test_pipeline_auto_transform_with_select_rename",
            &client,
            "desc table logs2",
            expected,
        )
        .await;

        // 4. check data
        let expected = "[[1716668197217000000,\"404\"],[1716668197218000000,\"200\"]]";
        validate_data(
            "test_pipeline_auto_transform_with_select_rename",
            &client,
            "select * from logs2",
            expected,
        )
        .await;
    }

    // select exclude
    {
        let body = r#"
            processors:
              - dissect:
                  fields:
                    - message
                  patterns:
                    - "%{+ts} %{+ts} %{http_status_code} %{content}"
              - date:
                  fields:
                    - ts
                  formats:
                    - "%Y-%m-%d %H:%M:%S%.3f"
              - select:
                  type: exclude
                  fields:
                    - http_status_code
            "#;

        // 1. create pipeline
        let res = client
            .post("/v1/pipelines/test3")
            .header("Content-Type", "application/x-yaml")
            .body(body)
            .send()
            .await;

        assert_eq!(res.status(), StatusCode::OK);

        // 2. write data
        let res = client
            .post("/v1/ingest?db=public&table=logs3&pipeline_name=test3")
            .header("Content-Type", "text/plain")
            .body(data_body)
            .send()
            .await;
        assert_eq!(res.status(), StatusCode::OK);

        // 3. check schema
        let expected = "[[\"ts\",\"TimestampNanosecond\",\"PRI\",\"NO\",\"\",\"TIMESTAMP\"],[\"content\",\"String\",\"\",\"YES\",\"\",\"FIELD\"],[\"message\",\"String\",\"\",\"YES\",\"\",\"FIELD\"]]";
        validate_data(
            "test_pipeline_auto_transform_with_select_rename",
            &client,
            "desc table logs3",
            expected,
        )
        .await;

        // 4. check data
        let expected = "[[1716668197217000000,\"hello\",\"2024-05-25 20:16:37.217 404 hello\"],[1716668197218000000,\"hello world\",\"2024-05-25 20:16:37.218 200 hello world\"]]";
        validate_data(
            "test_pipeline_auto_transform_with_select_rename",
            &client,
            "select * from logs3",
            expected,
        )
        .await;
    }

    guard.remove_all().await;
}

pub async fn test_otlp_metrics_new(store_type: StorageType) {
    // init
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_otlp_metrics_new").await;

    let content = r#"
{"resourceMetrics":[{"resource":{"attributes":[{"key":"host.arch","value":{"stringValue":"arm64"}},{"key":"os.type","value":{"stringValue":"darwin"}},{"key":"os.version","value":{"stringValue":"25.0.0"}},{"key":"service.name","value":{"stringValue":"claude-code"}},{"key":"service.version","value":{"stringValue":"1.0.62"}}],"droppedAttributesCount":0},"scopeMetrics":[{"scope":{"name":"com.anthropic.claude_code","version":"1.0.62","attributes":[],"droppedAttributesCount":0},"metrics":[{"name":"claude_code.cost.usage","description":"Cost of the Claude Code session","unit":"USD","metadata":[],"sum":{"dataPoints":[{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"model","value":{"stringValue":"claude-3-5-haiku-20241022"}}],"startTimeUnixNano":"1753780502453000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":0.0052544},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"model","value":{"stringValue":"claude-sonnet-4-20250514"}}],"startTimeUnixNano":"1753780513420000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":2.244618}],"aggregationTemporality":1,"isMonotonic":true}},{"name":"claude_code.token.usage","description":"Number of tokens used","unit":"tokens","metadata":[],"sum":{"dataPoints":[{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"input"}},{"key":"model","value":{"stringValue":"claude-3-5-haiku-20241022"}}],"startTimeUnixNano":"1753780502453000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":6208.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"output"}},{"key":"model","value":{"stringValue":"claude-3-5-haiku-20241022"}}],"startTimeUnixNano":"1753780502453000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":72.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"cacheRead"}},{"key":"model","value":{"stringValue":"claude-3-5-haiku-20241022"}}],"startTimeUnixNano":"1753780502453000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":0.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"cacheCreation"}},{"key":"model","value":{"stringValue":"claude-3-5-haiku-20241022"}}],"startTimeUnixNano":"1753780502453000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":0.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"input"}},{"key":"model","value":{"stringValue":"claude-sonnet-4-20250514"}}],"startTimeUnixNano":"1753780513420000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":743056.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"output"}},{"key":"model","value":{"stringValue":"claude-sonnet-4-20250514"}}],"startTimeUnixNano":"1753780513420000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":1030.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"cacheRead"}},{"key":"model","value":{"stringValue":"claude-sonnet-4-20250514"}}],"startTimeUnixNano":"1753780513420000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":0.0},{"attributes":[{"key":"user.id","value":{"stringValue":"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4"}},{"key":"session.id","value":{"stringValue":"736525A3-F5D4-496B-933E-827AF23A5B97"}},{"key":"terminal.type","value":{"stringValue":"ghostty"}},{"key":"type","value":{"stringValue":"cacheCreation"}},{"key":"model","value":{"stringValue":"claude-sonnet-4-20250514"}}],"startTimeUnixNano":"1753780513420000000","timeUnixNano":"1753780559836000000","exemplars":[],"flags":0,"asDouble":0.0}],"aggregationTemporality":1,"isMonotonic":true}}],"schemaUrl":""}],"schemaUrl":""}]}
    "#;

    let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();

    // handshake
    let client = TestClient::new(app).await;

    // write metrics data
    // with scope attrs and all resource attrs
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-promote-scope-attrs"),
                HeaderValue::from_static("true"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-promote-all-resource-attrs"),
                HeaderValue::from_static("true"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-ignore-resource-attrs"),
                HeaderValue::from_static("os.type"),
            ),
        ],
        "/v1/otlp/v1/metrics",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = "[[\"claude_code_cost_usage_USD_total\"],[\"claude_code_token_usage_tokens_total\"],[\"demo\"],[\"greptime_otel_resource_info\"],[\"greptime_physical_table\"],[\"numbers\"]]";
    validate_data("otlp_metrics_all_tables", &client, "show tables;", expected).await;

    // Metric-engine logical table carries the semantic identity. Match substrings
    // because extra_options ordering is not stable.
    validate_data(
        "otlp_metrics_semantic_identity",
        &client,
        "select count(*) from information_schema.tables where table_name = 'claude_code_cost_usage_USD_total' \
         and create_options like '%greptime.semantic.signal_type=metric%' \
         and create_options like '%greptime.semantic.source=opentelemetry%';",
        "[[1]]",
    )
    .await;
    // OTLP metric type is declared, so Phase 2 stamps `metadata_quality=declared`.
    validate_data(
        "otlp_metrics_metadata_quality_declared",
        &client,
        "select count(*) from information_schema.tables where table_name = 'claude_code_cost_usage_USD_total' \
         and create_options like '%greptime.semantic.metric.metadata_quality=declared%';",
        "[[1]]",
    )
    .await;

    // CREATE TABLE IF NOT EXISTS "claude_code_cost_usage_USD_total" (
    //   "greptime_timestamp" TIMESTAMP(3) NOT NULL,
    //   "greptime_value" DOUBLE NULL,
    //   "host_arch" STRING NULL,
    //   "job" STRING NULL,
    //   "model" STRING NULL,
    //   "os_version" STRING NULL,
    //   "otel_scope_name" STRING NULL,
    //   "otel_scope_schema_url" STRING NULL,
    //   "otel_scope_version" STRING NULL,
    //   "service_name" STRING NULL,
    //   "service_version" STRING NULL,
    //   "session_id" STRING NULL,
    //   "terminal_type" STRING NULL,
    //   "user_id" STRING NULL,
    //   TIME INDEX ("greptime_timestamp"),
    //   PRIMARY KEY ("host_arch", "job", "model", "os_version", "otel_scope_name", "otel_scope_schema_url", "otel_scope_version", "service_name", "service_version", "session_id", "terminal_type", "user_id")
    //   )
    // ENGINE=metric
    // WITH(
    //   'comment' = 'Created on insertion',
    //   on_physical_table = 'greptime_physical_table',
    //   otlp_metric_compat = 'prom'
    // )
    let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"greptime_value\\\" DOUBLE NULL,\\n  \\\"host_arch\\\" STRING NULL,\\n  \\\"job\\\" STRING NULL,\\n  \\\"model\\\" STRING NULL,\\n  \\\"os_version\\\" STRING NULL,\\n  \\\"otel_scope_name\\\" STRING NULL,\\n  \\\"otel_scope_schema_url\\\" STRING NULL,\\n  \\\"otel_scope_version\\\" STRING NULL,\\n  \\\"service_name\\\" STRING NULL,\\n  \\\"service_version\\\" STRING NULL,\\n  \\\"session_id\\\" STRING NULL,\\n  \\\"terminal_type\\\" STRING NULL,\\n  \\\"user_id\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\"),\\n  PRIMARY KEY (\\\"host_arch\\\", \\\"job\\\", \\\"model\\\", \\\"os_version\\\", \\\"otel_scope_name\\\", \\\"otel_scope_schema_url\\\", \\\"otel_scope_version\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n  'comment' = 'Created on insertion',\\n  'greptime.semantic.metric.metadata_quality' = 'declared',\\n  'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n  'greptime.semantic.metric.temporality' = 'delta',\\n  'greptime.semantic.metric.type' = 'counter',\\n  'greptime.semantic.metric.unit' = 'USD',\\n  'greptime.semantic.signal_type' = 'metric',\\n  'greptime.semantic.source' = 'opentelemetry',\\n  on_physical_table = 'greptime_physical_table',\\n  otlp_metric_compat = 'prom'\\n)\"]]";
    validate_data(
        "otlp_metrics_all_show_create_table",
        &client,
        "show create table `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // select metrics data
    let expected = "[[1753780559836,2.244618,\"arm64\",\"claude-code\",\"claude-sonnet-4-20250514\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,0.0052544,\"arm64\",\"claude-code\",\"claude-3-5-haiku-20241022\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
    validate_data(
        "otlp_metrics_all_select",
        &client,
        "select * from `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // The synthesized resource descriptor: a plain mito info table keyed by
    // the raw OTel attribute keys (service.name is the only allowlisted
    // resource attribute in this payload), independent of the promote/ignore
    // headers. The timestamp is the newest data-point time.
    validate_data(
        "otlp_metrics_resource_info",
        &client,
        "select job, \"service.name\", greptime_value, greptime_timestamp from greptime_otel_resource_info;",
        "[[\"claude-code\",\"claude-code\",1.0,1753780559836]]",
    )
    .await;
    // All four ownership markers must land on auto-create: the descriptor
    // write path refuses tables that miss any of them, so a missing stamp
    // here would silently stop every follow-up descriptor write.
    validate_data(
        "otlp_metrics_resource_info_options",
        &client,
        "select count(*) from information_schema.tables where table_name = 'greptime_otel_resource_info' \
         and engine = 'mito' \
         and create_options like '%greptime.semantic.metric.type=info%' \
         and create_options like '%greptime.semantic.metric.metadata_quality=declared%' \
         and create_options like '%greptime.semantic.signal_type=metric%' \
         and create_options like '%greptime.semantic.source=opentelemetry%';",
        "[[1]]",
    )
    .await;

    // drop table
    let res = client
        .get("/v1/sql?sql=drop table `claude_code_cost_usage_USD_total`;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?sql=drop table claude_code_token_usage_tokens_total;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?sql=drop table greptime_otel_resource_info;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // write metrics data
    // with scope attrs
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-promote-resource-attrs"),
                HeaderValue::from_static("os.type;os.version"),
            ),
        ],
        "/v1/otlp/v1/metrics",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // CREATE TABLE IF NOT EXISTS "claude_code_cost_usage_USD_total" (
    //     "greptime_timestamp" TIMESTAMP(3) NOT NULL,
    //     "greptime_value" DOUBLE NULL,
    //     "job" STRING NULL,
    //     "model" STRING NULL,
    //     "os_type" STRING NULL,
    //     "os_version" STRING NULL,
    //     "service_name" STRING NULL,
    //     "service_version" STRING NULL,
    //     "session_id" STRING NULL,
    //     "terminal_type" STRING NULL,
    //     "user_id" STRING NULL,
    //     TIME INDEX ("greptime_timestamp"),
    //     PRIMARY KEY ("job", "model", "os_type", "os_version", "service_name", "service_version", "session_id", "terminal_type", "user_id")
    //     )
    //   ENGINE=metric
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     on_physical_table = 'greptime_physical_table',
    //     otlp_metric_compat = 'prom'
    //   )
    let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"greptime_value\\\" DOUBLE NULL,\\n  \\\"job\\\" STRING NULL,\\n  \\\"model\\\" STRING NULL,\\n  \\\"os_type\\\" STRING NULL,\\n  \\\"os_version\\\" STRING NULL,\\n  \\\"service_name\\\" STRING NULL,\\n  \\\"service_version\\\" STRING NULL,\\n  \\\"session_id\\\" STRING NULL,\\n  \\\"terminal_type\\\" STRING NULL,\\n  \\\"user_id\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\"),\\n  PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"os_type\\\", \\\"os_version\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n  'comment' = 'Created on insertion',\\n  'greptime.semantic.metric.metadata_quality' = 'declared',\\n  'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n  'greptime.semantic.metric.temporality' = 'delta',\\n  'greptime.semantic.metric.type' = 'counter',\\n  'greptime.semantic.metric.unit' = 'USD',\\n  'greptime.semantic.signal_type' = 'metric',\\n  'greptime.semantic.source' = 'opentelemetry',\\n  on_physical_table = 'greptime_physical_table',\\n  otlp_metric_compat = 'prom'\\n)\"]]";
    validate_data(
        "otlp_metrics_show_create_table",
        &client,
        "show create table `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // select metrics data
    let expected = "[[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"darwin\",\"25.0.0\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"darwin\",\"25.0.0\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
    validate_data(
        "otlp_metrics_select",
        &client,
        "select * from `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // drop table
    let res = client
        .get("/v1/sql?sql=drop table `claude_code_cost_usage_USD_total`;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?sql=drop table claude_code_token_usage_tokens_total;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // write metrics data
    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        )],
        "/v1/otlp/v1/metrics",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // CREATE TABLE IF NOT EXISTS "claude_code_cost_usage_USD_total" (
    //     "greptime_timestamp" TIMESTAMP(3) NOT NULL,
    //     "greptime_value" DOUBLE NULL,
    //     "job" STRING NULL,
    //     "model" STRING NULL,
    //     "service_name" STRING NULL,
    //     "service_version" STRING NULL,
    //     "session_id" STRING NULL,
    //     "terminal_type" STRING NULL,
    //     "user_id" STRING NULL,
    //     TIME INDEX ("greptime_timestamp"),
    //     PRIMARY KEY ("job", "model", "service_name", "service_version", "session_id", "terminal_type", "user_id")
    //     )
    //   ENGINE=metric
    //   WITH(
    //    'comment' = 'Created on insertion',
    //     on_physical_table = 'greptime_physical_table',
    //     otlp_metric_compat = 'prom'
    //   )
    let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"greptime_value\\\" DOUBLE NULL,\\n  \\\"job\\\" STRING NULL,\\n  \\\"model\\\" STRING NULL,\\n  \\\"service_name\\\" STRING NULL,\\n  \\\"service_version\\\" STRING NULL,\\n  \\\"session_id\\\" STRING NULL,\\n  \\\"terminal_type\\\" STRING NULL,\\n  \\\"user_id\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\"),\\n  PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n  'comment' = 'Created on insertion',\\n  'greptime.semantic.metric.metadata_quality' = 'declared',\\n  'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n  'greptime.semantic.metric.temporality' = 'delta',\\n  'greptime.semantic.metric.type' = 'counter',\\n  'greptime.semantic.metric.unit' = 'USD',\\n  'greptime.semantic.signal_type' = 'metric',\\n  'greptime.semantic.source' = 'opentelemetry',\\n  on_physical_table = 'greptime_physical_table',\\n  otlp_metric_compat = 'prom'\\n)\"]]";
    validate_data(
        "otlp_metrics_show_create_table_none",
        &client,
        "show create table `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // select metrics data
    let expected = "[[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
    validate_data(
        "otlp_metrics_select_none",
        &client,
        "select * from `claude_code_cost_usage_USD_total`;",
        expected,
    )
    .await;

    // the descriptor was auto-created again after the earlier drop
    validate_data(
        "otlp_metrics_resource_info_recreated",
        &client,
        "select count(*) from information_schema.tables \
         where table_name = 'greptime_otel_resource_info' and engine = 'mito';",
        "[[1]]",
    )
    .await;

    // drop table
    let res = client
        .get("/v1/sql?sql=drop table `claude_code_cost_usage_USD_total`;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?sql=drop table claude_code_token_usage_tokens_total;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);
    let res = client
        .get("/v1/sql?sql=drop table greptime_otel_resource_info;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // job composition end-to-end: service.namespace folds into "<ns>/<name>"
    // on both the descriptor and the metric tables
    let content = r#"
{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"api"}},{"key":"service.namespace","value":{"stringValue":"shop"}},{"key":"host.id","value":{"stringValue":"h-1"}}]},"scopeMetrics":[{"scope":{"name":"s"},"metrics":[{"name":"ns_gauge","gauge":{"dataPoints":[{"timeUnixNano":"1753780559836000000","asDouble":1.0}]}}]}]}]}
    "#;
    let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        )],
        "/v1/otlp/v1/metrics",
        req.encode_to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    validate_data(
        "otlp_metrics_namespace_job_descriptor",
        &client,
        "select job, \"service.name\", \"service.namespace\", \"host.id\" from greptime_otel_resource_info;",
        "[[\"shop/api\",\"api\",\"shop\",\"h-1\"]]",
    )
    .await;
    validate_data(
        "otlp_metrics_namespace_job_metric_table",
        &client,
        "select job from ns_gauge;",
        "[[\"shop/api\"]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_otlp_exponential_histogram(store_type: StorageType) {
    use opentelemetry_proto::tonic::metrics::v1::{
        AggregationTemporality, ExponentialHistogram, ExponentialHistogramDataPoint, Metric,
        ResourceMetrics, ScopeMetrics, exponential_histogram_data_point, metric,
    };
    use tests_integration::test_util::setup_test_http_app_with_otlp_exponential_histogram;

    common_telemetry::init_default_ut_logging();
    let req = ExportMetricsServiceRequest {
        resource_metrics: vec![ResourceMetrics {
            scope_metrics: vec![ScopeMetrics {
                metrics: vec![Metric {
                    name: "otlp.exponential.latency".to_string(),
                    data: Some(metric::Data::ExponentialHistogram(ExponentialHistogram {
                        data_points: vec![
                            ExponentialHistogramDataPoint {
                                start_time_unix_nano: 1_000_000_000,
                                time_unix_nano: 3_000_000_000,
                                count: 4,
                                sum: Some(8.0),
                                scale: 0,
                                zero_count: 1,
                                positive: Some(exponential_histogram_data_point::Buckets {
                                    offset: -1,
                                    bucket_counts: vec![1, 2],
                                }),
                                ..Default::default()
                            },
                            ExponentialHistogramDataPoint {
                                start_time_unix_nano: 1_000_000_000,
                                time_unix_nano: 4_000_000_000,
                                scale: -5,
                                ..Default::default()
                            },
                        ],
                        aggregation_temporality: AggregationTemporality::Cumulative as i32,
                    })),
                    ..Default::default()
                }],
                ..Default::default()
            }],
            ..Default::default()
        }],
    };
    let body = req.encode_to_vec();
    let headers = || {
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        )]
    };

    let (app, mut guard) = setup_test_http_app_with_otlp_exponential_histogram(
        store_type,
        "test_otlp_exponential_histogram_disabled",
        false,
    )
    .await;
    let client = TestClient::new(app).await;
    let res = send_req(
        &client,
        headers(),
        "/v1/otlp/v1/metrics",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    let status = GoogleRpcStatus::decode(res.bytes().await.as_ref()).unwrap();
    assert_eq!(3, status.code);
    assert!(
        status
            .message
            .contains("otlp.experimental_enable_exponential_histogram")
    );
    validate_data(
        "otlp_exponential_histogram_disabled_no_table",
        &client,
        "select count(*) from information_schema.tables where table_name = 'otlp_exponential_latency';",
        "[[0]]",
    )
    .await;
    guard.remove_all().await;

    let (app, mut guard) = setup_test_http_app_with_otlp_exponential_histogram(
        store_type,
        "test_otlp_exponential_histogram_enabled",
        true,
    )
    .await;
    let client = TestClient::new(app).await;
    let res = send_req(&client, headers(), "/v1/otlp/v1/metrics", body, false).await;
    assert_eq!(StatusCode::OK, res.status());
    let response = ExportMetricsServiceResponse::decode(res.bytes().await).unwrap();
    let partial_success = response.partial_success.unwrap();
    assert_eq!(1, partial_success.rejected_data_points);
    assert!(partial_success.error_message.contains("scale -5"));

    validate_data(
        "otlp_exponential_histogram_row",
        &client,
        "select greptime_timestamp, greptime_native_histogram from otlp_exponential_latency;",
        "[[3000,{\"count_f64\":null,\"count_i64\":4,\"custom_values\":[],\"negative_buckets_f64\":[],\"negative_buckets_i64\":[],\"negative_span_lengths\":[],\"negative_span_offsets\":[],\"positive_buckets_f64\":[],\"positive_buckets_i64\":[1,2],\"positive_span_lengths\":[2],\"positive_span_offsets\":[0],\"reset_hint\":0,\"schema\":0,\"start_timestamp\":1000,\"sum\":8.0,\"zero_count_f64\":null,\"zero_count_i64\":1,\"zero_threshold\":0.0}]]",
    )
    .await;
    validate_data(
        "otlp_exponential_histogram_metadata",
        &client,
        "select count(*) from information_schema.tables where table_name = 'otlp_exponential_latency' and create_options like '%greptime.semantic.metric.type=histogram%' and create_options like '%greptime.semantic.metric.temporality=cumulative%';",
        "[[1]]",
    )
    .await;

    for query in [
        "histogram_count(otlp_exponential_latency)",
        "histogram_sum(otlp_exponential_latency)",
        "histogram_quantile(0.5,otlp_exponential_latency)",
        "histogram_count(rate(otlp_exponential_latency[5s]))",
    ] {
        let res = client
            .get(&format!(
                "/v1/prometheus/api/v1/query?query={}&time=3",
                encode(query)
            ))
            .send()
            .await;
        assert_eq!(StatusCode::OK, res.status(), "query: {query}");
        let body = res.text().await;
        assert!(body.contains("otlp_exponential_latency"), "{query}: {body}");
    }

    guard.remove_all().await;
}

pub async fn test_otlp_metric_translation_strategies(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_otlp_metric_translation_strategies")
            .await;

    let content = r#"
{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"strategy-service"}},{"key":"resource.attr","value":{"stringValue":"resource-a"}}]},"scopeMetrics":[{"scope":{"name":"test.scope","version":"1.0.0","attributes":[{"key":"scope.attr","value":{"stringValue":"scope-a"}}]},"schemaUrl":"https://example.com/schema","metrics":[{"name":"otel.strategy.duration","description":"strategy test metric","unit":"ms","sum":{"dataPoints":[{"attributes":[{"key":"data.point.label","value":{"stringValue":"duration-a"}}],"startTimeUnixNano":"1000000","timeUnixNano":"2000000","asDouble":42.0}],"aggregationTemporality":2,"isMonotonic":true}}]}]}]}
    "#;

    let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();
    let client = TestClient::new(app).await;

    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-translation-strategy"),
                HeaderValue::from_static("NoTranslation"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-promote-scope-attrs"),
                HeaderValue::from_static("true"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-promote-all-resource-attrs"),
                HeaderValue::from_static("true"),
            ),
        ],
        "/v1/otlp/v1/metrics",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = "[[\"otel.strategy.duration\"]]";
    validate_data(
        "otlp_metric_translation_strategy_tables",
        &client,
        "select table_name from information_schema.tables where table_schema = 'public' and table_name = 'otel.strategy.duration';",
        expected,
    )
    .await;

    let expected = "[[42.0,\"duration-a\",\"scope-a\",\"resource-a\",\"strategy-service\"]]";
    validate_data(
        "otlp_metric_translation_strategy_ingestion",
        &client,
        "select greptime_value, \"data.point.label\", \"otel_scope_scope.attr\", \"resource.attr\", job from \"otel.strategy.duration\";",
        expected,
    )
    .await;

    // the descriptor's columns are the fixed raw keys regardless of the
    // translation strategy, and only allowlisted attributes are projected
    validate_data(
        "otlp_metric_translation_strategy_resource_info",
        &client,
        "select job, \"service.name\" from greptime_otel_resource_info;",
        "[[\"strategy-service\",\"strategy-service\"]]",
    )
    .await;
    validate_data(
        "otlp_metric_translation_strategy_resource_info_allowlist",
        &client,
        "select count(*) from information_schema.columns \
         where table_name = 'greptime_otel_resource_info' and column_name = 'resource.attr';",
        "[[0]]",
    )
    .await;

    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-otlp-metric-translation-strategy"),
                HeaderValue::from_static("no_translation"),
            ),
        ],
        "/v1/otlp/v1/metrics",
        body,
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());

    guard.remove_all().await;
}

pub async fn test_otlp_metrics_resource_info_conflicts(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_otlp_metrics_resource_info_conflicts")
            .await;
    let client = TestClient::new(app).await;

    let content_type = || {
        (
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        )
    };

    // A metric named like the descriptor wins the table name: it goes through
    // the normal metric-engine path and no descriptor is synthesized.
    let content = r#"
{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"api"}},{"key":"host.id","value":{"stringValue":"h-1"}}]},"scopeMetrics":[{"scope":{"name":"s"},"metrics":[{"name":"greptime_otel_resource_info","gauge":{"dataPoints":[{"timeUnixNano":"1753780559836000000","asDouble":1.0}]}}]}]}]}
    "#;
    let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
    let res = send_req(
        &client,
        vec![content_type()],
        "/v1/otlp/v1/metrics",
        req.encode_to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let body = ExportMetricsServiceResponse::decode(res.bytes().await).unwrap();
    assert!(body.partial_success.is_none());
    validate_data(
        "otlp_metrics_name_collision_engine",
        &client,
        "select count(*) from information_schema.tables \
         where table_name = 'greptime_otel_resource_info' and engine = 'metric';",
        "[[1]]",
    )
    .await;
    let res = execute_sql(&client, "drop table greptime_otel_resource_info;").await;
    assert_eq!(res.status(), StatusCode::OK);

    // A pre-existing table with an incompatible schema fails the descriptor
    // write, which degrades to an OTLP partial-success warning while the
    // metric data itself is accepted.
    let res = execute_sql(
        &client,
        "create table greptime_otel_resource_info (ts timestamp time index, job double, greptime_value double);",
    )
    .await;
    assert_eq!(res.status(), StatusCode::OK);

    let content = r#"
{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"api"}}]},"scopeMetrics":[{"scope":{"name":"s"},"metrics":[{"name":"conflict_gauge","gauge":{"dataPoints":[{"timeUnixNano":"1753780559836000000","asDouble":7.0}]}}]}]}]}
    "#;
    let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
    let res = send_req(
        &client,
        vec![content_type()],
        "/v1/otlp/v1/metrics",
        req.encode_to_vec(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let body = ExportMetricsServiceResponse::decode(res.bytes().await).unwrap();
    let partial_success = body.partial_success.as_ref().unwrap();
    assert_eq!(partial_success.rejected_data_points, 0);
    assert!(
        partial_success
            .error_message
            .contains("greptime_otel_resource_info"),
        "unexpected warning: {}",
        partial_success.error_message
    );
    validate_data(
        "otlp_metrics_conflict_main_data_accepted",
        &client,
        "select greptime_value from conflict_gauge;",
        "[[7.0]]",
    )
    .await;

    // The descriptor is auto-created again once the conflicting table is
    // gone, and a follow-up write into it does not degrade.
    let res = execute_sql(&client, "drop table greptime_otel_resource_info;").await;
    assert_eq!(res.status(), StatusCode::OK);
    for round in 0..2 {
        let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
        let res = send_req(
            &client,
            vec![content_type()],
            "/v1/otlp/v1/metrics",
            req.encode_to_vec(),
            false,
        )
        .await;
        assert_eq!(StatusCode::OK, res.status());
        let body = ExportMetricsServiceResponse::decode(res.bytes().await).unwrap();
        assert!(
            body.partial_success.is_none(),
            "round {round} unexpectedly degraded: {:?}",
            body.partial_success
        );
    }
    validate_data(
        "otlp_metrics_owned_descriptor_keeps_accepting",
        &client,
        "select count(*) from greptime_otel_resource_info;",
        "[[1]]",
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_otlp_traces_v0(store_type: StorageType) {
    // init
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_otlp_traces").await;

    let content = r#"
{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"telemetrygen"}}],"droppedAttributesCount":0},"scopeSpans":[{"scope":{"name":"telemetrygen","version":"","attributes":[],"droppedAttributesCount":0},"spans":[{"traceId":"c05d7a4ec8e1f231f02ed6e8da8655b4","spanId":"9630f2916e2f7909","traceState":"","parentSpanId":"d24f921c75f68e23","flags":256,"name":"okey-dokey-0","kind":2,"startTimeUnixNano":"1736480942444376000","endTimeUnixNano":"1736480942444499000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-client"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"c05d7a4ec8e1f231f02ed6e8da8655b4","spanId":"d24f921c75f68e23","traceState":"","parentSpanId":"","flags":256,"name":"lets-go","kind":3,"startTimeUnixNano":"1736480942444376000","endTimeUnixNano":"1736480942444499000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-server"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"cc9e0991a2e63d274984bd44ee669203","spanId":"8f847259b0f6e1ab","traceState":"","parentSpanId":"eba7be77e3558179","flags":256,"name":"okey-dokey-0","kind":2,"startTimeUnixNano":"1736480942444589000","endTimeUnixNano":"1736480942444712000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-client"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"cc9e0991a2e63d274984bd44ee669203","spanId":"eba7be77e3558179","traceState":"","parentSpanId":"","flags":256,"name":"lets-go","kind":3,"startTimeUnixNano":"1736480942444589000","endTimeUnixNano":"1736480942444712000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-server"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}}],"schemaUrl":""}],"schemaUrl":"https://opentelemetry.io/schemas/1.4.0"}]}
"#;

    let req: ExportTraceServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();

    // handshake
    let client = TestClient::new(app).await;

    // write traces data
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static("greptime_trace_v0"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["telemetrygen"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        &format!(
            "select service_name from {};",
            trace_services_table_name(TRACE_TABLE_NAME)
        ),
        expected,
    )
    .await;

    // Validate operations table
    let expected = r#"[["telemetrygen","SPAN_KIND_CLIENT","lets-go"],["telemetrygen","SPAN_KIND_SERVER","okey-dokey-0"]]"#;
    validate_data(
        "otlp_traces_operations",
        &client,
        &format!(
            "select service_name, span_kind, span_name from {} order by span_kind, span_name;",
            trace_operations_table_name(TRACE_TABLE_NAME)
        ),
        expected,
    )
    .await;

    // select traces data
    let expected = r#"[[1736480942444376000,1736480942444499000,123000,"c05d7a4ec8e1f231f02ed6e8da8655b4","d24f921c75f68e23",null,"SPAN_KIND_CLIENT","lets-go","STATUS_CODE_UNSET","","","telemetrygen",{"net.peer.ip":"1.2.3.4","peer.service":"telemetrygen-server"},[],[],"telemetrygen","",{},{"service.name":"telemetrygen"}],[1736480942444376000,1736480942444499000,123000,"c05d7a4ec8e1f231f02ed6e8da8655b4","9630f2916e2f7909","d24f921c75f68e23","SPAN_KIND_SERVER","okey-dokey-0","STATUS_CODE_UNSET","","","telemetrygen",{"net.peer.ip":"1.2.3.4","peer.service":"telemetrygen-client"},[],[],"telemetrygen","",{},{"service.name":"telemetrygen"}],[1736480942444589000,1736480942444712000,123000,"cc9e0991a2e63d274984bd44ee669203","eba7be77e3558179",null,"SPAN_KIND_CLIENT","lets-go","STATUS_CODE_UNSET","","","telemetrygen",{"net.peer.ip":"1.2.3.4","peer.service":"telemetrygen-server"},[],[],"telemetrygen","",{},{"service.name":"telemetrygen"}],[1736480942444589000,1736480942444712000,123000,"cc9e0991a2e63d274984bd44ee669203","8f847259b0f6e1ab","eba7be77e3558179","SPAN_KIND_SERVER","okey-dokey-0","STATUS_CODE_UNSET","","","telemetrygen",{"net.peer.ip":"1.2.3.4","peer.service":"telemetrygen-client"},[],[],"telemetrygen","",{},{"service.name":"telemetrygen"}]]"#;
    validate_data(
        "otlp_traces",
        &client,
        "select * from opentelemetry_traces;",
        expected,
    )
    .await;

    // The v0 data model is frozen on the unsigned `duration_nano`: pin the
    // schema so the unsigned→signed flip of the built-in models (which flips
    // only v1) cannot leak into v0 and break writes into pre-existing v0
    // tables.
    validate_data(
        "otlp_traces_v0_schema",
        &client,
        "select column_name, lower(data_type) from information_schema.columns \
         where table_name = 'opentelemetry_traces' and column_name = 'duration_nano';",
        r#"[["duration_nano","bigint unsigned"]]"#,
    )
    .await;

    // drop table
    let res = client
        .get("/v1/sql?sql=drop table opentelemetry_traces;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // write traces data with gzip
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static("greptime_trace_v0"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        true,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // write traces data without pipeline
    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        )],
        "/v1/otlp/v1/traces",
        body.clone(),
        true,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());

    // select traces data again
    validate_data(
        "otlp_traces_with_gzip",
        &client,
        "select * from opentelemetry_traces;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

/// One real OTLP export must come out of `semantic_relationships` as the
/// zero-configuration chain; resources missing an identity column derive
/// nothing extra.
pub async fn test_otlp_traces_v1_entity_graph(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_otlp_traces_entity_graph").await;
    let client = TestClient::new(app).await;

    let span = |trace_id: &str, span_id: &str, parent: &str, kind: u32| {
        json!({
            "traceId": trace_id,
            "spanId": span_id,
            "parentSpanId": parent,
            "name": "op",
            "kind": kind,
            "startTimeUnixNano": "1736480942444376000",
            "endTimeUnixNano": "1736480942444499000",
            "attributes": [],
            "status": { "message": "", "code": 0 }
        })
    };
    let resource_spans = |attributes: Vec<Value>, spans: Vec<Value>| {
        json!({
            "resource": { "attributes": attributes },
            "scopeSpans": [{ "scope": { "name": "graph-tests" }, "spans": spans }],
            "schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
        })
    };

    // frontend: full identity (instance id + pod uid); cart: bare service —
    // no instance entity; batch: instance id but no pod uid — no runs_on.
    let req: ExportTraceServiceRequest = serde_json::from_value(json!({
        "resourceSpans": [
            resource_spans(
                vec![
                    make_string_attr("service.name", "frontend"),
                    make_string_attr("service.instance.id", "inst-1"),
                    make_string_attr("k8s.pod.uid", "uid-9"),
                    make_string_attr("k8s.pod.name", "api-1"),
                    make_string_attr("k8s.namespace.name", "default"),
                ],
                vec![span("c05d7a4ec8e1f231f02ed6e8da8655b4", "d24f921c75f68e23", "", 3)],
            ),
            resource_spans(
                vec![make_string_attr("service.name", "cart")],
                vec![span(
                    "c05d7a4ec8e1f231f02ed6e8da8655b4",
                    "9630f2916e2f7909",
                    "d24f921c75f68e23",
                    2,
                )],
            ),
            resource_spans(
                vec![
                    make_string_attr("service.name", "batch"),
                    make_string_attr("service.instance.id", "binst-1"),
                ],
                vec![span("cc9e0991a2e63d274984bd44ee669203", "8f847259b0f6e1ab", "", 1)],
            ),
        ]
    }))
    .unwrap();
    let res = send_trace_v1_req(&client, "graph_traces", req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["service","frontend","service","cart","calls"],["service.instance","batch,binst-1","service","batch","part_of"],["service.instance","frontend,inst-1","service","frontend","part_of"],["service.instance","frontend,inst-1","k8s.pod","uid-9","runs_on"]]"#;
    validate_data(
        "otlp_traces_entity_graph",
        &client,
        "select src_type, src_id, dst_type, dst_id, rel_type \
         from greptime_private.semantic_relationships \
         where observed_at >= '2025-01-01 00:00:00' order by rel_type, src_id;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_otlp_traces_v1(store_type: StorageType) {
    // init
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_otlp_traces").await;

    let content = r#"
{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"telemetrygen"}}],"droppedAttributesCount":0},"scopeSpans":[{"scope":{"name":"telemetrygen","version":"","attributes":[],"droppedAttributesCount":0},"spans":[{"traceId":"c05d7a4ec8e1f231f02ed6e8da8655b4","spanId":"9630f2916e2f7909","traceState":"","parentSpanId":"d24f921c75f68e23","flags":256,"name":"okey-dokey-0","kind":2,"startTimeUnixNano":"1736480942444376000","endTimeUnixNano":"1736480942444499000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-client"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"c05d7a4ec8e1f231f02ed6e8da8655b4","spanId":"d24f921c75f68e23","traceState":"","parentSpanId":"","flags":256,"name":"lets-go","kind":3,"startTimeUnixNano":"1736480942444376000","endTimeUnixNano":"1736480942444499000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-server"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"cc9e0991a2e63d274984bd44ee669203","spanId":"8f847259b0f6e1ab","traceState":"","parentSpanId":"eba7be77e3558179","flags":256,"name":"okey-dokey-0","kind":2,"startTimeUnixNano":"1736480942444589000","endTimeUnixNano":"1736480942444712000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-client"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}},{"traceId":"cc9e0991a2e63d274984bd44ee669203","spanId":"eba7be77e3558179","traceState":"","parentSpanId":"","flags":256,"name":"lets-go","kind":3,"startTimeUnixNano":"1736480942444589000","endTimeUnixNano":"1736480942444712000","attributes":[{"key":"net.peer.ip","value":{"stringValue":"1.2.3.4"}},{"key":"peer.service","value":{"stringValue":"telemetrygen-server"}}],"droppedAttributesCount":0,"events":[],"droppedEventsCount":0,"links":[],"droppedLinksCount":0,"status":{"message":"","code":0}}],"schemaUrl":""}],"schemaUrl":"https://opentelemetry.io/schemas/1.4.0"}]}
"#;

    let trace_table_name = "mytable";
    let req: ExportTraceServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();

    // handshake
    let client = TestClient::new(app).await;

    // write traces data
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_name),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"[["telemetrygen"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        &format!(
            "select service_name from {};",
            trace_services_table_name(trace_table_name)
        ),
        expected,
    )
    .await;

    // Validate operations table
    let expected = r#"[["telemetrygen","SPAN_KIND_CLIENT","lets-go"],["telemetrygen","SPAN_KIND_SERVER","okey-dokey-0"]]"#;
    validate_data(
        "otlp_traces_operations_v1",
        &client,
        &format!(
            "select service_name, span_kind, span_name from {} order by span_kind, span_name;",
            trace_operations_table_name(trace_table_name)
        ),
        expected,
    )
    .await;

    // select traces data
    let expected = r#"[[1736480942444376000,1736480942444499000,123000,null,"c05d7a4ec8e1f231f02ed6e8da8655b4","d24f921c75f68e23","SPAN_KIND_CLIENT","lets-go","STATUS_CODE_UNSET","","","telemetrygen","","telemetrygen","1.2.3.4","telemetrygen-server",[],[]],[1736480942444376000,1736480942444499000,123000,"d24f921c75f68e23","c05d7a4ec8e1f231f02ed6e8da8655b4","9630f2916e2f7909","SPAN_KIND_SERVER","okey-dokey-0","STATUS_CODE_UNSET","","","telemetrygen","","telemetrygen","1.2.3.4","telemetrygen-client",[],[]],[1736480942444589000,1736480942444712000,123000,null,"cc9e0991a2e63d274984bd44ee669203","eba7be77e3558179","SPAN_KIND_CLIENT","lets-go","STATUS_CODE_UNSET","","","telemetrygen","","telemetrygen","1.2.3.4","telemetrygen-server",[],[]],[1736480942444589000,1736480942444712000,123000,"eba7be77e3558179","cc9e0991a2e63d274984bd44ee669203","8f847259b0f6e1ab","SPAN_KIND_SERVER","okey-dokey-0","STATUS_CODE_UNSET","","","telemetrygen","","telemetrygen","1.2.3.4","telemetrygen-client",[],[]]]"#;
    validate_data("otlp_traces", &client, "select * from mytable;", expected).await;

    // The trace v1 main table carries the trace identity (events/links preserved as
    // JSON columns by the v1 model).
    validate_data(
        "otlp_traces_semantic_identity",
        &client,
        "select count(*) from information_schema.tables where table_name = 'mytable' \
         and create_options like '%greptime.semantic.signal_type=trace%' \
         and create_options like '%greptime.semantic.source=opentelemetry%' \
         and create_options like '%greptime.semantic.pipeline=greptime_trace_v1%';",
        "[[1]]",
    )
    .await;

    let expected_ddl = r#"[["mytable","CREATE TABLE IF NOT EXISTS \"mytable\" (\n  \"timestamp\" TIMESTAMP(9) NOT NULL,\n  \"timestamp_end\" TIMESTAMP(9) NULL,\n  \"duration_nano\" BIGINT NULL,\n  \"parent_span_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"trace_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_id\" STRING NULL,\n  \"span_kind\" STRING NULL,\n  \"span_name\" STRING NULL,\n  \"span_status_code\" STRING NULL,\n  \"span_status_message\" STRING NULL,\n  \"trace_state\" STRING NULL,\n  \"scope_name\" STRING NULL,\n  \"scope_version\" STRING NULL,\n  \"service_name\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_attributes.net.peer.ip\" STRING NULL,\n  \"span_attributes.peer.service\" STRING NULL,\n  \"span_events\" JSON NULL,\n  \"span_links\" JSON NULL,\n  TIME INDEX (\"timestamp\"),\n  PRIMARY KEY (\"service_name\")\n)\nPARTITION ON COLUMNS (\"trace_id\") (\n  trace_id < '1',\n  trace_id >= '1' AND trace_id < '2',\n  trace_id >= '2' AND trace_id < '3',\n  trace_id >= '3' AND trace_id < '4',\n  trace_id >= '4' AND trace_id < '5',\n  trace_id >= '5' AND trace_id < '6',\n  trace_id >= '6' AND trace_id < '7',\n  trace_id >= '7' AND trace_id < '8',\n  trace_id >= '8' AND trace_id < '9',\n  trace_id >= '9' AND trace_id < 'a',\n  trace_id >= 'a' AND trace_id < 'b',\n  trace_id >= 'b' AND trace_id < 'c',\n  trace_id >= 'c' AND trace_id < 'd',\n  trace_id >= 'd' AND trace_id < 'e',\n  trace_id >= 'e' AND trace_id < 'f',\n  trace_id >= 'f'\n)\nENGINE=mito\nWITH(\n  'comment' = 'Created on insertion',\n  append_mode = 'true',\n  'greptime.semantic.entity.service.id' = 'service_name',\n  'greptime.semantic.pipeline' = 'greptime_trace_v1',\n  'greptime.semantic.signal_type' = 'trace',\n  'greptime.semantic.source' = 'opentelemetry',\n  'greptime.semantic.trace.conventions' = 'https://opentelemetry.io/schemas/1.4.0',\n  table_data_model = 'greptime_trace_v1'\n)"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        "show create table mytable;",
        expected_ddl,
    )
    .await;

    let expected_ddl = r#"[["mytable_services","CREATE TABLE IF NOT EXISTS \"mytable_services\" (\n  \"timestamp\" TIMESTAMP(9) NOT NULL,\n  \"service_name\" STRING NULL,\n  TIME INDEX (\"timestamp\"),\n  PRIMARY KEY (\"service_name\")\n)\n\nENGINE=mito\nWITH(\n  'comment' = 'Created on insertion',\n  append_mode = 'false'\n)"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        &format!(
            "show create table {};",
            trace_services_table_name(trace_table_name)
        ),
        expected_ddl,
    )
    .await;

    // drop table
    let res = client.get("/v1/sql?sql=drop table mytable;").send().await;
    assert_eq!(res.status(), StatusCode::OK);

    // test trace table with custom partitions: 1
    let trace_table_part1 = "trace_table_part1";
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_part1),
            ),
            (
                HeaderName::from_static("x-greptime-hints"),
                HeaderValue::from_static("trace_table_partitions=1"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected_ddl = r#"[["trace_table_part1","CREATE TABLE IF NOT EXISTS \"trace_table_part1\" (\n  \"timestamp\" TIMESTAMP(9) NOT NULL,\n  \"timestamp_end\" TIMESTAMP(9) NULL,\n  \"duration_nano\" BIGINT NULL,\n  \"parent_span_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"trace_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_id\" STRING NULL,\n  \"span_kind\" STRING NULL,\n  \"span_name\" STRING NULL,\n  \"span_status_code\" STRING NULL,\n  \"span_status_message\" STRING NULL,\n  \"trace_state\" STRING NULL,\n  \"scope_name\" STRING NULL,\n  \"scope_version\" STRING NULL,\n  \"service_name\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_attributes.net.peer.ip\" STRING NULL,\n  \"span_attributes.peer.service\" STRING NULL,\n  \"span_events\" JSON NULL,\n  \"span_links\" JSON NULL,\n  TIME INDEX (\"timestamp\"),\n  PRIMARY KEY (\"service_name\")\n)\n\nENGINE=mito\nWITH(\n  'comment' = 'Created on insertion',\n  append_mode = 'true',\n  'greptime.semantic.entity.service.id' = 'service_name',\n  'greptime.semantic.pipeline' = 'greptime_trace_v1',\n  'greptime.semantic.signal_type' = 'trace',\n  'greptime.semantic.source' = 'opentelemetry',\n  'greptime.semantic.trace.conventions' = 'https://opentelemetry.io/schemas/1.4.0',\n  table_data_model = 'greptime_trace_v1'\n)"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        "show create table trace_table_part1;",
        expected_ddl,
    )
    .await;

    // test trace table with custom partitions: 4
    let trace_table_part4 = "trace_table_part4";
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_part4),
            ),
            (
                HeaderName::from_static("x-greptime-hints"),
                HeaderValue::from_static("trace_table_partitions=4"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected_ddl = r#"[["trace_table_part4","CREATE TABLE IF NOT EXISTS \"trace_table_part4\" (\n  \"timestamp\" TIMESTAMP(9) NOT NULL,\n  \"timestamp_end\" TIMESTAMP(9) NULL,\n  \"duration_nano\" BIGINT NULL,\n  \"parent_span_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"trace_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_id\" STRING NULL,\n  \"span_kind\" STRING NULL,\n  \"span_name\" STRING NULL,\n  \"span_status_code\" STRING NULL,\n  \"span_status_message\" STRING NULL,\n  \"trace_state\" STRING NULL,\n  \"scope_name\" STRING NULL,\n  \"scope_version\" STRING NULL,\n  \"service_name\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_attributes.net.peer.ip\" STRING NULL,\n  \"span_attributes.peer.service\" STRING NULL,\n  \"span_events\" JSON NULL,\n  \"span_links\" JSON NULL,\n  TIME INDEX (\"timestamp\"),\n  PRIMARY KEY (\"service_name\")\n)\nPARTITION ON COLUMNS (\"trace_id\") (\n  trace_id < '4',\n  trace_id >= '4' AND trace_id < '8',\n  trace_id >= '8' AND trace_id < 'c',\n  trace_id >= 'c'\n)\nENGINE=mito\nWITH(\n  'comment' = 'Created on insertion',\n  append_mode = 'true',\n  'greptime.semantic.entity.service.id' = 'service_name',\n  'greptime.semantic.pipeline' = 'greptime_trace_v1',\n  'greptime.semantic.signal_type' = 'trace',\n  'greptime.semantic.source' = 'opentelemetry',\n  'greptime.semantic.trace.conventions' = 'https://opentelemetry.io/schemas/1.4.0',\n  table_data_model = 'greptime_trace_v1'\n)"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        "show create table trace_table_part4;",
        expected_ddl,
    )
    .await;

    // test trace table with custom partitions: 32
    let trace_table_part32 = "trace_table_part32";
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_part32),
            ),
            (
                HeaderName::from_static("x-greptime-hints"),
                HeaderValue::from_static("trace_table_partitions=32"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected_ddl = r#"[["trace_table_part32","CREATE TABLE IF NOT EXISTS \"trace_table_part32\" (\n  \"timestamp\" TIMESTAMP(9) NOT NULL,\n  \"timestamp_end\" TIMESTAMP(9) NULL,\n  \"duration_nano\" BIGINT NULL,\n  \"parent_span_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"trace_id\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_id\" STRING NULL,\n  \"span_kind\" STRING NULL,\n  \"span_name\" STRING NULL,\n  \"span_status_code\" STRING NULL,\n  \"span_status_message\" STRING NULL,\n  \"trace_state\" STRING NULL,\n  \"scope_name\" STRING NULL,\n  \"scope_version\" STRING NULL,\n  \"service_name\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\n  \"span_attributes.net.peer.ip\" STRING NULL,\n  \"span_attributes.peer.service\" STRING NULL,\n  \"span_events\" JSON NULL,\n  \"span_links\" JSON NULL,\n  TIME INDEX (\"timestamp\"),\n  PRIMARY KEY (\"service_name\")\n)\nPARTITION ON COLUMNS (\"trace_id\") (\n  trace_id < '08',\n  trace_id >= '08' AND trace_id < '10',\n  trace_id >= '10' AND trace_id < '18',\n  trace_id >= '18' AND trace_id < '20',\n  trace_id >= '20' AND trace_id < '28',\n  trace_id >= '28' AND trace_id < '30',\n  trace_id >= '30' AND trace_id < '38',\n  trace_id >= '38' AND trace_id < '40',\n  trace_id >= '40' AND trace_id < '48',\n  trace_id >= '48' AND trace_id < '50',\n  trace_id >= '50' AND trace_id < '58',\n  trace_id >= '58' AND trace_id < '60',\n  trace_id >= '60' AND trace_id < '68',\n  trace_id >= '68' AND trace_id < '70',\n  trace_id >= '70' AND trace_id < '78',\n  trace_id >= '78' AND trace_id < '80',\n  trace_id >= '80' AND trace_id < '88',\n  trace_id >= '88' AND trace_id < '90',\n  trace_id >= '90' AND trace_id < '98',\n  trace_id >= '98' AND trace_id < 'a0',\n  trace_id >= 'a0' AND trace_id < 'a8',\n  trace_id >= 'a8' AND trace_id < 'b0',\n  trace_id >= 'b0' AND trace_id < 'b8',\n  trace_id >= 'b8' AND trace_id < 'c0',\n  trace_id >= 'c0' AND trace_id < 'c8',\n  trace_id >= 'c8' AND trace_id < 'd0',\n  trace_id >= 'd0' AND trace_id < 'd8',\n  trace_id >= 'd8' AND trace_id < 'e0',\n  trace_id >= 'e0' AND trace_id < 'e8',\n  trace_id >= 'e8' AND trace_id < 'f0',\n  trace_id >= 'f0' AND trace_id < 'f8',\n  trace_id >= 'f8'\n)\nENGINE=mito\nWITH(\n  'comment' = 'Created on insertion',\n  append_mode = 'true',\n  'greptime.semantic.entity.service.id' = 'service_name',\n  'greptime.semantic.pipeline' = 'greptime_trace_v1',\n  'greptime.semantic.signal_type' = 'trace',\n  'greptime.semantic.source' = 'opentelemetry',\n  'greptime.semantic.trace.conventions' = 'https://opentelemetry.io/schemas/1.4.0',\n  table_data_model = 'greptime_trace_v1'\n)"]]"#;
    validate_data(
        "otlp_traces",
        &client,
        "show create table trace_table_part32;",
        expected_ddl,
    )
    .await;

    // invalid partition count
    let trace_table_part_abc = "trace_table_part_abc";
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_part_abc),
            ),
            (
                HeaderName::from_static("x-greptime-hints"),
                HeaderValue::from_static("trace_table_partitions=abc"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());

    // write traces data with gzip
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_name),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        true,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // select traces data again
    validate_data(
        "otlp_traces_with_gzip",
        &client,
        "select * from mytable;",
        expected,
    )
    .await;

    let coercion_table_name = "trace_type_coercion";
    let coercion_req = make_trace_v1_request(
        "type-coercion",
        vec![make_trace_v1_span(
            "00000000000000000000000000000001",
            "0000000000000001",
            "coercion-seed",
            1_736_480_942_444_376_000,
            1_736_480_942_444_499_000,
            vec![
                make_double_attr("attr_float", 1.5),
                make_int_attr("attr_int", 10),
                make_bool_attr("attr_bool", true),
            ],
        )],
    );
    let res = send_trace_v1_req(&client, coercion_table_name, coercion_req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    let coercion_req = make_trace_v1_request(
        "type-coercion",
        vec![make_trace_v1_span(
            "00000000000000000000000000000002",
            "0000000000000002",
            "coercion-apply",
            1_736_480_942_444_589_000,
            1_736_480_942_444_712_000,
            vec![
                make_int_attr("attr_float", 2),
                make_string_attr("attr_int", "20"),
                make_string_attr("attr_bool", "false"),
            ],
        )],
    );
    let res = send_trace_v1_req(&client, coercion_table_name, coercion_req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    let string_target_table_name = "trace_type_coercion_to_string";
    let string_target_seed_req = make_trace_v1_request(
        "type-coercion-string",
        vec![make_trace_v1_span(
            "00000000000000000000000000000021",
            "0000000000000021",
            "string-target-seed",
            1_736_480_942_444_720_000,
            1_736_480_942_444_820_000,
            vec![
                make_string_attr("attr_int", "seed"),
                make_string_attr("attr_float", "seed"),
                make_string_attr("attr_bool", "seed"),
            ],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        string_target_table_name,
        string_target_seed_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let string_target_req = make_trace_v1_request(
        "type-coercion-string",
        vec![make_trace_v1_span(
            "00000000000000000000000000000022",
            "0000000000000022",
            "string-target-apply",
            1_736_480_942_444_830_000,
            1_736_480_942_444_930_000,
            vec![
                make_int_attr("attr_int", 20),
                make_double_attr("attr_float", 2.5),
                make_bool_attr("attr_bool", false),
            ],
        )],
    );
    let res = send_trace_v1_req(&client, string_target_table_name, string_target_req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    validate_data(
        "otlp_traces_v1_type_coercion_to_string_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_bool\", \"span_attributes.attr_float\", \"span_attributes.attr_int\" from {} order by trace_id;",
            string_target_table_name
        ),
        r#"[["00000000000000000000000000000021","seed","seed","seed"],["00000000000000000000000000000022","false","2.5","20"]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_type_coercion_to_string_schema",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_coercion_to_string' and column_name in ('span_attributes.attr_bool', 'span_attributes.attr_float', 'span_attributes.attr_int') order by column_name;",
        r#"[["span_attributes.attr_bool","string","FIELD"],["span_attributes.attr_float","string","FIELD"],["span_attributes.attr_int","string","FIELD"]]"#,
    )
    .await;

    let intra_batch_prefer_non_string_table_name = "trace_type_prefer_non_string";
    let intra_batch_prefer_non_string_req = make_trace_v1_request(
        "type-prefer-non-string",
        vec![
            make_trace_v1_span(
                "00000000000000000000000000000031",
                "0000000000000031",
                "prefer-non-string-seed",
                1_736_480_942_444_940_000,
                1_736_480_942_445_040_000,
                vec![
                    make_string_attr("attr_int", "10"),
                    make_string_attr("attr_float", "1.5"),
                    make_string_attr("attr_bool", "true"),
                ],
            ),
            make_trace_v1_span(
                "00000000000000000000000000000032",
                "0000000000000032",
                "prefer-non-string-apply",
                1_736_480_942_445_050_000,
                1_736_480_942_445_150_000,
                vec![
                    make_int_attr("attr_int", 20),
                    make_double_attr("attr_float", 2.5),
                    make_bool_attr("attr_bool", false),
                ],
            ),
        ],
    );
    let res = send_trace_v1_req(
        &client,
        intra_batch_prefer_non_string_table_name,
        intra_batch_prefer_non_string_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    validate_data(
        "otlp_traces_v1_prefer_non_string_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_bool\", \"span_attributes.attr_float\", \"span_attributes.attr_int\" from {} order by trace_id;",
            intra_batch_prefer_non_string_table_name
        ),
        r#"[["00000000000000000000000000000031",true,1.5,10],["00000000000000000000000000000032",false,2.5,20]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_prefer_non_string_schema",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_prefer_non_string' and column_name in ('span_attributes.attr_bool', 'span_attributes.attr_float', 'span_attributes.attr_int') order by column_name;",
        r#"[["span_attributes.attr_bool","boolean","FIELD"],["span_attributes.attr_float","double","FIELD"],["span_attributes.attr_int","bigint","FIELD"]]"#,
    )
    .await;

    let existing_float_table_name = "trace_type_existing_float_prefers_schema";
    let existing_float_seed_req = make_trace_v1_request(
        "type-existing-float",
        vec![make_trace_v1_span(
            "00000000000000000000000000000041",
            "0000000000000041",
            "existing-float-seed",
            1_736_480_942_445_160_000,
            1_736_480_942_445_260_000,
            vec![make_double_attr("attr_num", 1.25)],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_float_table_name,
        existing_float_seed_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let existing_float_req = make_trace_v1_request(
        "type-existing-float",
        vec![
            make_trace_v1_span(
                "00000000000000000000000000000042",
                "0000000000000042",
                "existing-float-int-first",
                1_736_480_942_445_270_000,
                1_736_480_942_445_370_000,
                vec![make_int_attr("attr_num", 2)],
            ),
            make_trace_v1_span(
                "00000000000000000000000000000043",
                "0000000000000043",
                "existing-float-float-later",
                1_736_480_942_445_380_000,
                1_736_480_942_445_480_000,
                vec![make_double_attr("attr_num", 3.5)],
            ),
        ],
    );
    let res = send_trace_v1_req(
        &client,
        existing_float_table_name,
        existing_float_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    validate_data(
        "otlp_traces_v1_existing_float_prefers_schema_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_num\" from {} order by trace_id;",
            existing_float_table_name
        ),
        r#"[["00000000000000000000000000000041",1.25],["00000000000000000000000000000042",2.0],["00000000000000000000000000000043",3.5]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_existing_float_prefers_schema_type",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_existing_float_prefers_schema' and column_name = 'span_attributes.attr_num';",
        r#"[["span_attributes.attr_num","double","FIELD"]]"#,
    )
    .await;

    let existing_int_table_name = "trace_type_existing_int_widens_to_float";
    let existing_int_seed_req = make_trace_v1_request(
        "type-existing-int",
        vec![make_trace_v1_span(
            "00000000000000000000000000000051",
            "0000000000000051",
            "existing-int-seed",
            1_736_480_942_445_490_000,
            1_736_480_942_445_590_000,
            vec![make_int_attr("attr_num", 1)],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_int_table_name,
        existing_int_seed_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let existing_int_req = make_trace_v1_request(
        "type-existing-int",
        vec![
            make_trace_v1_span(
                "00000000000000000000000000000052",
                "0000000000000052",
                "existing-int-upcast-int",
                1_736_480_942_445_600_000,
                1_736_480_942_445_700_000,
                vec![make_int_attr("attr_num", 2)],
            ),
            make_trace_v1_span(
                "00000000000000000000000000000053",
                "0000000000000053",
                "existing-int-upcast-float",
                1_736_480_942_445_710_000,
                1_736_480_942_445_810_000,
                vec![make_double_attr("attr_num", 3.5)],
            ),
        ],
    );
    let res = send_trace_v1_req(&client, existing_int_table_name, existing_int_req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    // Trace schema reconciliation widens the existing column through a direct
    // alter-table call, so it must identify itself as an OTLP auto alteration.
    wait_for_event_data(
        &client,
        "SELECT DISTINCT json_to_string(event_context) FROM greptime_private.events WHERE type = 'alter_table' AND json_get_string(procedure_trigger, 'type') = 'Submitted' AND json_get_string(event_context, 'protocol') = 'otlp' AND json_get_string(event_context, 'reason') = 'auto_alter'",
        r#"[["{\"protocol\":\"otlp\",\"reason\":\"auto_alter\"}"]]"#,
    )
    .await;

    validate_data(
        "otlp_traces_v1_existing_int_widens_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_num\" from {} order by trace_id;",
            existing_int_table_name
        ),
        r#"[["00000000000000000000000000000051",1.0],["00000000000000000000000000000052",2.0],["00000000000000000000000000000053",3.5]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_existing_int_widens_type",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_existing_int_widens_to_float' and column_name = 'span_attributes.attr_num';",
        r#"[["span_attributes.attr_num","double","FIELD"]]"#,
    )
    .await;

    let existing_int_atomic_table_name = "trace_type_existing_int_widen_atomic";
    let existing_int_atomic_seed_req = make_trace_v1_request(
        "type-existing-int-atomic",
        vec![make_trace_v1_span(
            "00000000000000000000000000000054",
            "0000000000000054",
            "existing-int-atomic-seed",
            1_736_480_942_445_720_000,
            1_736_480_942_445_820_000,
            vec![
                make_int_attr("attr_num", 1),
                make_int_attr("attr_parse", 10),
            ],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_int_atomic_table_name,
        existing_int_atomic_seed_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let existing_int_atomic_req = make_trace_v1_request(
        "type-existing-int-atomic",
        vec![make_trace_v1_span(
            "00000000000000000000000000000055",
            "0000000000000055",
            "existing-int-atomic-invalid",
            1_736_480_942_445_830_000,
            1_736_480_942_445_930_000,
            vec![
                make_double_attr("attr_num", 3.5),
                make_string_attr("attr_parse", "not_a_number"),
            ],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_int_atomic_table_name,
        existing_int_atomic_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    let body = res.bytes().await;
    let status = GoogleRpcStatus::decode(body.as_ref()).unwrap();
    assert!(
        status
            .message
            .contains("Accepted 0 spans, rejected 1 spans"),
        "unexpected error body: {status:?}"
    );

    validate_data(
        "otlp_traces_v1_existing_int_widen_atomic_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_num\", \"span_attributes.attr_parse\" from {} order by trace_id;",
            existing_int_atomic_table_name
        ),
        r#"[["00000000000000000000000000000054",1,10]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_existing_int_widen_atomic_types",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_existing_int_widen_atomic' and column_name in ('span_attributes.attr_num', 'span_attributes.attr_parse') order by column_name;",
        r#"[["span_attributes.attr_num","bigint","FIELD"],["span_attributes.attr_parse","bigint","FIELD"]]"#,
    )
    .await;

    let existing_int_float_only_table_name = "trace_type_existing_int_float_only";
    let existing_int_float_only_seed_req = make_trace_v1_request(
        "type-existing-int-float-only",
        vec![make_trace_v1_span(
            "00000000000000000000000000000061",
            "0000000000000061",
            "existing-int-float-only-seed",
            1_736_480_942_445_820_000,
            1_736_480_942_445_920_000,
            vec![make_int_attr("attr_num", 1)],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_int_float_only_table_name,
        existing_int_float_only_seed_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let existing_int_float_only_req = make_trace_v1_request(
        "type-existing-int-float-only",
        vec![make_trace_v1_span(
            "00000000000000000000000000000062",
            "0000000000000062",
            "existing-int-float-only-apply",
            1_736_480_942_445_930_000,
            1_736_480_942_446_030_000,
            vec![make_double_attr("attr_num", 2.5)],
        )],
    );
    let res = send_trace_v1_req(
        &client,
        existing_int_float_only_table_name,
        existing_int_float_only_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    validate_data(
        "otlp_traces_v1_existing_int_float_only_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_num\" from {} order by trace_id;",
            existing_int_float_only_table_name
        ),
        r#"[["00000000000000000000000000000061",1.0],["00000000000000000000000000000062",2.5]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_existing_int_float_only_type",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_existing_int_float_only' and column_name = 'span_attributes.attr_num';",
        r#"[["span_attributes.attr_num","double","FIELD"]]"#,
    )
    .await;

    validate_data(
        "otlp_traces_v1_type_coercion_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_int\", \"span_attributes.attr_bool\" from {} order by trace_id;",
            coercion_table_name
        ),
        r#"[["00000000000000000000000000000001",10,true],["00000000000000000000000000000002",20,false]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_type_coercion_float_sum",
        &client,
        &format!(
            "select sum(\"span_attributes.attr_float\") from {};",
            coercion_table_name
        ),
        r#"[[3.5]]"#,
    )
    .await;
    validate_data(
        "otlp_traces_v1_type_coercion_schema",
        &client,
        "select column_name, lower(data_type), semantic_type from information_schema.columns where table_name = 'trace_type_coercion' and column_name in ('span_attributes.attr_bool', 'span_attributes.attr_float', 'span_attributes.attr_int') order by column_name;",
        r#"[["span_attributes.attr_bool","boolean","FIELD"],["span_attributes.attr_float","double","FIELD"],["span_attributes.attr_int","bigint","FIELD"]]"#,
    )
    .await;

    let abort_table_name = "trace_type_abort";
    let abort_seed_req = make_trace_v1_request(
        "type-abort",
        vec![make_trace_v1_span(
            "00000000000000000000000000000011",
            "0000000000000011",
            "abort-seed",
            1_736_480_942_444_800_000,
            1_736_480_942_444_900_000,
            vec![make_int_attr("attr_int", 10)],
        )],
    );
    let res = send_trace_v1_req(&client, abort_table_name, abort_seed_req, false).await;
    assert_eq!(StatusCode::OK, res.status());

    let abort_req = make_trace_v1_request(
        "type-abort",
        vec![
            make_trace_v1_span(
                "00000000000000000000000000000012",
                "0000000000000012",
                "abort-parseable",
                1_736_480_942_445_000_000,
                1_736_480_942_445_100_000,
                vec![make_string_attr("attr_int", "20")],
            ),
            make_trace_v1_span(
                "00000000000000000000000000000013",
                "0000000000000013",
                "abort-unparsable",
                1_736_480_942_445_200_000,
                1_736_480_942_445_300_000,
                vec![make_string_attr("attr_int", "not_a_number")],
            ),
        ],
    );
    let res = send_trace_v1_req(&client, abort_table_name, abort_req, false).await;
    assert_eq!(StatusCode::OK, res.status());
    let body = ExportTraceServiceResponse::decode(res.bytes().await).unwrap();
    let partial_success = body.partial_success.as_ref().unwrap();
    assert_eq!(partial_success.rejected_spans, 1);
    assert!(
        partial_success
            .error_message
            .contains("Accepted 1 spans, rejected 1 spans"),
        "unexpected partial success body: {body:?}"
    );
    assert!(
        partial_success.error_message.contains(
            "Rejected span 00000000000000000000000000000013:0000000000000013 (InvalidArguments)"
        ),
        "unexpected partial success body: {body:?}"
    );
    // The rejection must name the column, the failing value and the target type,
    // otherwise locating the bad attribute needs a collector-side capture.
    assert!(
        partial_success.error_message.contains(
            "failed to coerce trace column 'span_attributes.attr_int' in table \
             'trace_type_abort' from String(\"not_a_number\") to Int64"
        ),
        "unexpected partial success body: {body:?}"
    );

    validate_data(
        "otlp_traces_v1_type_abort_rows",
        &client,
        &format!(
            "select trace_id, \"span_attributes.attr_int\" from {} order by trace_id",
            abort_table_name
        ),
        r#"[["00000000000000000000000000000011",10],["00000000000000000000000000000012",20]]"#,
    )
    .await;

    let chunk_failure_req = make_trace_v1_request(
        "type-discard",
        vec![
            make_trace_v1_span(
                "00000000000000000000000000000021",
                "0000000000000021",
                "discard-one",
                1_736_480_942_445_400_000,
                1_736_480_942_445_500_000,
                vec![make_string_attr("attr_text", "alpha")],
            ),
            make_trace_v1_span(
                "00000000000000000000000000000022",
                "0000000000000022",
                "discard-two",
                1_736_480_942_445_600_000,
                1_736_480_942_445_700_000,
                vec![make_string_attr("attr_text", "beta")],
            ),
        ],
    );
    let res = send_trace_v1_req_with_db(
        &client,
        "nonexistent",
        "trace_chunk_discard",
        chunk_failure_req,
        false,
    )
    .await;
    assert_eq!(StatusCode::BAD_REQUEST, res.status());
    let body = GoogleRpcStatus::decode(res.bytes().await.as_ref()).unwrap();
    assert!(
        body.message.contains("Accepted 0 spans, rejected 2 spans"),
        "unexpected error body: {body:?}"
    );
    assert!(
        body.message
            .contains("Discarded 2 spans after pre-write request failure"),
        "unexpected error body: {body:?}"
    );

    guard.remove_all().await;
}

pub async fn test_otlp_logs(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_otlp_logs").await;

    let client = TestClient::new(app).await;
    let content = r#"
{"resourceLogs":[{"resource":{"attributes":[],"droppedAttributesCount":0},"scopeLogs":[{"scope":{"name":"","version":"","attributes":[{"key":"instance_num","value":{"stringValue":"10"}}],"droppedAttributesCount":0},"logRecords":[{"timeUnixNano":"1736413568497632000","observedTimeUnixNano":"0","severityNumber":9,"severityText":"Info","body":{"stringValue":"the message line one"},"attributes":[{"key":"app","value":{"stringValue":"server1"}}],"droppedAttributesCount":0,"flags":0,"traceId":"f665100a612542b69cc362fe2ae9d3bf","spanId":"e58f01c4c69f4488"}],"schemaUrl":""}],"schemaUrl":"https://opentelemetry.io/schemas/1.4.0"},{"resource":{"attributes":[],"droppedAttributesCount":0},"scopeLogs":[{"scope":{"name":"","version":"","attributes":[],"droppedAttributesCount":0},"logRecords":[{"timeUnixNano":"1736413568538897000","observedTimeUnixNano":"0","severityNumber":9,"severityText":"Info","body":{"stringValue":"the message line two"},"attributes":[{"key":"app","value":{"stringValue":"server2"}}],"droppedAttributesCount":0,"flags":0,"traceId":"f665100a612542b69cc362fe2ae9d3bf","spanId":"e58f01c4c69f4488"}],"schemaUrl":""}],"schemaUrl":"https://opentelemetry.io/schemas/1.4.0"}]}
"#;

    let req: ExportLogsServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();

    {
        // write log data
        let res = send_req(
            &client,
            vec![(
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            )],
            "/v1/otlp/v1/logs?db=public",
            body.clone(),
            false,
        )
        .await;
        assert_eq!(StatusCode::OK, res.status());
        let expected = "[[1736413568497632000,\"f665100a612542b69cc362fe2ae9d3bf\",\"e58f01c4c69f4488\",\"Info\",9,\"the message line one\",{\"app\":\"server1\"},0,\"\",\"\",{\"instance_num\":\"10\"},\"\",{},\"https://opentelemetry.io/schemas/1.4.0\"],[1736413568538897000,\"f665100a612542b69cc362fe2ae9d3bf\",\"e58f01c4c69f4488\",\"Info\",9,\"the message line two\",{\"app\":\"server2\"},0,\"\",\"\",{},\"\",{},\"https://opentelemetry.io/schemas/1.4.0\"]]";
        validate_data(
            "otlp_logs",
            &client,
            "select * from opentelemetry_logs;",
            expected,
        )
        .await;

        // The auto-created log table carries the log identity.
        validate_data(
            "otlp_logs_semantic_identity",
            &client,
            "select count(*) from information_schema.tables where table_name = 'opentelemetry_logs' \
             and create_options like '%greptime.semantic.signal_type=log%' \
             and create_options like '%greptime.semantic.source=opentelemetry%';",
            "[[1]]",
        )
        .await;

        // Write another batch after the table exists. This verifies existing-schema
        // alignment accepts the built-in JSONB columns in direct OTLP logs.
        let res = send_req(
            &client,
            vec![(
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            )],
            "/v1/otlp/v1/logs?db=public",
            body.clone(),
            false,
        )
        .await;
        assert_eq!(StatusCode::OK, res.status());
        validate_data(
            "otlp_logs_multiple_batches",
            &client,
            "select count(*) from opentelemetry_logs;",
            "[[4]]",
        )
        .await;
    }

    {
        // write log data with selector
        let res = send_req(
            &client,
            vec![
                (
                    HeaderName::from_static("content-type"),
                    HeaderValue::from_static("application/x-protobuf"),
                ),
                (
                    HeaderName::from_static("x-greptime-log-table-name"),
                    HeaderValue::from_static("cus_logs"),
                ),
                (
                    HeaderName::from_static("x-greptime-log-extract-keys"),
                    HeaderValue::from_static("resource-attr,instance_num,app,not-exist"),
                ),
            ],
            "/v1/otlp/v1/logs?db=public",
            body.clone(),
            false,
        )
        .await;
        assert_eq!(StatusCode::OK, res.status());

        let expected = "[[1736413568538897000,\"f665100a612542b69cc362fe2ae9d3bf\",\"e58f01c4c69f4488\",\"Info\",9,\"the message line two\",{\"app\":\"server2\"},0,\"\",\"\",{},\"\",{},\"https://opentelemetry.io/schemas/1.4.0\",null,\"server2\"],[1736413568497632000,\"f665100a612542b69cc362fe2ae9d3bf\",\"e58f01c4c69f4488\",\"Info\",9,\"the message line one\",{\"app\":\"server1\"},0,\"\",\"\",{\"instance_num\":\"10\"},\"\",{},\"https://opentelemetry.io/schemas/1.4.0\",\"10\",\"server1\"]]";
        validate_data(
            "otlp_logs_with_selector",
            &client,
            "select * from cus_logs;",
            expected,
        )
        .await;
    }

    {
        // test same selector with multiple value
        let content = r#"
        {"resourceLogs":[{"resource":{"attributes":[{"key":"fromwhere","value":{"stringValue":"resource"}}],"droppedAttributesCount":0},"scopeLogs":[{"scope":{"name":"","version":"","attributes":[{"key":"fromwhere","value":{"stringValue":"scope"}}],"droppedAttributesCount":0},"logRecords":[{"timeUnixNano":"1736413568497632000","observedTimeUnixNano":"0","severityNumber":9,"severityText":"Info","body":{"stringValue":"the message line one"},"attributes":[{"key":"app","value":{"stringValue":"server"}},{"key":"fromwhere","value":{"stringValue":"log_attr"}}],"droppedAttributesCount":0,"flags":0,"traceId":"f665100a612542b69cc362fe2ae9d3bf","spanId":"e58f01c4c69f4488"}],"schemaUrl":""}],"schemaUrl":"https://opentelemetry.io/schemas/1.4.0"}]}
        "#;
        let req: ExportLogsServiceRequest = serde_json::from_str(content).unwrap();
        let body = req.encode_to_vec();
        let res = send_req(
            &client,
            vec![
                (
                    HeaderName::from_static("content-type"),
                    HeaderValue::from_static("application/x-protobuf"),
                ),
                (
                    HeaderName::from_static("x-greptime-log-table-name"),
                    HeaderValue::from_static("logs2"),
                ),
                (
                    HeaderName::from_static("x-greptime-log-extract-keys"),
                    HeaderValue::from_static("fromwhere"),
                ),
            ],
            "/v1/otlp/v1/logs?db=public",
            body.clone(),
            false,
        )
        .await;
        assert_eq!(StatusCode::OK, res.status());

        let expected = "[[1736413568497632000,\"f665100a612542b69cc362fe2ae9d3bf\",\"e58f01c4c69f4488\",\"Info\",9,\"the message line one\",{\"app\":\"server\",\"fromwhere\":\"log_attr\"},0,\"\",\"\",{\"fromwhere\":\"scope\"},\"\",{\"fromwhere\":\"resource\"},\"https://opentelemetry.io/schemas/1.4.0\",\"log_attr\"]]";
        validate_data(
            "otlp_logs_with_selector_overlapping",
            &client,
            "select * from logs2;",
            expected,
        )
        .await;
    }

    {
        let existing_table_name = "otlp_logs_existing_schema";
        let res = execute_sql(
            &client,
            &format!(
                "create table {existing_table_name} (\"timestamp\" timestamp(3) time index, trace_id string, host string, body string, primary key(trace_id, host))"
            ),
        )
        .await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        let req = make_log_request(vec![make_log_record(
            "00000000000000000000000000000001",
            "0000000000000001",
            1_736_413_568_497_632_000,
            "existing schema",
            vec![make_int_attr("host", 42)],
        )]);
        let res = send_log_req(&client, existing_table_name, Some("host"), req, false).await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        validate_data(
            "otlp_logs_existing_schema_rows",
            &client,
            &format!(
                "select trace_id, host, body, \"timestamp\" from {existing_table_name} order by trace_id;"
            ),
            r#"[["00000000000000000000000000000001","42","existing schema",1736413568497]]"#,
        )
        .await;
        validate_data(
            "otlp_logs_existing_schema_semantic_types",
            &client,
            "select column_name, semantic_type from information_schema.columns where table_name = 'otlp_logs_existing_schema' and column_name in ('timestamp', 'trace_id', 'host', 'scope_name') order by column_name;",
            r#"[["host","TAG"],["scope_name","FIELD"],["timestamp","TIMESTAMP"],["trace_id","TAG"]]"#,
        )
        .await;
    }

    {
        let existing_field_table_name = "otlp_logs_existing_string_field";
        let res = execute_sql(
            &client,
            &format!(
                "create table {existing_field_table_name} (\"timestamp\" timestamp(3) time index, host string, body string)"
            ),
        )
        .await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        let req = make_log_request(vec![make_log_record(
            "00000000000000000000000000000004",
            "0000000000000004",
            1_736_413_568_497_700_000,
            "existing string field",
            vec![make_int_attr("host", 42)],
        )]);
        let res = send_log_req(&client, existing_field_table_name, Some("host"), req, false).await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        validate_data(
            "otlp_logs_existing_string_field_rows",
            &client,
            &format!(
                "select host, body, \"timestamp\" from {existing_field_table_name} order by body;"
            ),
            r#"[["42","existing string field",1736413568497]]"#,
        )
        .await;
        validate_data(
            "otlp_logs_existing_string_field_semantic_type",
            &client,
            "select column_name, semantic_type from information_schema.columns where table_name = 'otlp_logs_existing_string_field' and column_name = 'host';",
            r#"[["host","FIELD"]]"#,
        )
        .await;
    }

    {
        let missing_pk_table_name = "otlp_logs_missing_pk";
        let res = execute_sql(
            &client,
            &format!(
                "create table {missing_pk_table_name} (\"timestamp\" timestamp(9) time index, host string, body string, primary key(host))"
            ),
        )
        .await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        let req = make_log_request(vec![make_log_record(
            "00000000000000000000000000000002",
            "0000000000000002",
            1_736_413_568_498_000_000,
            "missing host",
            vec![make_string_attr("not_host", "node-a")],
        )]);
        let res = send_log_req(&client, missing_pk_table_name, None, req, false).await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        validate_data(
            "otlp_logs_missing_pk_rows",
            &client,
            &format!("select host, body from {missing_pk_table_name};"),
            r#"[[null,"missing host"]]"#,
        )
        .await;
    }

    {
        let incompatible_table_name = "otlp_logs_incompatible_schema";
        let res = execute_sql(
            &client,
            &format!(
                "create table {incompatible_table_name} (\"timestamp\" timestamp(9) time index, host bigint, primary key(host))"
            ),
        )
        .await;
        assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

        let req = make_log_request(vec![make_log_record(
            "00000000000000000000000000000003",
            "0000000000000003",
            1_736_413_568_499_000_000,
            "bad host",
            vec![make_string_attr("host", "node-a")],
        )]);
        let res = send_log_req(&client, incompatible_table_name, Some("host"), req, false).await;
        let status = res.status();
        let body = res.text().await;
        assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
        assert!(
            body.contains("failed to align log column 'host'"),
            "unexpected error body: {body}"
        );
        validate_data(
            "otlp_logs_incompatible_schema_rows",
            &client,
            &format!("select count(*) from {incompatible_table_name};"),
            "[[0]]",
        )
        .await;
    }

    guard.remove_all().await;
}

pub async fn test_loki_pb_logs(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_loki_pb_logs").await;

    let client = TestClient::new(app).await;

    // init loki request
    let req: PushRequest = PushRequest {
        streams: vec![StreamAdapter {
            labels: r#"{service="test",source="integration",wadaxi="do anything"}"#.to_string(),
            entries: vec![
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:50").unwrap()),
                    line: "this is a log message".to_string(),
                    structured_metadata: vec![
                        LabelPairAdapter {
                            name: "key1".to_string(),
                            value: "value1".to_string(),
                        },
                        LabelPairAdapter {
                            name: "key2".to_string(),
                            value: "value2".to_string(),
                        },
                    ],
                    parsed: vec![],
                },
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:51").unwrap()),
                    line: "this is a log message 2".to_string(),
                    structured_metadata: vec![LabelPairAdapter {
                        name: "key3".to_string(),
                        value: "value3".to_string(),
                    }],
                    parsed: vec![],
                },
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:52").unwrap()),
                    line: "this is a log message 2".to_string(),
                    structured_metadata: vec![],
                    parsed: vec![],
                },
            ],
            hash: rand::random(),
        }],
    };
    let encode = req.encode_to_vec();
    let body = prom_store::snappy_compress(&encode).unwrap();

    // write to loki
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("content-encoding"),
                HeaderValue::from_static("snappy"),
            ),
            (
                HeaderName::from_static("accept-encoding"),
                HeaderValue::from_static("identity"),
            ),
            (
                HeaderName::from_static(GREPTIME_LOG_TABLE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_table_name"),
            ),
        ],
        "/v1/loki/api/v1/push",
        body,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // test schema
    let expected = "[[\"loki_table_name\",\"CREATE TABLE IF NOT EXISTS \\\"loki_table_name\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(9) NOT NULL,\\n  \\\"line\\\" STRING NULL,\\n  \\\"structured_metadata\\\" JSON NULL,\\n  \\\"service\\\" STRING NULL,\\n  \\\"source\\\" STRING NULL,\\n  \\\"wadaxi\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\"),\\n  PRIMARY KEY (\\\"service\\\", \\\"source\\\", \\\"wadaxi\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true',\\n  'greptime.semantic.signal_type' = 'log',\\n  'greptime.semantic.source' = 'loki'\\n)\"]]";
    validate_data(
        "loki_pb_schema",
        &client,
        "show create table loki_table_name;",
        expected,
    )
    .await;

    // test content
    let expected = "[[1730976830000000000,\"this is a log message\",{\"key1\":\"value1\",\"key2\":\"value2\"},\"test\",\"integration\",\"do anything\"],[1730976831000000000,\"this is a log message 2\",{\"key3\":\"value3\"},\"test\",\"integration\",\"do anything\"],[1730976832000000000,\"this is a log message 2\",{},\"test\",\"integration\",\"do anything\"]]";
    validate_data(
        "loki_pb_content",
        &client,
        "select * from loki_table_name;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_loki_pb_logs_with_pipeline(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_loki_pb_logs_with_pipeline").await;

    let client = TestClient::new(app).await;

    let pipeline = r#"
processors:
  - epoch:
      field: greptime_timestamp
      resolution: ms
    "#;

    let res = client
        .post("/v1/pipelines/loki_pipe")
        .header("content-type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());

    // init loki request
    let req: PushRequest = PushRequest {
        streams: vec![StreamAdapter {
            labels: r#"{service="test",source="integration",wadaxi="do anything"}"#.to_string(),
            entries: vec![
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:50").unwrap()),
                    line: "this is a log message".to_string(),
                    structured_metadata: vec![
                        LabelPairAdapter {
                            name: "key1".to_string(),
                            value: "value1".to_string(),
                        },
                        LabelPairAdapter {
                            name: "key2".to_string(),
                            value: "value2".to_string(),
                        },
                    ],
                    parsed: vec![],
                },
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:51").unwrap()),
                    line: "this is a log message 2".to_string(),
                    structured_metadata: vec![LabelPairAdapter {
                        name: "key3".to_string(),
                        value: "value3".to_string(),
                    }],
                    parsed: vec![],
                },
                EntryAdapter {
                    timestamp: Some(Timestamp::from_str("2024-11-07T10:53:52").unwrap()),
                    line: "this is a log message 2".to_string(),
                    structured_metadata: vec![],
                    parsed: vec![],
                },
            ],
            hash: rand::random(),
        }],
    };
    let encode = req.encode_to_vec();
    let body = prom_store::snappy_compress(&encode).unwrap();

    // write to loki
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("content-encoding"),
                HeaderValue::from_static("snappy"),
            ),
            (
                HeaderName::from_static("accept-encoding"),
                HeaderValue::from_static("identity"),
            ),
            (
                HeaderName::from_static(GREPTIME_LOG_TABLE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_table_name"),
            ),
            (
                HeaderName::from_static(GREPTIME_PIPELINE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_pipe"),
            ),
        ],
        "/v1/loki/api/v1/push",
        body,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // test schema
    // CREATE TABLE IF NOT EXISTS "loki_table_name" (
    //     "greptime_timestamp" TIMESTAMP(3) NOT NULL,
    //     "loki_label_service" STRING NULL,
    //     "loki_label_source" STRING NULL,
    //     "loki_label_wadaxi" STRING NULL,
    //     "loki_line" STRING NULL,
    //     "loki_metadata_key1" STRING NULL,
    //     "loki_metadata_key2" STRING NULL,
    //     "loki_metadata_key3" STRING NULL,
    //     TIME INDEX ("greptime_timestamp")
    //     )
    //   ENGINE=mito
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     append_mode = 'true'
    //   )
    let expected = "[[\"loki_table_name\",\"CREATE TABLE IF NOT EXISTS \\\"loki_table_name\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"loki_label_service\\\" STRING NULL,\\n  \\\"loki_label_source\\\" STRING NULL,\\n  \\\"loki_label_wadaxi\\\" STRING NULL,\\n  \\\"loki_line\\\" STRING NULL,\\n  \\\"loki_metadata_key1\\\" STRING NULL,\\n  \\\"loki_metadata_key2\\\" STRING NULL,\\n  \\\"loki_metadata_key3\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true',\\n  'greptime.semantic.signal_type' = 'log',\\n  'greptime.semantic.source' = 'loki'\\n)\"]]";
    validate_data(
        "loki_pb_schema",
        &client,
        "show create table loki_table_name;",
        expected,
    )
    .await;

    // test content
    let expected = "[[1730976830000,\"test\",\"integration\",\"do anything\",\"this is a log message\",\"value1\",\"value2\",null],[1730976831000,\"test\",\"integration\",\"do anything\",\"this is a log message 2\",null,null,\"value3\"],[1730976832000,\"test\",\"integration\",\"do anything\",\"this is a log message 2\",null,null,null]]";
    validate_data(
        "loki_pb_content",
        &client,
        "select * from loki_table_name;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_loki_json_logs(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_loki_json_logs").await;

    let client = TestClient::new(app).await;

    let body = r#"
{
  "streams": [
    {
      "stream": {
        "source": "test",
        "sender": "integration"
      },
      "values": [
          [ "1735901380059465984", "this is line one", {"key1":"value1","key2":"value2"}],
          [ "1735901398478897920", "this is line two", {"key3":"value3"}],
          [ "1735901398478897921", "this is line two updated"]
      ]
    }
  ]
}
    "#;

    let body = body.as_bytes().to_vec();

    // write plain to loki
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/json"),
            ),
            (
                HeaderName::from_static(GREPTIME_LOG_TABLE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_table_name"),
            ),
        ],
        "/v1/loki/api/v1/push",
        body,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // test schema
    let expected = "[[\"loki_table_name\",\"CREATE TABLE IF NOT EXISTS \\\"loki_table_name\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(9) NOT NULL,\\n  \\\"line\\\" STRING NULL,\\n  \\\"structured_metadata\\\" JSON NULL,\\n  \\\"sender\\\" STRING NULL,\\n  \\\"source\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\"),\\n  PRIMARY KEY (\\\"sender\\\", \\\"source\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true',\\n  'greptime.semantic.signal_type' = 'log',\\n  'greptime.semantic.source' = 'loki'\\n)\"]]";
    validate_data(
        "loki_json_schema",
        &client,
        "show create table loki_table_name;",
        expected,
    )
    .await;

    // test content
    let expected = "[[1735901380059465984,\"this is line one\",{\"key1\":\"value1\",\"key2\":\"value2\"},\"integration\",\"test\"],[1735901398478897920,\"this is line two\",{\"key3\":\"value3\"},\"integration\",\"test\"],[1735901398478897921,\"this is line two updated\",{},\"integration\",\"test\"]]";
    validate_data(
        "loki_json_content",
        &client,
        "select * from loki_table_name;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_loki_json_logs_with_pipeline(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_loki_json_logs_with_pipeline").await;

    let client = TestClient::new(app).await;

    let pipeline = r#"
processors:
  - epoch:
      field: greptime_timestamp
      resolution: ms
    "#;

    let res = client
        .post("/v1/pipelines/loki_pipe")
        .header("content-type", "application/x-yaml")
        .body(pipeline)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());

    let body = r#"
{
  "streams": [
    {
      "stream": {
        "source": "test",
        "sender": "integration"
      },
      "values": [
          [ "1735901380059465984", "this is line one", {"key1":"value1","key2":"value2"}],
          [ "1735901398478897920", "this is line two", {"key3":"value3"}],
          [ "1735901398478897921", "this is line two updated"]
      ]
    }
  ]
}
    "#;

    let body = body.as_bytes().to_vec();

    // write plain to loki
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/json"),
            ),
            (
                HeaderName::from_static(GREPTIME_LOG_TABLE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_table_name"),
            ),
            (
                HeaderName::from_static(GREPTIME_PIPELINE_NAME_HEADER_NAME),
                HeaderValue::from_static("loki_pipe"),
            ),
        ],
        "/v1/loki/api/v1/push",
        body,
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // test schema
    // CREATE TABLE IF NOT EXISTS "loki_table_name" (
    //     "greptime_timestamp" TIMESTAMP(3) NOT NULL,
    //     "loki_label_sender" STRING NULL,
    //     "loki_label_source" STRING NULL,
    //     "loki_line" STRING NULL,
    //     "loki_metadata_key1" STRING NULL,
    //     "loki_metadata_key2" STRING NULL,
    //     "loki_metadata_key3" STRING NULL,
    //     TIME INDEX ("greptime_timestamp")
    //     )
    //   ENGINE=mito
    //   WITH(
    //     'comment' = 'Created on insertion',
    //     append_mode = 'true'
    //   )
    let expected = "[[\"loki_table_name\",\"CREATE TABLE IF NOT EXISTS \\\"loki_table_name\\\" (\\n  \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n  \\\"loki_label_sender\\\" STRING NULL,\\n  \\\"loki_label_source\\\" STRING NULL,\\n  \\\"loki_line\\\" STRING NULL,\\n  \\\"loki_metadata_key1\\\" STRING NULL,\\n  \\\"loki_metadata_key2\\\" STRING NULL,\\n  \\\"loki_metadata_key3\\\" STRING NULL,\\n  TIME INDEX (\\\"greptime_timestamp\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true',\\n  'greptime.semantic.signal_type' = 'log',\\n  'greptime.semantic.source' = 'loki'\\n)\"]]";
    validate_data(
        "loki_json_schema",
        &client,
        "show create table loki_table_name;",
        expected,
    )
    .await;

    // test content
    let expected = "[[1735901380059,\"integration\",\"test\",\"this is line one\",\"value1\",\"value2\",null],[1735901398478,\"integration\",\"test\",\"this is line two updated\",null,null,null],[1735901398478,\"integration\",\"test\",\"this is line two\",null,null,\"value3\"]]";
    validate_data(
        "loki_json_content",
        &client,
        "select * from loki_table_name;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_elasticsearch_logs(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_elasticsearch_logs").await;

    let client = TestClient::new(app).await;

    let body = r#"
        {"create":{"_index":"test","_id":"1"}}
        {"foo":"foo_value1", "bar":"value1"}
        {"create":{"_index":"test","_id":"2"}}
        {"foo":"foo_value2","bar":"value2"}
    "#;

    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        )],
        "/v1/elasticsearch/_bulk",
        body.as_bytes().to_vec(),
        false,
    )
    .await;

    assert_eq!(StatusCode::OK, res.status());

    let expected = "[[\"foo_value1\",\"value1\"],[\"foo_value2\",\"value2\"]]";

    validate_data(
        "test_elasticsearch_logs",
        &client,
        "select foo, bar from test;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_elasticsearch_logs_with_index(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_elasticsearch_logs_with_index").await;

    let client = TestClient::new(app).await;

    // It will write to test_index1 and test_index2(specified in the path).
    let body = r#"
        {"create":{"_index":"test_index1","_id":"1"}}
        {"foo":"foo_value1", "bar":"value1"}
        {"create":{"_id":"2"}}
        {"foo":"foo_value2","bar":"value2"}
    "#;

    let res = send_req(
        &client,
        vec![(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        )],
        "/v1/elasticsearch/test_index2/_bulk",
        body.as_bytes().to_vec(),
        false,
    )
    .await;

    assert_eq!(StatusCode::OK, res.status());

    // test content of test_index1
    let expected = "[[\"foo_value1\",\"value1\"]]";
    validate_data(
        "test_elasticsearch_logs_with_index",
        &client,
        "select foo, bar from test_index1;",
        expected,
    )
    .await;

    // test content of test_index2
    let expected = "[[\"foo_value2\",\"value2\"]]";
    validate_data(
        "test_elasticsearch_logs_with_index_2",
        &client,
        "select foo, bar from test_index2;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

pub async fn test_log_query(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "test_log_query").await;

    let client = TestClient::new(app).await;

    // prepare data with SQL API
    let res = client
        .get("/v1/sql?sql=create table logs (`ts` timestamp time index, `message` string);")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    let res = client
        .post(
            "/v1/sql?sql=insert into logs values ('2024-11-06 23:59:59', 'before-date-end'), ('2024-11-07 10:53:50', 'before-explicit-end'), ('2024-11-07 10:53:51', 'at-explicit-end'), ('2024-11-07 10:53:52', 'after-explicit-end');",
        )
        .header("Content-Type", "application/x-www-form-urlencoded")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

    // test log query
    let mut log_query = LogQuery {
        table: TableName {
            catalog_name: "greptime".to_string(),
            schema_name: "public".to_string(),
            table_name: "logs".to_string(),
        },
        time_filter: TimeFilter {
            start: Some("2024-11-07".to_string()),
            end: Some("2024-11-07T10:53:51Z".to_string()),
            span: None,
        },
        limit: Limit {
            skip: None,
            fetch: Some(3),
        },
        columns: vec!["ts".to_string(), "message".to_string()],
        filters: Default::default(),
        context: Context::None,
        exprs: vec![],
    };
    let res = client
        .post("/v1/logs")
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&log_query).unwrap())
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    let resp = res.text().await;
    let v = get_rows_from_output(&resp);
    assert_eq!(v, "[[1730976830000,\"before-explicit-end\"]]");

    log_query.time_filter.start = Some("2024-11-06".to_string());
    log_query.time_filter.end = Some("2024-11-07".to_string());
    log_query.limit.fetch = Some(4);
    let res = client
        .post("/v1/logs")
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&log_query).unwrap())
        .send()
        .await;

    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    let resp = res.text().await;
    let output = serde_json::from_str::<Value>(&resp).unwrap();
    let rows = output["output"][0]["records"]["rows"].as_array().unwrap();
    let mut messages = rows
        .iter()
        .map(|row| row[1].as_str().unwrap())
        .collect::<Vec<_>>();
    messages.sort_unstable();
    assert_eq!(
        messages,
        vec![
            "after-explicit-end",
            "at-explicit-end",
            "before-date-end",
            "before-explicit-end",
        ]
    );

    let res = client
        .get("/v1/sql?sql=create table logs_limit (`ts` timestamp time index, `message` string);")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    let res = client
        .get("/v1/sql?sql=insert into logs_limit select number, 'hello' from numbers limit 1001;")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);

    let aggregate_query = LogQuery {
        table: TableName::new("greptime", "public", "logs_limit"),
        time_filter: TimeFilter {
            start: Some("1970-01-01T00:00:00Z".to_string()),
            end: Some("1970-01-01T00:00:02Z".to_string()),
            span: None,
        },
        limit: Limit {
            skip: None,
            fetch: None,
        },
        columns: vec![],
        filters: Default::default(),
        context: Context::None,
        exprs: vec![LogExpr::AggrFunc {
            expr: vec![AggFunc::new(
                "count".to_string(),
                vec![LogExpr::NamedIdent("message".to_string())],
                Some("count_result".to_string()),
            )],
            by: vec![],
        }],
    };
    let res = client
        .post("/v1/logs")
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&aggregate_query).unwrap())
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    assert_eq!(get_rows_from_output(&res.text().await), "[[1001]]");

    let mut output_query = LogQuery {
        table: TableName::new("greptime", "public", "logs_limit"),
        time_filter: TimeFilter {
            start: Some("1970-01-01T00:00:00Z".to_string()),
            end: Some("1970-01-01T00:00:02Z".to_string()),
            span: None,
        },
        limit: Limit {
            skip: None,
            fetch: None,
        },
        columns: vec!["ts".to_string(), "message".to_string()],
        filters: Default::default(),
        context: Context::None,
        exprs: vec![],
    };
    let res = client
        .post("/v1/logs")
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&output_query).unwrap())
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    assert_eq!(get_output_row_count(&res.text().await), 1000);

    output_query.limit.fetch = Some(17);
    let res = client
        .post("/v1/logs")
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&output_query).unwrap())
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "{:?}", res.text().await);
    assert_eq!(get_output_row_count(&res.text().await), 17);

    guard.remove_all().await;
}

pub async fn test_jaeger_query_api(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_jaeger_query_api").await;

    let client = TestClient::new(app).await;

    // Test empty response for `/api/services` API before writing any traces.
    let res = client.get("/v1/jaeger/api/services").send().await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": null,
        "total": 0,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    let content = r#"
    {
        "resourceSpans": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "test-jaeger-query-api"
                            }
                        }
                    ]
                },
                "scopeSpans": [
                    {
                        "scope": {
                        "name": "test-jaeger-query-api",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08ea",
                                "spanId": "008421dbbd33a3e9",
                                "name": "access-mysql",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492421000",
                                "endTimeUnixNano": "1738726754592421000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-mysql"
                                        }
                                    },
                                    {
                                        "key": "net.peer.ip",
                                        "value": {
                                        "stringValue": "1.2.3.4"
                                        }
                                    },
                                    {
                                        "key": "peer.service",
                                        "value": {
                                        "stringValue": "test-jaeger-query-api"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    },
                    {
                        "scope": {
                        "name": "test-jaeger-query-api",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08ea",
                                "spanId": "ffa03416a7b9ea48",
                                "name": "access-redis",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492422000",
                                "endTimeUnixNano": "1738726754592422000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-redis"
                                        }
                                    },
                                    {
                                        "key": "net.peer.ip",
                                        "value": {
                                        "stringValue": "1.2.3.4"
                                        }
                                    },
                                    {
                                        "key": "peer.service",
                                        "value": {
                                        "stringValue": "test-jaeger-query-api"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    }
                ],
                "schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
            }
        ]
    }
    "#;

    let req: ExportTraceServiceRequest = serde_json::from_str(content).unwrap();
    let body = req.encode_to_vec();

    // write traces data.
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static("greptime_trace_v0"),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    // Test `/api/services` API.
    let res = client.get("/v1/jaeger/api/services").send().await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            "test-jaeger-query-api"
        ],
        "total": 1,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/operations` API.
    let res = client
        .get("/v1/jaeger/api/operations?service=test-jaeger-query-api&start=1738726754492421&end=1738726754642422")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            {
                "name": "access-mysql",
                "spanKind": "server"
            },
            {
                "name": "access-redis",
                "spanKind": "server"
            }
        ],
        "total": 2,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/services/{service_name}/operations` API.
    let res = client
        .get("/v1/jaeger/api/services/test-jaeger-query-api/operations?start=1738726754492421&end=1738726754642422")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            "access-mysql",
            "access-redis"
        ],
        "total": 2,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces/{trace_id}` API.
    let res = client
        .get("/v1/jaeger/api/traces/5611dce1bc9ebed65352d99a027b08ea")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
{
  "data": [
    {
      "traceID": "5611dce1bc9ebed65352d99a027b08ea",
      "spans": [
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "ffa03416a7b9ea48",
          "operationName": "access-redis",
          "references": [],
          "startTime": 1738726754492422,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-redis"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        },
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "008421dbbd33a3e9",
          "operationName": "access-mysql",
          "references": [],
          "startTime": 1738726754492421,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-mysql"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        }
      ],
      "processes": {
        "p1": {
          "serviceName": "test-jaeger-query-api",
          "tags": []
        }
      }
    }
  ],
  "total": 0,
  "limit": 0,
  "offset": 0,
  "errors": []
}
    "#;

    let resp_txt = &res.text().await;
    let resp: Value = serde_json::from_str(resp_txt).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces` API.
    let res = client
        .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&operation=access-mysql&start=1738726754492421&end=1738726754642422&tags=%7B%22operation.type%22%3A%22access-mysql%22%7D")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
{
    "data":
    [
        {
            "traceID": "5611dce1bc9ebed65352d99a027b08ea",
            "spans":
            [
                {
                    "traceID": "5611dce1bc9ebed65352d99a027b08ea",
                    "spanID": "ffa03416a7b9ea48",
                    "operationName": "access-redis",
                    "references":
                    [],
                    "startTime": 1738726754492422,
                    "duration": 100000,
                    "tags":
                    [
                        {
                            "key": "net.peer.ip",
                            "type": "string",
                            "value": "1.2.3.4"
                        },
                        {
                            "key": "operation.type",
                            "type": "string",
                            "value": "access-redis"
                        },
                        {
                            "key": "otel.scope.name",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "otel.scope.version",
                            "type": "string",
                            "value": "1.0.0"
                        },
                        {
                            "key": "otel.status_description",
                            "type": "string",
                            "value": "success"
                        },
                        {
                            "key": "peer.service",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "span.kind",
                            "type": "string",
                            "value": "server"
                        }
                    ],
                    "logs":
                    [],
                    "processID": "p1"
                },
                {
                    "traceID": "5611dce1bc9ebed65352d99a027b08ea",
                    "spanID": "008421dbbd33a3e9",
                    "operationName": "access-mysql",
                    "references":
                    [],
                    "startTime": 1738726754492421,
                    "duration": 100000,
                    "tags":
                    [
                        {
                            "key": "net.peer.ip",
                            "type": "string",
                            "value": "1.2.3.4"
                        },
                        {
                            "key": "operation.type",
                            "type": "string",
                            "value": "access-mysql"
                        },
                        {
                            "key": "otel.scope.name",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "otel.scope.version",
                            "type": "string",
                            "value": "1.0.0"
                        },
                        {
                            "key": "otel.status_description",
                            "type": "string",
                            "value": "success"
                        },
                        {
                            "key": "peer.service",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "span.kind",
                            "type": "string",
                            "value": "server"
                        }
                    ],
                    "logs":
                    [],
                    "processID": "p1"
                }
            ],
            "processes":
            {
                "p1":
                {
                    "serviceName": "test-jaeger-query-api",
                    "tags":
                    []
                }
            }
        }
    ],
    "total": 0,
    "limit": 0,
    "offset": 0,
    "errors":
    []
}
    "#;

    let resp_txt = &res.text().await;
    let resp: Value = serde_json::from_str(resp_txt).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    guard.remove_all().await;
}

pub async fn test_jaeger_query_api_for_trace_v1(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_jaeger_query_api_v1").await;

    let client = TestClient::new(app).await;

    // Test empty response for `/api/services` API before writing any traces.
    let res = client.get("/v1/jaeger/api/services").send().await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": null,
        "total": 0,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    let content = r#"
    {
        "resourceSpans": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "test-jaeger-query-api"
                            }
                        }
                    ]
                },
                "scopeSpans": [
                    {
                        "scope": {
                        "name": "test-jaeger-query-api",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08ea",
                                "spanId": "008421dbbd33a3e9",
                                "name": "access-mysql",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492421000",
                                "endTimeUnixNano": "1738726754592421000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-mysql"
                                        }
                                    },
                                    {
                                        "key": "net.peer.ip",
                                        "value": {
                                        "stringValue": "1.2.3.4"
                                        }
                                    },
                                    {
                                        "key": "peer.service",
                                        "value": {
                                        "stringValue": "test-jaeger-query-api"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    },
                    {
                        "scope": {
                        "name": "test-jaeger-query-api",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08ea",
                                "spanId": "ffa03416a7b9ea48",
                                "name": "access-redis",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492422000",
                                "endTimeUnixNano": "1738726754592422000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-redis"
                                        }
                                    },
                                    {
                                        "key": "net.peer.ip",
                                        "value": {
                                        "stringValue": "1.2.3.4"
                                        }
                                    },
                                    {
                                        "key": "peer.service",
                                        "value": {
                                        "stringValue": "test-jaeger-query-api"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    },
                    {
                        "scope": {
                        "name": "test-jaeger-get-operations",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08ff",
                                "spanId": "ffa03416a7b9ea48",
                                "name": "access-pg",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492422000",
                                "endTimeUnixNano": "1738726754592422000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-pg"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    },
                    {
                        "scope": {
                        "name": "test-jaeger-find-traces",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08fa",
                                "spanId": "ffa03416a7b9ea49",
                                "name": "access-pg",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754492423000",
                                "endTimeUnixNano": "1738726754592423000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-pg"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    },
                    {
                        "scope": {
                        "name": "test-jaeger-grafana-ua",
                        "version": "1.0.0"
                        },
                        "spans": [
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08fb",
                                "spanId": "ffa03416a7b9ea50",
                                "name": "access-span-1",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754600000000",
                                "endTimeUnixNano": "1738726754700000000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-span-1"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            },
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08fb",
                                "spanId": "ffa03416a7b9ea51",
                                "name": "access-span-2",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754600001000",
                                "endTimeUnixNano": "1738726754700001000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-span-2"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            },
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08fb",
                                "spanId": "ffa03416a7b9ea52",
                                "name": "access-span-3",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754600002000",
                                "endTimeUnixNano": "1738726754700002000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-span-3"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            },
                            {
                                "traceId": "5611dce1bc9ebed65352d99a027b08fb",
                                "spanId": "ffa03416a7b9ea53",
                                "name": "access-span-4",
                                "kind": 2,
                                "startTimeUnixNano": "1738726754600003000",
                                "endTimeUnixNano": "1738726754700003000",
                                "attributes": [
                                    {
                                        "key": "operation.type",
                                        "value": {
                                        "stringValue": "access-span-4"
                                        }
                                    }
                                ],
                                "status": {
                                    "message": "success",
                                    "code": 0
                                }
                            }
                        ]
                    }
                ],
                "schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
            }
        ]
    }
    "#;

    let mut req: ExportTraceServiceRequest = serde_json::from_str(content).unwrap();
    // Modify timestamp fields
    let now = Utc::now().timestamp_nanos_opt().unwrap() as u64;
    for span in req.resource_spans.iter_mut() {
        for scope_span in span.scope_spans.iter_mut() {
            // Only modify the timestamp fields for the span with the name "test-jaeger-get-operations" to current time.
            if scope_span.scope.as_ref().unwrap().name == "test-jaeger-get-operations" {
                for span in scope_span.spans.iter_mut() {
                    span.start_time_unix_nano = now - 5_000_000_000; // 5 seconds ago
                    span.end_time_unix_nano = now;
                }
            }
        }
    }
    let body = req.encode_to_vec();

    let trace_table_name = "mytable";
    // write traces data.
    let res = send_req(
        &client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-log-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-hints"),
                HeaderValue::from_static("ttl=7d"),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_static(trace_table_name),
            ),
        ],
        "/v1/otlp/v1/traces",
        body.clone(),
        false,
    )
    .await;
    assert_eq!(StatusCode::OK, res.status());

    let trace_table_sql = "[[\"mytable\",\"CREATE TABLE IF NOT EXISTS \\\"mytable\\\" (\\n  \\\"timestamp\\\" TIMESTAMP(9) NOT NULL,\\n  \\\"timestamp_end\\\" TIMESTAMP(9) NULL,\\n  \\\"duration_nano\\\" BIGINT NULL,\\n  \\\"parent_span_id\\\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\\n  \\\"trace_id\\\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\\n  \\\"span_id\\\" STRING NULL,\\n  \\\"span_kind\\\" STRING NULL,\\n  \\\"span_name\\\" STRING NULL,\\n  \\\"span_status_code\\\" STRING NULL,\\n  \\\"span_status_message\\\" STRING NULL,\\n  \\\"trace_state\\\" STRING NULL,\\n  \\\"scope_name\\\" STRING NULL,\\n  \\\"scope_version\\\" STRING NULL,\\n  \\\"service_name\\\" STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),\\n  \\\"span_attributes.operation.type\\\" STRING NULL,\\n  \\\"span_attributes.net.peer.ip\\\" STRING NULL,\\n  \\\"span_attributes.peer.service\\\" STRING NULL,\\n  \\\"span_events\\\" JSON NULL,\\n  \\\"span_links\\\" JSON NULL,\\n  TIME INDEX (\\\"timestamp\\\"),\\n  PRIMARY KEY (\\\"service_name\\\")\\n)\\nPARTITION ON COLUMNS (\\\"trace_id\\\") (\\n  trace_id < '1',\\n  trace_id >= '1' AND trace_id < '2',\\n  trace_id >= '2' AND trace_id < '3',\\n  trace_id >= '3' AND trace_id < '4',\\n  trace_id >= '4' AND trace_id < '5',\\n  trace_id >= '5' AND trace_id < '6',\\n  trace_id >= '6' AND trace_id < '7',\\n  trace_id >= '7' AND trace_id < '8',\\n  trace_id >= '8' AND trace_id < '9',\\n  trace_id >= '9' AND trace_id < 'a',\\n  trace_id >= 'a' AND trace_id < 'b',\\n  trace_id >= 'b' AND trace_id < 'c',\\n  trace_id >= 'c' AND trace_id < 'd',\\n  trace_id >= 'd' AND trace_id < 'e',\\n  trace_id >= 'e' AND trace_id < 'f',\\n  trace_id >= 'f'\\n)\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'true',\\n  'greptime.semantic.entity.service.id' = 'service_name',\\n  'greptime.semantic.pipeline' = 'greptime_trace_v1',\\n  'greptime.semantic.signal_type' = 'trace',\\n  'greptime.semantic.source' = 'opentelemetry',\\n  'greptime.semantic.trace.conventions' = 'https://opentelemetry.io/schemas/1.4.0',\\n  table_data_model = 'greptime_trace_v1',\\n  ttl = '7days'\\n)\"]]";
    validate_data(
        "trace_v1_create_table",
        &client,
        "show create table mytable;",
        trace_table_sql,
    )
    .await;

    let trace_meta_table_sql = "[[\"mytable_services\",\"CREATE TABLE IF NOT EXISTS \\\"mytable_services\\\" (\\n  \\\"timestamp\\\" TIMESTAMP(9) NOT NULL,\\n  \\\"service_name\\\" STRING NULL,\\n  TIME INDEX (\\\"timestamp\\\"),\\n  PRIMARY KEY (\\\"service_name\\\")\\n)\\n\\nENGINE=mito\\nWITH(\\n  'comment' = 'Created on insertion',\\n  append_mode = 'false'\\n)\"]]";
    validate_data(
        "trace_v1_create_meta_table",
        &client,
        "show create table mytable_services;",
        trace_meta_table_sql,
    )
    .await;

    // Test `/api/services` API.
    let res = client
        .get("/v1/jaeger/api/services")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            "test-jaeger-query-api"
        ],
        "total": 1,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/operations` API.
    let res = client
        .get("/v1/jaeger/api/operations?service=test-jaeger-query-api")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            {
                "name": "access-mysql",
                "spanKind": "server"
            },
            {
                "name": "access-pg",
                "spanKind": "server"
            },
            {
                "name": "access-redis",
                "spanKind": "server"
            },
            {
                "name": "access-span-1",
                "spanKind": "server"
            },
            {
                "name": "access-span-2",
                "spanKind": "server"
            },
            {
                "name": "access-span-3",
                "spanKind": "server"
            },
            {
                "name": "access-span-4",
                "spanKind": "server"
            }
        ],
        "total": 7,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/services/{service_name}/operations` API.
    let res = client
        .get("/v1/jaeger/api/services/test-jaeger-query-api/operations?start=1738726754492421&end=1738726754642422")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
    {
        "data": [
            "access-mysql",
            "access-pg",
            "access-redis",
            "access-span-1",
            "access-span-2",
            "access-span-3",
            "access-span-4"
        ],
        "total": 7,
        "limit": 0,
        "offset": 0,
        "errors": []
    }
    "#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces/{trace_id}` API without start and end.
    let res = client
        .get("/v1/jaeger/api/traces/5611dce1bc9ebed65352d99a027b08ea")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"{
  "data": [
    {
      "traceID": "5611dce1bc9ebed65352d99a027b08ea",
      "spans": [
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "ffa03416a7b9ea48",
          "operationName": "access-redis",
          "references": [],
          "startTime": 1738726754492422,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-redis"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        },
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "008421dbbd33a3e9",
          "operationName": "access-mysql",
          "references": [],
          "startTime": 1738726754492421,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-mysql"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        }
      ],
      "processes": {
        "p1": {
          "serviceName": "test-jaeger-query-api",
          "tags": []
        }
      }
    }
  ],
  "total": 0,
  "limit": 0,
  "offset": 0,
  "errors": []
}
"#;

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces/{trace_id}` API with start and end in microseconds.
    let res = client
        .get("/v1/jaeger/api/traces/5611dce1bc9ebed65352d99a027b08ea?start=1738726754492421&end=1738726754642422")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"{
  "data": [
    {
      "traceID": "5611dce1bc9ebed65352d99a027b08ea",
      "spans": [
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "ffa03416a7b9ea48",
          "operationName": "access-redis",
          "references": [],
          "startTime": 1738726754492422,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-redis"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        },
        {
          "traceID": "5611dce1bc9ebed65352d99a027b08ea",
          "spanID": "008421dbbd33a3e9",
          "operationName": "access-mysql",
          "references": [],
          "startTime": 1738726754492421,
          "duration": 100000,
          "tags": [
            {
              "key": "net.peer.ip",
              "type": "string",
              "value": "1.2.3.4"
            },
            {
              "key": "operation.type",
              "type": "string",
              "value": "access-mysql"
            },
            {
              "key": "otel.scope.name",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "otel.scope.version",
              "type": "string",
              "value": "1.0.0"
            },
            {
              "key": "otel.status_description",
              "type": "string",
              "value": "success"
            },
            {
              "key": "peer.service",
              "type": "string",
              "value": "test-jaeger-query-api"
            },
            {
              "key": "span.kind",
              "type": "string",
              "value": "server"
            }
          ],
          "logs": [],
          "processID": "p1"
        }
      ],
      "processes": {
        "p1": {
          "serviceName": "test-jaeger-query-api",
          "tags": []
        }
      }
    }
  ],
  "total": 0,
  "limit": 0,
  "offset": 0,
  "errors": []
}
"#;

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces/{trace_id}` API for non-existent trace.
    let res = client
        .get("/v1/jaeger/api/traces/0000000000000000000000000000dead")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::NOT_FOUND, res.status());
    let expected = r#"{
  "data": null,
  "total": 0,
  "limit": 0,
  "offset": 0,
  "errors": [
    {
      "code": 404,
      "msg": "trace not found"
    }
  ]
}
"#;
    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces` API.
    let res = client
        .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&operation=access-mysql&start=1738726754492421&end=1738726754642422&tags=%7B%22operation.type%22%3A%22access-mysql%22%7D")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());
    let expected = r#"
 {
    "data": [
        {
            "traceID": "5611dce1bc9ebed65352d99a027b08ea",
            "spans": [
                {
                    "traceID": "5611dce1bc9ebed65352d99a027b08ea",
                    "spanID": "ffa03416a7b9ea48",
                    "operationName": "access-redis",
                    "references": [],
                    "startTime": 1738726754492422,
                    "duration": 100000,
                    "tags": [
                        {
                            "key": "net.peer.ip",
                            "type": "string",
                            "value": "1.2.3.4"
                        },
                        {
                            "key": "operation.type",
                            "type": "string",
                            "value": "access-redis"
                        },
                        {
                            "key": "otel.scope.name",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "otel.scope.version",
                            "type": "string",
                            "value": "1.0.0"
                        },
                        {
                            "key": "otel.status_description",
                            "type": "string",
                            "value": "success"
                        },
                        {
                            "key": "peer.service",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "span.kind",
                            "type": "string",
                            "value": "server"
                        }
                    ],
                    "logs": [],
                    "processID": "p1"
                },
                {
                    "traceID": "5611dce1bc9ebed65352d99a027b08ea",
                    "spanID": "008421dbbd33a3e9",
                    "operationName": "access-mysql",
                    "references": [],
                    "startTime": 1738726754492421,
                    "duration": 100000,
                    "tags": [
                        {
                            "key": "net.peer.ip",
                            "type": "string",
                            "value": "1.2.3.4"
                        },
                        {
                            "key": "operation.type",
                            "type": "string",
                            "value": "access-mysql"
                        },
                        {
                            "key": "otel.scope.name",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "otel.scope.version",
                            "type": "string",
                            "value": "1.0.0"
                        },
                        {
                            "key": "otel.status_description",
                            "type": "string",
                            "value": "success"
                        },
                        {
                            "key": "peer.service",
                            "type": "string",
                            "value": "test-jaeger-query-api"
                        },
                        {
                            "key": "span.kind",
                            "type": "string",
                            "value": "server"
                        }
                    ],
                    "logs": [],
                    "processID": "p1"
                }
            ],
            "processes": {
                "p1": {
                    "serviceName": "test-jaeger-query-api",
                    "tags": []
                }
            }
        }
    ],
    "total": 0,
    "limit": 0,
    "offset": 0,
    "errors": []
}
    "#;

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces` API with tags.
    // 1. first query without tags, get 2 results
    let res = client
            .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&start=1738726754492422&end=1738726754592422")
            .header("x-greptime-trace-table-name", trace_table_name)
            .send()
            .await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"
{"data":[{"processes":{"p1":{"serviceName":"test-jaeger-query-api","tags":[]}},"spans":[{"duration":100000,"logs":[],"operationName":"access-redis","processID":"p1","references":[],"spanID":"ffa03416a7b9ea48","startTime":1738726754492422,"tags":[{"key":"net.peer.ip","type":"string","value":"1.2.3.4"},{"key":"operation.type","type":"string","value":"access-redis"},{"key":"otel.scope.name","type":"string","value":"test-jaeger-query-api"},{"key":"otel.scope.version","type":"string","value":"1.0.0"},{"key":"otel.status_description","type":"string","value":"success"},{"key":"peer.service","type":"string","value":"test-jaeger-query-api"},{"key":"span.kind","type":"string","value":"server"}],"traceID":"5611dce1bc9ebed65352d99a027b08ea"}],"traceID":"5611dce1bc9ebed65352d99a027b08ea"},{"processes":{"p1":{"serviceName":"test-jaeger-query-api","tags":[]}},"spans":[{"duration":100000,"logs":[],"operationName":"access-pg","processID":"p1","references":[],"spanID":"ffa03416a7b9ea49","startTime":1738726754492423,"tags":[{"key":"operation.type","type":"string","value":"access-pg"},{"key":"otel.scope.name","type":"string","value":"test-jaeger-find-traces"},{"key":"otel.scope.version","type":"string","value":"1.0.0"},{"key":"otel.status_description","type":"string","value":"success"},{"key":"span.kind","type":"string","value":"server"}],"traceID":"5611dce1bc9ebed65352d99a027b08fa"}],"traceID":"5611dce1bc9ebed65352d99a027b08fa"}],"errors":[],"limit":0,"offset":0,"total":0}
    "#;

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // 2. second query with tags, get 1 result
    let res = client
.get("/v1/jaeger/api/traces?service=test-jaeger-query-api&start=1738726754492422&end=1738726754592422&tags=%7B%22operation.type%22%3A%22access-pg%22%7D")
.header("x-greptime-trace-table-name", trace_table_name)
.send()
.await;
    assert_eq!(StatusCode::OK, res.status());

    let expected = r#"
{"data":[{"processes":{"p1":{"serviceName":"test-jaeger-query-api","tags":[]}},"spans":[{"duration":100000,"logs":[],"operationName":"access-pg","processID":"p1","references":[],"spanID":"ffa03416a7b9ea49","startTime":1738726754492423,"tags":[{"key":"operation.type","type":"string","value":"access-pg"},{"key":"otel.scope.name","type":"string","value":"test-jaeger-find-traces"},{"key":"otel.scope.version","type":"string","value":"1.0.0"},{"key":"otel.status_description","type":"string","value":"success"},{"key":"span.kind","type":"string","value":"server"}],"traceID":"5611dce1bc9ebed65352d99a027b08fa"}],"traceID":"5611dce1bc9ebed65352d99a027b08fa"}],"errors":[],"limit":0,"offset":0,"total":0}
"#;

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let expected: Value = serde_json::from_str(expected).unwrap();
    assert_eq!(resp, expected);

    // Test `/api/traces` API with Grafana User-Agent.
    // When user agent is Grafana, only return at most 3 spans per trace (earliest by timestamp).
    // Trace `5611dce1bc9ebed65352d99a027b08fb` has 4 spans, so only 3 should be returned.
    let res = client
        .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&start=1738726754600000&end=1738726754700004")
        .header("x-greptime-trace-table-name", trace_table_name)
        .header("User-Agent", "Grafana/8.0.0")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    // Verify that the trace has exactly 3 spans (limited by Grafana user agent)
    let data = resp.get("data").unwrap().as_array().unwrap();
    assert_eq!(data.len(), 1, "Expected 1 trace");
    let trace = &data[0];
    assert_eq!(
        trace.get("traceID").unwrap().as_str().unwrap(),
        "5611dce1bc9ebed65352d99a027b08fb"
    );
    let spans = trace.get("spans").unwrap().as_array().unwrap();
    assert_eq!(
        spans.len(),
        3,
        "Expected 3 spans (limited by Grafana user agent), got {}",
        spans.len()
    );
    // Verify that the 3 earliest spans are returned (by timestamp ascending)
    let span_names: Vec<&str> = spans
        .iter()
        .map(|s| s.get("operationName").unwrap().as_str().unwrap())
        .collect();
    assert!(
        span_names.contains(&"access-span-1"),
        "Expected access-span-1 in spans"
    );
    assert!(
        span_names.contains(&"access-span-2"),
        "Expected access-span-2 in spans"
    );
    assert!(
        span_names.contains(&"access-span-3"),
        "Expected access-span-3 in spans"
    );
    assert!(
        !span_names.contains(&"access-span-4"),
        "access-span-4 should NOT be in spans (4th span)"
    );

    // Test `/api/traces` API without User-Agent (default behavior).
    // All 4 spans should be returned for the trace.
    let res = client
        .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&start=1738726754600000&end=1738726754700004")
        .header("x-greptime-trace-table-name", trace_table_name)
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let data = resp.get("data").unwrap().as_array().unwrap();
    assert_eq!(data.len(), 1, "Expected 1 trace");
    let trace = &data[0];
    let spans = trace.get("spans").unwrap().as_array().unwrap();
    assert_eq!(
        spans.len(),
        4,
        "Expected 4 spans (no user agent limit), got {}",
        spans.len()
    );

    // Test `/api/traces` API with Jaeger User-Agent (should return all spans like default).
    let res = client
        .get("/v1/jaeger/api/traces?service=test-jaeger-query-api&start=1738726754600000&end=1738726754700004")
        .header("x-greptime-trace-table-name", trace_table_name)
        .header("User-Agent", "Jaeger-Query/1.0.0")
        .send()
        .await;
    assert_eq!(StatusCode::OK, res.status());

    let resp: Value = serde_json::from_str(&res.text().await).unwrap();
    let data = resp.get("data").unwrap().as_array().unwrap();
    assert_eq!(data.len(), 1, "Expected 1 trace");
    let trace = &data[0];
    let spans = trace.get("spans").unwrap().as_array().unwrap();
    assert_eq!(
        spans.len(),
        4,
        "Expected 4 spans (Jaeger user agent, no limit), got {}",
        spans.len()
    );

    guard.remove_all().await;
}

pub async fn test_influxdb_write(store_type: StorageType) {
    common_telemetry::init_default_ut_logging();
    let (app, mut guard) =
        setup_test_http_app_with_frontend(store_type, "test_influxdb_write").await;

    let client = TestClient::new(app).await;

    // Only write field cpu.
    let result = client
        .post("/v1/influxdb/write?db=public&p=greptime&u=greptime")
        .body("test_alter,host=host1 cpu=1.2 1664370459457010101")
        .send()
        .await;
    assert_eq!(result.status(), 204);
    assert!(result.text().await.is_empty());

    // Only write field mem.
    let result = client
        .post("/v1/influxdb/write?db=public&p=greptime&u=greptime")
        .body("test_alter,host=host1 mem=10240.0 1664370469457010101")
        .send()
        .await;
    assert_eq!(result.status(), 204);
    assert!(result.text().await.is_empty());

    // Write field cpu & mem.
    let result = client
        .post("/v1/influxdb/write?db=public&p=greptime&u=greptime")
        .body("test_alter,host=host1 cpu=3.2,mem=20480.0 1664370479457010101")
        .send()
        .await;
    assert_eq!(result.status(), 204);
    assert!(result.text().await.is_empty());

    let expected = r#"[["host1",1.2,1664370459457010101,null],["host1",null,1664370469457010101,10240.0],["host1",3.2,1664370479457010101,20480.0]]"#;
    validate_data(
        "test_influxdb_write",
        &client,
        "select * from test_alter order by greptime_timestamp;",
        expected,
    )
    .await;

    guard.remove_all().await;
}

async fn validate_data(test_name: &str, client: &TestClient, sql: &str, expected: &str) {
    let res = client
        .get(format!("/v1/sql?sql={sql}").as_str())
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "validate {test_name} fail");
    let resp = res.text().await;
    let v = get_rows_from_output(&resp);

    assert_eq!(
        expected, v,
        "validate {test_name} fail, expected: {expected}, actual: {v}"
    );
}

async fn wait_for_data(client: &TestClient, sql: &str, expected: &str) {
    wait_for_data_with_timeout(client, sql, expected, Duration::from_secs(10)).await;
}

async fn wait_for_event_data(client: &TestClient, sql: &str, expected: &str) {
    wait_for_data_with_timeout(client, sql, expected, Duration::from_secs(30)).await;
}

async fn wait_for_data_with_timeout(
    client: &TestClient,
    sql: &str,
    expected: &str,
    timeout: Duration,
) {
    tokio::time::timeout(timeout, async {
        let encoded_sql = encode(sql);
        loop {
            let res = client
                .get(format!("/v1/sql?sql={encoded_sql}").as_str())
                .send()
                .await;
            if res.status() != StatusCode::OK {
                tokio::time::sleep(Duration::from_millis(50)).await;
                continue;
            }
            let resp = res.text().await;
            let v = get_rows_from_output(&resp);

            if expected == v {
                break;
            }

            tokio::time::sleep(Duration::from_millis(50)).await;
        }
    })
    .await
    .unwrap();
}

async fn execute_sql(client: &TestClient, sql: &str) -> TestResponse {
    let encoded_sql = encode(sql);
    client
        .get(format!("/v1/sql?sql={encoded_sql}").as_str())
        .send()
        .await
}

async fn send_req(
    client: &TestClient,
    headers: Vec<(HeaderName, HeaderValue)>,
    path: &str,
    body: Vec<u8>,
    with_gzip: bool,
) -> TestResponse {
    let mut req = client.post(path);

    for (k, v) in headers {
        req = req.header(k, v);
    }

    let mut len = body.len();

    if with_gzip {
        let encoded = compress_vec_with_gzip(body);
        len = encoded.len();
        req = req.header("content-encoding", "gzip").body(encoded);
    } else {
        req = req.body(body);
    }

    req.header("content-length", len).send().await
}

async fn send_log_req(
    client: &TestClient,
    table_name: &str,
    extract_keys: Option<&str>,
    req: ExportLogsServiceRequest,
    with_gzip: bool,
) -> TestResponse {
    let mut headers = vec![
        (
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/x-protobuf"),
        ),
        (
            HeaderName::from_static(GREPTIME_LOG_TABLE_NAME_HEADER_NAME),
            HeaderValue::from_str(table_name).unwrap(),
        ),
    ];
    if let Some(extract_keys) = extract_keys {
        headers.push((
            HeaderName::from_static(GREPTIME_LOG_EXTRACT_KEYS_HEADER_NAME),
            HeaderValue::from_str(extract_keys).unwrap(),
        ));
    }

    send_req(
        client,
        headers,
        "/v1/otlp/v1/logs?db=public",
        req.encode_to_vec(),
        with_gzip,
    )
    .await
}

async fn send_trace_v1_req(
    client: &TestClient,
    table_name: &str,
    req: ExportTraceServiceRequest,
    with_gzip: bool,
) -> TestResponse {
    send_trace_v1_req_with_db(client, "public", table_name, req, with_gzip).await
}

async fn send_trace_v1_req_with_db(
    client: &TestClient,
    db_name: &str,
    table_name: &str,
    req: ExportTraceServiceRequest,
    with_gzip: bool,
) -> TestResponse {
    send_req(
        client,
        vec![
            (
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/x-protobuf"),
            ),
            (
                HeaderName::from_static("x-greptime-pipeline-name"),
                HeaderValue::from_static(GREPTIME_INTERNAL_TRACE_PIPELINE_V1_NAME),
            ),
            (
                HeaderName::from_static("x-greptime-trace-table-name"),
                HeaderValue::from_str(table_name).unwrap(),
            ),
            (
                GREPTIME_DB_HEADER_NAME.clone(),
                HeaderValue::from_str(db_name).unwrap(),
            ),
        ],
        "/v1/otlp/v1/traces",
        req.encode_to_vec(),
        with_gzip,
    )
    .await
}

fn make_log_request(records: Vec<Value>) -> ExportLogsServiceRequest {
    serde_json::from_value(json!({
        "resourceLogs": [{
            "resource": {
                "attributes": []
            },
            "scopeLogs": [{
                "scope": {
                    "name": "",
                    "version": "",
                    "attributes": []
                },
                "logRecords": records,
                "schemaUrl": ""
            }],
            "schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
        }]
    }))
    .unwrap()
}

fn make_log_record(
    trace_id: &str,
    span_id: &str,
    time_unix_nano: i64,
    body: &str,
    attributes: Vec<Value>,
) -> Value {
    json!({
        "timeUnixNano": time_unix_nano.to_string(),
        "observedTimeUnixNano": "0",
        "severityNumber": 9,
        "severityText": "Info",
        "body": {
            "stringValue": body
        },
        "attributes": attributes,
        "droppedAttributesCount": 0,
        "flags": 0,
        "traceId": trace_id,
        "spanId": span_id
    })
}

fn make_trace_v1_request(service_name: &str, spans: Vec<Value>) -> ExportTraceServiceRequest {
    serde_json::from_value(json!({
        "resourceSpans": [{
            "resource": {
                "attributes": [{
                    "key": "service.name",
                    "value": { "stringValue": service_name }
                }]
            },
            "scopeSpans": [{
                "scope": {
                    "name": "trace-v1-type-tests"
                },
                "spans": spans
            }],
            "schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
        }]
    }))
    .unwrap()
}

fn make_trace_v1_span(
    trace_id: &str,
    span_id: &str,
    name: &str,
    start_time_unix_nano: i64,
    end_time_unix_nano: i64,
    attributes: Vec<Value>,
) -> Value {
    json!({
        "traceId": trace_id,
        "spanId": span_id,
        "name": name,
        "kind": 2,
        "startTimeUnixNano": start_time_unix_nano.to_string(),
        "endTimeUnixNano": end_time_unix_nano.to_string(),
        "attributes": attributes,
        "status": {
            "message": "",
            "code": 0
        }
    })
}

fn make_string_attr(key: &str, value: &str) -> Value {
    json!({
        "key": key,
        "value": {
            "stringValue": value
        }
    })
}

fn make_int_attr(key: &str, value: i64) -> Value {
    json!({
        "key": key,
        "value": {
            "intValue": value.to_string()
        }
    })
}

fn make_double_attr(key: &str, value: f64) -> Value {
    json!({
        "key": key,
        "value": {
            "doubleValue": value
        }
    })
}

fn make_bool_attr(key: &str, value: bool) -> Value {
    json!({
        "key": key,
        "value": {
            "boolValue": value
        }
    })
}

fn get_rows_from_output(output: &str) -> String {
    let resp: Value = serde_json::from_str(output).unwrap();
    resp.get("output")
        .and_then(Value::as_array)
        .and_then(|v| v.first())
        .and_then(|v| v.get("records"))
        .and_then(|v| v.get("rows"))
        .unwrap()
        .to_string()
}

fn get_output_row_count(output: &str) -> usize {
    let resp: Value = serde_json::from_str(output).unwrap();
    resp.get("output")
        .and_then(Value::as_array)
        .and_then(|v| v.first())
        .and_then(|v| v.get("records"))
        .and_then(|v| v.get("rows"))
        .and_then(Value::as_array)
        .unwrap()
        .len()
}

fn compress_vec_with_gzip(data: Vec<u8>) -> Vec<u8> {
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(&data).unwrap();
    encoder.finish().unwrap()
}

pub async fn test_http_memory_limit(store_type: StorageType) {
    use tests_integration::test_util::setup_test_http_app_with_frontend_and_custom_options;

    common_telemetry::init_default_ut_logging();

    let http_opts = servers::http::HttpOptions {
        addr: format!("127.0.0.1:{}", common_test_util::ports::get_port()),
        ..Default::default()
    };

    // Create memory limiter with 2KB limit and fail-fast policy.
    // Note: MemoryManager uses 1KB granularity (PermitGranularity::Kilobyte),
    // so 2KB = 2 permits. Small/medium requests should fit, large should fail.
    let memory_limiter = ServerMemoryLimiter::new(2048, OnExhaustedPolicy::Fail);

    let (app, mut guard) = setup_test_http_app_with_frontend_and_custom_options(
        store_type,
        "test_http_memory_limit",
        None,
        Some(http_opts),
        Some(memory_limiter),
        false,
    )
    .await;

    let client = TestClient::new(app).await;

    // Create table first
    let res = client
        .get("/v1/sql?sql=CREATE TABLE test_mem_limit(host STRING, cpu DOUBLE, ts TIMESTAMP TIME INDEX)")
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK);

    // Test 1: Small POST request should succeed (uses actual memory limiting path)
    // Note: GET requests have no body, so they bypass body memory limiting entirely.
    let small_insert = "INSERT INTO test_mem_limit VALUES ('host1', 1.0, 0)";
    let res = client
        .post("/v1/sql")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .body(format!("sql={}", small_insert))
        .send()
        .await;
    assert_eq!(res.status(), StatusCode::OK, "Small POST should succeed");

    // Test 1B: Medium request in the 500-1024 byte range should also succeed
    // (due to 1KB granularity alignment)
    let medium_values: Vec<String> = (0..8)
        .map(|i| format!("('host{}', {}.5, {})", i, i, i * 1000))
        .collect();
    let medium_insert = format!(
        "INSERT INTO test_mem_limit VALUES {}",
        medium_values.join(", ")
    );

    let res = client
        .post("/v1/sql")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .body(format!("sql={}", medium_insert))
        .send()
        .await;
    assert_eq!(
        res.status(),
        StatusCode::OK,
        "Medium request (~700 bytes) should succeed within aligned 1KB limit"
    );

    // Test 2: Large write request should be rejected (exceeds 2KB limit)
    // Generate a large INSERT with many rows and long strings to definitely exceed 2KB
    let large_values: Vec<String> = (0..100)
        .map(|i| {
            format!(
                "('this_is_a_very_long_hostname_string_to_increase_body_size_row_{}', {}.5, {})",
                i,
                i,
                i * 1000
            )
        })
        .collect();
    let large_insert = format!(
        "INSERT INTO test_mem_limit VALUES {}",
        large_values.join(", ")
    );

    let res = client
        .post("/v1/sql")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .body(format!("sql={}", large_insert))
        .send()
        .await;

    assert_eq!(
        res.status(),
        StatusCode::TOO_MANY_REQUESTS,
        "Large write should be rejected with 429"
    );

    let error_body = res.text().await;
    assert!(
        error_body.contains("Request body memory limit exceeded"),
        "Error message should be 'Request body memory limit exceeded', got: {}",
        error_body
    );

    // Test 3A: Small InfluxDB write should succeed
    let small_influx = "test_influx,host=host1 cpu=1.5 1000000000";
    let res = client
        .post("/v1/influxdb/write?db=public")
        .body(small_influx)
        .send()
        .await;
    assert_eq!(
        res.status(),
        StatusCode::NO_CONTENT,
        "Small InfluxDB write should succeed"
    );

    // Test 3B: Large InfluxDB write should be rejected
    let large_influx_body = (0..100)
        .map(|i| {
            format!(
                "test_influx,host=host{} cpu={}.5 {}",
                i,
                i,
                (i as i64) * 1000000000
            )
        })
        .collect::<Vec<_>>()
        .join("\n");

    let res = client
        .post("/v1/influxdb/write?db=public")
        .body(large_influx_body)
        .send()
        .await;

    assert_eq!(
        res.status(),
        StatusCode::TOO_MANY_REQUESTS,
        "Large InfluxDB write should be rejected"
    );

    guard.remove_all().await;
}
