mod common;

use common::mock_client;
use ticksupply::resources::export_schemas::{
    DataType, ExchangeExtractor, MetaExtraction, MetaValue, SchemaColumn, SchemaContent,
    StreamCategory, TimestampFormat,
};
use wiremock::matchers::{body_partial_json, header, method, path};
use wiremock::{Mock, ResponseTemplate};

fn list_item_json(id: &str, name: &str) -> serde_json::Value {
    serde_json::json!({
        "id": id,
        "name": name,
        "stream_category": "trade",
        "is_built_in": false,
        "created_at": "2024-01-01T00:00:00Z",
        "version": 1,
        "has_draft": false,
    })
}

fn detail_json(id: &str, name: &str, version: i32, has_draft: bool) -> serde_json::Value {
    serde_json::json!({
        "id": id,
        "name": name,
        "stream_category": "trade",
        "is_built_in": false,
        "created_at": "2024-01-01T00:00:00Z",
        "version": version,
        "has_draft": has_draft,
        "columns": [
            { "output_column": "ts", "meta": { "value": "collection_timestamp_ns", "format": "ns" } },
            { "output_column": "price",
              "data": { "binance": { "json": { "path": "data.p", "type": "decimal(18)" } } } }
        ],
        "unfold": null,
        "derive": null,
    })
}

fn sample_content() -> SchemaContent {
    SchemaContent::builder()
        .column(SchemaColumn::meta(
            "ts",
            MetaExtraction::new(MetaValue::CollectionTimestampNs).format(TimestampFormat::Ns),
        ))
        .column(SchemaColumn::data("price").exchange(
            "binance",
            ExchangeExtractor::json("data.p", DataType::Decimal(18)),
        ))
        .build()
}

#[tokio::test]
async fn list_returns_flat_array() {
    let (server, client) = mock_client().await;
    Mock::given(method("GET"))
        .and(path("/v1/export-schemas"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
            list_item_json("sch_abc", "trades-basic"),
            list_item_json("sch_def", "trades-extended"),
        ])))
        .mount(&server)
        .await;

    let items = client.export_schemas().list().await.unwrap();
    assert_eq!(items.len(), 2);
    assert_eq!(items[0].id, "sch_abc");
    assert_eq!(items[1].name, "trades-extended");
    assert_eq!(items[0].stream_category, StreamCategory::Trade);
}

#[tokio::test]
async fn get_returns_typed_columns() {
    let (server, client) = mock_client().await;
    Mock::given(method("GET"))
        .and(path("/v1/export-schemas/sch_abc"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(detail_json("sch_abc", "x", 1, false)),
        )
        .mount(&server)
        .await;

    let detail = client.export_schemas().get("sch_abc").await.unwrap();
    assert_eq!(detail.id, "sch_abc");
    assert_eq!(detail.columns.len(), 2);
    assert_eq!(detail.columns[0].output_column, "ts");
}

#[tokio::test]
async fn create_serializes_typed_content() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas"))
        .and(body_partial_json(serde_json::json!({
            "name": "my_trades",
            "stream_category": "trade",
            "columns": [
                { "output_column": "ts",
                  "meta": { "value": "collection_timestamp_ns", "format": "ns" } },
                { "output_column": "price",
                  "data": { "binance": { "json": { "path": "data.p", "type": "decimal(18)" } } } }
            ]
        })))
        .respond_with(ResponseTemplate::new(201).set_body_json(detail_json(
            "sch_new",
            "my_trades",
            1,
            false,
        )))
        .mount(&server)
        .await;

    let schema = client
        .export_schemas()
        .create("my_trades", StreamCategory::Trade, sample_content())
        .send()
        .await
        .unwrap();
    assert_eq!(schema.id, "sch_new");
    assert_eq!(schema.version, 1);
}

#[tokio::test]
async fn update_sends_put_with_content() {
    let (server, client) = mock_client().await;
    Mock::given(method("PUT"))
        .and(path("/v1/export-schemas/sch_abc"))
        .and(body_partial_json(serde_json::json!({
            "columns": [
                { "output_column": "ts",
                  "meta": { "value": "collection_timestamp_ns", "format": "ns" } }
            ]
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(detail_json(
            "sch_abc",
            "my_trades",
            2,
            false,
        )))
        .mount(&server)
        .await;

    let content = SchemaContent::builder()
        .column(SchemaColumn::meta(
            "ts",
            MetaExtraction::new(MetaValue::CollectionTimestampNs).format(TimestampFormat::Ns),
        ))
        .build();
    let updated = client
        .export_schemas()
        .update("sch_abc", content)
        .send()
        .await
        .unwrap();
    assert_eq!(updated.version, 2);
}

#[tokio::test]
async fn delete_roundtrip() {
    let (server, client) = mock_client().await;
    Mock::given(method("DELETE"))
        .and(path("/v1/export-schemas/sch_abc"))
        .respond_with(ResponseTemplate::new(204))
        .mount(&server)
        .await;

    client
        .export_schemas()
        .delete("sch_abc")
        .send()
        .await
        .unwrap();
}

#[tokio::test]
async fn create_draft_no_body_copies_from_published() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas/sch_abc/draft"))
        .respond_with(
            ResponseTemplate::new(201).set_body_json(detail_json("sch_abc", "x", 1, true)),
        )
        .mount(&server)
        .await;

    let draft = client
        .export_schemas()
        .create_draft("sch_abc")
        .send()
        .await
        .unwrap();
    assert!(draft.has_draft);
}

#[tokio::test]
async fn create_draft_with_content_sends_body() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas/sch_abc/draft"))
        .and(body_partial_json(serde_json::json!({
            "columns": [
                { "output_column": "ts",
                  "meta": { "value": "collection_timestamp_ns", "format": "ns" } }
            ]
        })))
        .respond_with(
            ResponseTemplate::new(201).set_body_json(detail_json("sch_abc", "x", 1, true)),
        )
        .mount(&server)
        .await;

    let content = SchemaContent::builder()
        .column(SchemaColumn::meta(
            "ts",
            MetaExtraction::new(MetaValue::CollectionTimestampNs).format(TimestampFormat::Ns),
        ))
        .build();
    let draft = client
        .export_schemas()
        .create_draft("sch_abc")
        .content(content)
        .send()
        .await
        .unwrap();
    assert!(draft.has_draft);
}

#[tokio::test]
async fn publish_draft_sends_idempotency_header() {
    let (server, client) = mock_client().await;
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas/sch_abc/publish"))
        .and(header(
            "Idempotency-Key",
            "550e8400-e29b-41d4-a716-446655440000",
        ))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(detail_json("sch_abc", "x", 2, false)),
        )
        .mount(&server)
        .await;

    let published = client
        .export_schemas()
        .publish_draft("sch_abc")
        .idempotency_key("550e8400-e29b-41d4-a716-446655440000")
        .send()
        .await
        .unwrap();
    assert_eq!(published.version, 2);
    assert!(!published.has_draft);
}

#[tokio::test]
async fn handle_drives_full_draft_lifecycle() {
    let (server, client) = mock_client().await;
    // create_draft (no body)
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas/sch_abc/draft"))
        .respond_with(
            ResponseTemplate::new(201).set_body_json(detail_json("sch_abc", "x", 1, true)),
        )
        .mount(&server)
        .await;
    // update_draft
    Mock::given(method("PUT"))
        .and(path("/v1/export-schemas/sch_abc/draft"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(detail_json("sch_abc", "x", 1, true)),
        )
        .mount(&server)
        .await;
    // publish_draft
    Mock::given(method("POST"))
        .and(path("/v1/export-schemas/sch_abc/publish"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(detail_json("sch_abc", "x", 2, false)),
        )
        .mount(&server)
        .await;

    let h = client.export_schemas().for_id("sch_abc");
    h.create_draft().send().await.unwrap();
    h.update_draft(sample_content()).send().await.unwrap();
    let published = h.publish_draft().send().await.unwrap();
    assert_eq!(published.version, 2);
}

#[tokio::test]
async fn data_type_decimal_roundtrip() {
    let (server, client) = mock_client().await;
    Mock::given(method("GET"))
        .and(path("/v1/export-schemas/sch_abc"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(detail_json("sch_abc", "x", 1, false)),
        )
        .mount(&server)
        .await;
    let detail = client.export_schemas().get("sch_abc").await.unwrap();
    let price = &detail.columns[1];
    let json = serde_json::to_value(price).unwrap();
    assert_eq!(
        json["data"]["binance"]["json"]["type"],
        serde_json::Value::String("decimal(18)".into())
    );
}

#[tokio::test]
async fn unknown_stream_category_decodes_as_other() {
    let (server, client) = mock_client().await;
    let mut item = list_item_json("sch_abc", "x");
    item["stream_category"] = serde_json::Value::String("future_category".into());
    Mock::given(method("GET"))
        .and(path("/v1/export-schemas"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([item])))
        .mount(&server)
        .await;

    let items = client.export_schemas().list().await.unwrap();
    assert_eq!(
        items[0].stream_category,
        StreamCategory::Other("future_category".into())
    );
}
