//! HTTP-mocked integration tests for the Imaige client using wiremock.

use imaige_core::{
    media_url, AnalyzeImageRequest, Attribution, GenerateImageRequest, GenerateTextRequest,
    ImaigeClient, ImaigeError, JobFilter, JobStatus,
};
use wiremock::matchers::{header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn client(base: &str) -> ImaigeClient {
    // Shrink poll interval so wait_for_job tests don't drag.
    let cfg = imaige_core::ClientConfig {
        poll_interval: std::time::Duration::from_millis(10),
        wait_timeout: std::time::Duration::from_secs(5),
        ..Default::default()
    };
    ImaigeClient::with_config(base, "imk_abcd1234_secret", cfg).unwrap()
}

#[tokio::test]
async fn generate_image_returns_handle_with_auth() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/images/generate"))
        .and(header("authorization", "Bearer imk_abcd1234_secret"))
        .respond_with(ResponseTemplate::new(202).set_body_json(serde_json::json!({
            "jobId": "job_1",
            "mode": "txt2img",
            "estimatedCostCents": 7
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let req = GenerateImageRequest {
        prompt: "hello".into(),
        model: "example-model".into(),
        negative_prompt: None,
        image_size: None,
        num_images: Some(1),
        seed: None,
        attribution: Attribution::default(),
    };
    let handle = c.generate_image(&req).await.unwrap();
    assert_eq!(handle.job_id, "job_1");
    assert_eq!(handle.mode, "txt2img");
    assert_eq!(handle.estimated_cost_cents, Some(7));
}

#[tokio::test]
async fn attribution_headers_are_sent() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/images/analyze"))
        .and(header("idempotency-key", "idem-123"))
        .and(header("x-imaige-customer-id", "cust-9"))
        .respond_with(ResponseTemplate::new(202).set_body_json(serde_json::json!({
            "jobId": "job_a", "mode": "analyze"
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let req = AnalyzeImageRequest {
        image_url: "https://x/y.png".into(),
        attribution: Attribution {
            idempotency_key: Some("idem-123".into()),
            customer_id: Some("cust-9".into()),
        },
    };
    let handle = c.analyze_image(&req).await.unwrap();
    assert_eq!(handle.job_id, "job_a");
}

#[tokio::test]
async fn wait_for_job_polls_to_completion() {
    let server = MockServer::start().await;
    // First call: processing. Use up_to_n_times so the second mock wins next.
    Mock::given(method("GET"))
        .and(path("/v1/jobs/job_x"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "jobId": "job_x", "status": "processing"
        })))
        .up_to_n_times(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/v1/jobs/job_x"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "jobId": "job_x",
            "status": "complete",
            "mode": "txt2img",
            "mediaFileIds": ["file_aaa", "file_bbb"],
            "actualCostCents": 12
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let res = c.wait_for_job("job_x").await.unwrap();
    assert_eq!(res.status, JobStatus::Complete);
    assert_eq!(res.actual_cost_cents, Some(12));
    assert_eq!(
        res.media_urls,
        vec![media_url("file_aaa"), media_url("file_bbb")]
    );
}

#[tokio::test]
async fn auth_failure_maps_to_auth_error() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/jobs/nope"))
        .respond_with(ResponseTemplate::new(401).set_body_string("invalid_api_key"))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let err = c.get_job("nope").await.unwrap_err();
    assert!(matches!(err, ImaigeError::Auth { .. }), "got {err:?}");
}

#[tokio::test]
async fn rate_limit_surfaces_retry_after_ms() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/images/generate"))
        .respond_with(
            ResponseTemplate::new(429)
                .insert_header("Retry-After", "3")
                .set_body_string("slow down"),
        )
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let req = GenerateImageRequest {
        prompt: "x".into(),
        model: "m".into(),
        negative_prompt: None,
        image_size: None,
        num_images: None,
        seed: None,
        attribution: Attribution::default(),
    };
    let err = c.generate_image(&req).await.unwrap_err();
    match err {
        ImaigeError::RateLimit { retry_after_ms, .. } => {
            assert_eq!(retry_after_ms, Some(3000));
        }
        other => panic!("expected RateLimit, got {other:?}"),
    }
}

#[tokio::test]
async fn list_jobs_passes_filter_params() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/jobs"))
        .and(query_param("customerId", "cust-9"))
        .and(query_param("status", "complete"))
        .and(query_param("limit", "25"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "jobs": [{"jobId": "j1", "status": "complete"}],
            "nextCursor": "cur_2"
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let list = c
        .list_jobs(&JobFilter {
            customer_id: Some("cust-9".into()),
            status: Some(JobStatus::Complete),
            limit: Some(25),
            ..Default::default()
        })
        .await
        .unwrap();
    assert_eq!(list.jobs.len(), 1);
    assert_eq!(list.next_cursor.as_deref(), Some("cur_2"));
}

#[tokio::test]
async fn list_models_handles_envelope_and_array() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/models"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "models": [{"id": "example-model", "name": "Example Model"}]
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let models = c.list_models().await.unwrap();
    assert_eq!(models.len(), 1);
    assert_eq!(models[0].id, "example-model");
}

#[tokio::test]
async fn generate_text_sync_extracts_text_and_usage() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/text/generate"))
        .respond_with(ResponseTemplate::new(202).set_body_json(serde_json::json!({
            "jobId": "txt_1", "mode": "text_generate"
        })))
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/v1/jobs/txt_1"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "jobId": "txt_1",
            "status": "complete",
            "result": {
                "text": "hello world",
                "model": "claude-resolved",
                "usage": {"promptTokens": 5, "completionTokens": 2, "totalTokens": 7}
            }
        })))
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let req = GenerateTextRequest {
        model: "some-model".into(),
        prompt: Some("hi".into()),
        messages: None,
        system: None,
        max_tokens: Some(32),
        temperature: None,
        response_format: None,
        attribution: Attribution::default(),
    };
    let out = c.generate_text_sync(&req).await.unwrap();
    assert_eq!(out.text, "hello world");
    assert_eq!(out.model, "claude-resolved");
    assert_eq!(out.usage.total_tokens, Some(7));
}

#[tokio::test]
async fn stream_job_parses_sse_events_to_terminal() {
    use futures_util::StreamExt;

    let server = MockServer::start().await;
    let sse_body = concat!(
        "event: progress\ndata: {\"status\":\"processing\",\"stage\":\"render\",\"progress\":0.5}\n\n",
        "event: complete\ndata: {\"status\":\"complete\",\"mediaFileIds\":[\"file_z\"]}\n\n",
    );
    Mock::given(method("GET"))
        .and(path("/v1/jobs/job_s/events"))
        .respond_with(
            ResponseTemplate::new(200)
                .insert_header("Content-Type", "text/event-stream")
                .set_body_string(sse_body),
        )
        .mount(&server)
        .await;

    let c = client(&server.uri());
    let stream = c.stream_job("job_s").await.unwrap();
    futures_util::pin_mut!(stream);

    let first = stream.next().await.unwrap().unwrap();
    assert_eq!(first.stage.as_deref(), Some("render"));
    assert_eq!(first.progress, Some(0.5));
    assert!(!first.terminal);

    let second = stream.next().await.unwrap().unwrap();
    assert_eq!(second.status, Some(JobStatus::Complete));
    assert!(second.terminal);
    assert_eq!(second.media_urls, vec![media_url("file_z")]);

    // Terminal event ends the stream.
    assert!(stream.next().await.is_none());
}
