// 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::sync::Arc;
use std::time::Duration;

use common_test_util::temp_dir::create_temp_dir;
use meta_srv::gc::GcSchedulerOptions;
use mito2::gc::GcConfig;
use servers::query_handler::sql::SqlQueryHandler;
use session::context::QueryContext;
use tests_integration::cluster::{GreptimeDbCluster, GreptimeDbClusterBuilder};
use tests_integration::test_util::{
    StorageType, get_test_store_config, setup_authenticated_grpc_database,
};

use crate::event_recorder_test_util::{assert_procedure_actor, find_eventually_string};

const TABLE_NAME: &str = "batch_gc_events";
const PROCEDURE_ACTOR: &str = "procedure_actor";
const PROCEDURE_ACTOR_PASSWORD: &str = "procedure_actor_pwd";
const MAX_ATTEMPTS: usize = 60;
const POLL_INTERVAL: Duration = Duration::from_millis(250);

#[tokio::test(flavor = "multi_thread")]
async fn test_batch_gc_event() {
    let store_type = StorageType::File;
    if !store_type.test_on() {
        return;
    }

    common_telemetry::init_default_ut_logging();
    let (store_config, _guard) = get_test_store_config(&store_type);
    let home_dir = create_temp_dir("test_batch_gc_event_data_home");
    let cluster = GreptimeDbClusterBuilder::new("test_batch_gc_event")
        .await
        .with_datanodes(1)
        .with_store_config(store_config)
        .with_shared_home_dir(Arc::new(home_dir))
        .with_metasrv_gc_config(GcSchedulerOptions {
            enable: true,
            ..Default::default()
        })
        .with_datanode_gc_config(GcConfig {
            enable: true,
            lingering_time: Some(Duration::ZERO),
            unknown_file_lingering_time: Duration::ZERO,
            ..Default::default()
        })
        .build(true)
        .await;
    let instance = cluster.fe_instance();

    execute(
        instance,
        &format!(
            "CREATE TABLE {TABLE_NAME} (\
                ts TIMESTAMP TIME INDEX, \
                val DOUBLE, \
                host STRING\
            ) WITH (append_mode = 'true')"
        ),
    )
    .await;
    for day in 1..=4 {
        execute(
            instance,
            &format!(
                "INSERT INTO {TABLE_NAME} VALUES \
                 ('2023-01-{day:02} 10:00:00', {day}.0, 'host{day}')"
            ),
        )
        .await;
        execute(instance, &format!("ADMIN FLUSH_TABLE('{TABLE_NAME}')")).await;
    }
    assert_sst_count(&cluster, 4).await;
    let mut deleted_file_ids = cluster
        .list_sst_files_from_all_datanodes()
        .await
        .into_iter()
        .map(|path| {
            path.rsplit('/')
                .next()
                .unwrap()
                .strip_suffix(".parquet")
                .unwrap()
                .to_string()
        })
        .collect::<Vec<_>>();

    execute(instance, &format!("ADMIN COMPACT_TABLE('{TABLE_NAME}')")).await;
    assert_sst_count(&cluster, 5).await;

    let table = instance
        .catalog_manager()
        .table("greptime", "public", TABLE_NAME, None)
        .await
        .unwrap()
        .unwrap();
    let table_id = table.table_info().table_id();
    let (_, route) = cluster
        .metasrv
        .table_metadata_manager()
        .table_route_manager()
        .get_physical_table_route(table_id)
        .await
        .unwrap();
    let regions = route
        .region_routes
        .iter()
        .map(|route| route.region.id)
        .collect::<Vec<_>>();
    assert_eq!(regions.len(), 1);

    let (actor_db, _actor_grpc_server) = setup_authenticated_grpc_database(
        instance.clone(),
        PROCEDURE_ACTOR,
        PROCEDURE_ACTOR_PASSWORD,
    )
    .await;
    actor_db
        .sql(format!("ADMIN GC_REGIONS({})", regions[0].as_u64()))
        .await
        .unwrap();
    assert_sst_count(&cluster, 1).await;

    let procedure_id = find_eventually_string(
        instance,
        &format!(
            "SELECT procedure_id FROM greptime_private.events WHERE type = 'batch_gc' AND actor = '{PROCEDURE_ACTOR}' AND json_get_string(procedure_trigger, 'type') = 'Submitted'"
        ),
        "procedure_id",
    )
    .await;
    assert_procedure_actor(instance, &procedure_id, Some(PROCEDURE_ACTOR)).await;

    let submitted = format!(
        r#"SELECT json_to_string(event_context) AS event_context
FROM greptime_private.events
WHERE type = 'batch_gc'
  AND procedure_id = '{procedure_id}'
  AND procedure_state = 'Running'
  AND json_get_string(procedure_trigger, 'type') = 'Submitted'"#,
    );
    assert_eq!(
        r#"{"protocol":"grpc","reason":"manual"}"#,
        find_eventually_string(instance, &submitted, "event_context").await
    );

    let succeeded = format!(
        r#"SELECT json_to_string(gc_report) AS gc_report
FROM greptime_private.events
WHERE type = 'batch_gc'
  AND procedure_id = '{procedure_id}'
  AND procedure_state = 'Done'
  AND json_path_match(procedure_trigger, '$.type == "Succeeded"')
  AND json_is_null(payload)
  AND event_context IS NULL"#,
    );
    let actual_report: serde_json::Value =
        serde_json::from_str(&find_eventually_string(instance, &succeeded, "gc_report").await)
            .unwrap();
    assert_eq!(actual_report["need_retry"], false);

    let mut actual_file_ids = actual_report["deleted_files"]
        .as_array()
        .unwrap()
        .iter()
        .map(|file_id| file_id.as_str().unwrap())
        .collect::<Vec<_>>();
    actual_file_ids.sort_unstable();
    deleted_file_ids.sort_unstable();
    assert_eq!(actual_file_ids, deleted_file_ids);
}

async fn execute(instance: &Arc<frontend::instance::Instance>, sql: &str) {
    instance
        .do_query(sql, QueryContext::arc())
        .await
        .remove(0)
        .unwrap();
}

async fn assert_sst_count(cluster: &GreptimeDbCluster, expected: usize) {
    let mut last_actual = 0;
    for _ in 0..MAX_ATTEMPTS {
        last_actual = cluster.list_sst_files_from_all_datanodes().await.len();
        if last_actual == expected {
            return;
        }
        tokio::time::sleep(POLL_INTERVAL).await;
    }

    panic!("timed out waiting for {expected} SST files, found {last_actual}");
}
