use std::time::Duration;

use anakin::{Client, Error};
use mockito::Matcher;
use serde_json::json;

fn build_client(server_url: &str) -> Client {
    Client::builder()
        .api_key("ak-test")
        .base_url(server_url)
        .max_retries(2)
        .poll_interval(Duration::from_millis(5))
        .poll_max_interval(Duration::from_millis(20))
        .poll_timeout(Duration::from_secs(3))
        .timeout(Duration::from_secs(5))
        .build()
        .expect("build client")
}

#[tokio::test]
async fn requires_api_key() {
    std::env::remove_var("ANAKIN_API_KEY");
    let err = Client::builder().build().unwrap_err();
    let msg = err.to_string();
    assert!(msg.to_lowercase().contains("api key"), "unexpected: {msg}");
}

#[tokio::test]
async fn version_is_exposed() {
    assert_eq!(anakin::VERSION, "0.1.0");
}

#[tokio::test]
async fn countries_bundled() {
    let all = anakin::supported_countries();
    assert!(!all.is_empty());
    assert!(all.iter().any(|(c, _)| *c == "US"));
    assert!(all.len() >= 50);
}

#[tokio::test]
async fn scrape_polls_to_completion() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/url-scraper")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(json!({"job_id": "j1"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/url-scraper/j1")
        .expect_at_least(2)
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body_from_request(|_req| {
            // Mockito returns the same body for every match — to get
            // alternating responses we'd need separate mocks. Use a
            // completed-immediately response and a separate test for
            // polling iteration counts.
            br##"{"status":"completed","result":{"url":"https://example.com","markdown":"# Hi","status_code":200}}"##.to_vec()
        })
        .create_async()
        .await;

    let client = build_client(&server.url());
    let doc = client.scrape("https://example.com").await.expect("scrape ok");
    assert_eq!(doc.url.as_deref(), Some("https://example.com"));
    assert_eq!(doc.markdown.as_deref(), Some("# Hi"));
    assert_eq!(doc.status_code, Some(200));
}

#[tokio::test]
async fn scrape_running_then_completed() {
    let mut server = mockito::Server::new_async().await;
    let _post = server
        .mock("POST", "/url-scraper")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(json!({"job_id": "jp"}).to_string())
        .create_async()
        .await;
    let _running = server
        .mock("GET", "/url-scraper/jp")
        .with_status(200)
        .with_body(r#"{"status":"running"}"#)
        .expect(1)
        .create_async()
        .await;
    let _completed = server
        .mock("GET", "/url-scraper/jp")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"markdown":"ok"}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let doc = client.scrape("https://example.com").await.expect("scrape ok");
    assert_eq!(doc.markdown.as_deref(), Some("ok"));
}

#[tokio::test]
async fn scrape_failed_job() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/url-scraper")
        .with_status(200)
        .with_body(json!({"job_id": "jbad"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/url-scraper/jbad")
        .with_status(200)
        .with_body(r#"{"status":"failed","error":"target unreachable","job_id":"jbad"}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("https://example.com").await.unwrap_err();
    match err {
        Error::JobFailed { reason, job_id } => {
            assert_eq!(reason, "target unreachable");
            assert_eq!(job_id.as_deref(), Some("jbad"));
        }
        other => panic!("expected JobFailed, got {other:?}"),
    }
}

#[tokio::test]
async fn scrape_polling_timeout() {
    let mut server = mockito::Server::new_async().await;
    let _post = server
        .mock("POST", "/url-scraper")
        .with_status(200)
        .with_body(json!({"job_id": "jslow"}).to_string())
        .create_async()
        .await;
    let _poll = server
        .mock("GET", "/url-scraper/jslow")
        .with_status(200)
        .with_body(r#"{"status":"running","job_id":"jslow"}"#)
        .expect_at_least(1)
        .create_async()
        .await;

    let client = Client::builder()
        .api_key("ak-test")
        .base_url(&server.url())
        .poll_interval(Duration::from_millis(20))
        .poll_max_interval(Duration::from_millis(20))
        .poll_timeout(Duration::from_millis(80))
        .build()
        .unwrap();

    let err = client.scrape("https://example.com").await.unwrap_err();
    assert!(matches!(err, Error::JobTimeout { .. }), "got {err:?}");
}

#[tokio::test]
async fn map_polls() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/map")
        .with_status(200)
        .with_body(json!({"job_id": "m1"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/map/m1")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"url":"https://x.com","links":["https://x.com/a","https://x.com/b"],"total":2}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let r = client.map("https://x.com").await.unwrap();
    assert_eq!(r.total, Some(2));
    assert_eq!(r.links.len(), 2);
}

#[tokio::test]
async fn crawl_polls() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/crawl")
        .with_status(200)
        .with_body(json!({"job_id": "c1"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/crawl/c1")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"url":"https://x.com","pages":[{"url":"https://x.com/p","markdown":"hi"}],"total":1}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let r = client.crawl("https://x.com").await.unwrap();
    assert_eq!(r.total, Some(1));
    assert_eq!(r.pages.len(), 1);
    assert_eq!(r.pages[0].markdown.as_deref(), Some("hi"));
}

#[tokio::test]
async fn agentic_search_polls() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/agentic-search")
        .with_status(200)
        .with_body(json!({"job_id": "a1"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/agentic-search/a1")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"answer":"42","sources":[{"url":"https://wiki.com","title":"life"}]}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let r = client.agentic_search("meaning of life").await.unwrap();
    assert_eq!(r.answer.as_deref(), Some("42"));
    assert_eq!(r.sources.len(), 1);
}

#[tokio::test]
async fn wire_polls() {
    let mut server = mockito::Server::new_async().await;
    let _m1 = server
        .mock("POST", "/holocron/task")
        .with_status(200)
        .with_body(json!({"job_id": "w1"}).to_string())
        .create_async()
        .await;
    let _m2 = server
        .mock("GET", "/holocron/task/w1")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"action_id":"act-1","output":{"ok":true}}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let r = client.wire("act-1", Some(json!({"query":"test"}))).await.unwrap();
    assert_eq!(r.action_id.as_deref(), Some("act-1"));
}

#[tokio::test]
async fn search_sync() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/search")
        .match_body(Matcher::PartialJson(json!({"prompt": "rust reqwest"})))
        .with_status(200)
        .with_body(r#"{"query":"rust reqwest","results":[{"title":"docs.rs","url":"https://docs.rs","snippet":"..."}]}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let r = client.search("rust reqwest").await.unwrap();
    assert_eq!(r.query.as_deref(), Some("rust reqwest"));
    assert_eq!(r.results.len(), 1);
}

#[tokio::test]
async fn unauthorized() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/url-scraper")
        .with_status(401)
        .with_body(r#"{"error":"bad key","code":"unauthorized"}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("https://example.com").await.unwrap_err();
    assert!(matches!(err, Error::Authentication { status: 401, .. }), "got {err:?}");
}

#[tokio::test]
async fn payment_required() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/url-scraper")
        .with_status(402)
        .with_body(r#"{"error":"out of credits","code":"insufficient_credits","balance":3,"required":10}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("https://example.com").await.unwrap_err();
    match err {
        Error::InsufficientCredits { balance, required, .. } => {
            assert_eq!(balance, 3);
            assert_eq!(required, 10);
        }
        other => panic!("expected InsufficientCredits, got {other:?}"),
    }
}

#[tokio::test]
async fn bad_request() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/url-scraper")
        .with_status(400)
        .with_body(r#"{"error":"missing url","code":"validation_error"}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("").await.unwrap_err();
    assert!(matches!(err, Error::InvalidRequest { status: 400, .. }), "got {err:?}");
}

#[tokio::test]
async fn rate_limited_after_retries() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/url-scraper")
        .expect_at_least(2)
        .with_status(429)
        .with_header("retry-after", "0")
        .with_body(r#"{"error":"slow down"}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("https://example.com").await.unwrap_err();
    assert!(matches!(err, Error::RateLimit { status: 429, .. }), "got {err:?}");
}

#[tokio::test]
async fn server_error() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("POST", "/url-scraper")
        .expect_at_least(1)
        .with_status(503)
        .with_body(r#"{"error":"down"}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let err = client.scrape("https://example.com").await.unwrap_err();
    assert!(matches!(err, Error::Server { status: 503, .. }), "got {err:?}");
}

#[tokio::test]
async fn retries_5xx_then_succeeds() {
    let mut server = mockito::Server::new_async().await;
    let _bad = server
        .mock("POST", "/url-scraper")
        .with_status(502)
        .with_body(r#"{"error":"bad gateway"}"#)
        .expect(1)
        .create_async()
        .await;
    let _ok = server
        .mock("POST", "/url-scraper")
        .with_status(200)
        .with_body(json!({"job_id": "j2"}).to_string())
        .create_async()
        .await;
    let _poll = server
        .mock("GET", "/url-scraper/j2")
        .with_status(200)
        .with_body(r#"{"status":"completed","result":{"url":"https://example.com","markdown":"ok"}}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let doc = client.scrape("https://example.com").await.unwrap();
    assert_eq!(doc.markdown.as_deref(), Some("ok"));
}

#[tokio::test]
async fn sessions_list() {
    let mut server = mockito::Server::new_async().await;
    let _m = server
        .mock("GET", "/browser-sessions")
        .with_status(200)
        .with_body(r#"{"sessions":[{"id":"s1","name":"login"}]}"#)
        .create_async()
        .await;

    let client = build_client(&server.url());
    let list = client.sessions().list().await.unwrap();
    assert_eq!(list.len(), 1);
    assert_eq!(list[0].name.as_deref(), Some("login"));
}

#[tokio::test]
async fn sessions_create_and_delete() {
    let mut server = mockito::Server::new_async().await;
    let _create = server
        .mock("POST", "/browser-sessions")
        .with_status(200)
        .with_body(r#"{"id":"s2","name":"new"}"#)
        .create_async()
        .await;
    let _delete = server
        .mock("DELETE", "/browser-sessions/s2")
        .with_status(200)
        .with_body("{}")
        .create_async()
        .await;

    let client = build_client(&server.url());
    let created = client.sessions().create("new", None).await.unwrap();
    assert_eq!(created.id.as_deref(), Some("s2"));
    client.sessions().delete("s2").await.unwrap();
}
