mod common;

use common::mock_client;
use ticksupply::resources::exports::ExportStatus;
use wiremock::matchers::{body_partial_json, header, method, path};
use wiremock::{Mock, ResponseTemplate};

fn job_json(id: &str, status: &str) -> serde_json::Value {
    serde_json::json!({
        "id": id,
        "datastream_id": 1,
        "start_time": "1704067200000000000",
        "end_time": "1704153600000000000",
        "format": "csv",
        "status": status,
        "created_at": "2024-01-01T00:00:00Z"
    })
}

#[tokio::test]
async fn create_export_sends_required_fields_and_decodes_202() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "datastream_id": 1,
            "start_time": "1704067200000000000",
            "end_time": "1704153600000000000"
        })))
        .and(header("Idempotency-Key", "k1"))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    let job = client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .idempotency_key("k1")
        .send()
        .await
        .unwrap();
    assert_eq!(job.id, "exp_a");
    assert!(matches!(job.status, ExportStatus::Queued));
}

#[tokio::test]
async fn get_export_decodes() {
    let (server, client) = mock_client().await;
    Mock::given(method("GET"))
        .and(path("/v1/exports/exp_a"))
        .respond_with(ResponseTemplate::new(200).set_body_json(job_json("exp_a", "succeeded")))
        .mount(&server)
        .await;
    let job = client.exports().get("exp_a").await.unwrap();
    assert!(matches!(job.status, ExportStatus::Succeeded));
}

#[tokio::test]
async fn create_export_with_string_schema_serializes_as_string() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "datastream_id": 1,
            "schema": "normalized"
        })))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .schema("normalized")
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn create_export_with_inline_schema_serializes_as_object() {
    let (server, client) = mock_client().await;
    let inline = serde_json::json!({
        "columns": [
            {"name": "ts", "source": {"kind": "collection_timestamp_ns"}}
        ],
        "derive": {
            "binance": [{"name": "mid", "from": ["bid_px", "ask_px"], "op": "mean"}]
        }
    });
    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "datastream_id": 1,
            "schema": {
                "columns": [
                    {"name": "ts", "source": {"kind": "collection_timestamp_ns"}}
                ],
                "derive": {
                    "binance": [{"name": "mid", "from": ["bid_px", "ask_px"], "op": "mean"}]
                }
            }
        })))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .inline_schema(inline)
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn create_export_with_inline_content_serializes_typed_schema() {
    use ticksupply::resources::export_schemas::{
        DataType, ExchangeExtractor, MetaExtraction, MetaValue, SchemaColumn, SchemaContent,
    };
    let (server, client) = mock_client().await;
    let content = SchemaContent::builder()
        .column(SchemaColumn::meta(
            "ts",
            MetaExtraction::new(MetaValue::CollectionTimestampNs),
        ))
        .column(SchemaColumn::data("price").exchange(
            "binance",
            ExchangeExtractor::json("data.p", DataType::Decimal(18)),
        ))
        .build();

    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "schema": {
                "columns": [
                    { "output_column": "ts",
                      "meta": { "value": "collection_timestamp_ns" } },
                    { "output_column": "price",
                      "data": { "binance": { "json": { "path": "data.p", "type": "decimal(18)" } } } }
                ]
            }
        })))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .inline_content(content)
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn create_export_inline_schema_replaces_prior_schema() {
    let (server, client) = mock_client().await;
    let inline = serde_json::json!({
        "columns": [{"name": "ts", "source": {"kind": "collection_timestamp_ns"}}]
    });
    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "schema": {
                "columns": [{"name": "ts", "source": {"kind": "collection_timestamp_ns"}}]
            }
        })))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .schema("normalized")
        .inline_schema(inline)
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn create_export_schema_replaces_prior_inline_schema() {
    let (server, client) = mock_client().await;
    let inline = serde_json::json!({
        "columns": [{"name": "ts", "source": {"kind": "collection_timestamp_ns"}}]
    });
    Mock::given(method("POST"))
        .and(path("/v1/exports"))
        .and(body_partial_json(serde_json::json!({
            "schema": "normalized"
        })))
        .respond_with(ResponseTemplate::new(202).set_body_json(job_json("exp_a", "queued")))
        .mount(&server)
        .await;

    client
        .exports()
        .create(
            1,
            1_704_067_200_000_000_000_i64,
            1_704_153_600_000_000_000_i64,
        )
        .inline_schema(inline)
        .schema("normalized")
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn get_download_decodes_artifacts() {
    let (server, client) = mock_client().await;
    Mock::given(method("GET"))
        .and(path("/v1/exports/exp_a/download"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "artifacts": [{
                "id": "art_1",
                "filename": "part-0.parquet",
                "url": "https://s3.example/part-0.parquet?sig=...",
                "bytes": 1024
            }],
            "count": 1,
            "total_bytes": 1024
        })))
        .mount(&server)
        .await;
    let d = client.exports().get_download("exp_a").await.unwrap();
    assert_eq!(d.count, 1);
    assert_eq!(d.artifacts[0].filename, "part-0.parquet");
}
