use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot;

use super::SunoClient;
use super::types::{
    ClipReaction, CreateAudioUploadRequest, CreateAudioUploadSpec, CreatePersonaRequest,
    EditPersonaRequest, FinishAudioUploadRequest, GenerateRequest, InitializeAudioClipRequest,
    PersonaListScope, PlaylistReaction, SetMetadataRequest,
};
use crate::auth::AuthState;

struct CapturedRequest {
    method: String,
    path: String,
    headers: String,
    body: String,
}

struct MockServer {
    base_url: String,
    requests: oneshot::Receiver<Vec<CapturedRequest>>,
}

impl MockServer {
    async fn json(response_body: &str) -> Self {
        Self::json_sequence(&[response_body]).await
    }

    async fn json_sequence(response_bodies: &[&str]) -> Self {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind mock server");
        let addr = listener.local_addr().expect("mock server address");
        let (tx, rx) = oneshot::channel();
        let responses: Vec<String> = response_bodies
            .iter()
            .map(|body| body.to_string())
            .collect();

        tokio::spawn(async move {
            let mut captured = Vec::with_capacity(responses.len());
            for response_body in responses {
                let (stream, _) = listener.accept().await.expect("accept request");
                captured.push(capture_request(stream, &response_body).await);
            }
            let _ = tx.send(captured);
        });

        Self {
            base_url: format!("http://{addr}"),
            requests: rx,
        }
    }

    fn client(&self) -> SunoClient {
        SunoClient::new_for_tests(
            self.base_url.clone(),
            AuthState {
                jwt: Some("test-jwt".into()),
                device_id: Some("device-1".into()),
                ..AuthState::default()
            },
        )
        .expect("test client")
    }

    async fn captured(self) -> CapturedRequest {
        let mut requests = self.captured_all().await;
        assert_eq!(requests.len(), 1);
        requests.remove(0)
    }

    async fn captured_all(self) -> Vec<CapturedRequest> {
        self.requests.await.expect("captured requests")
    }
}

async fn capture_request(mut stream: TcpStream, response_body: &str) -> CapturedRequest {
    let mut data = Vec::new();
    let mut buf = [0_u8; 1024];

    let header_end = loop {
        let n = stream.read(&mut buf).await.expect("read request");
        assert_ne!(n, 0, "connection closed before headers");
        data.extend_from_slice(&buf[..n]);
        if let Some(pos) = data.windows(4).position(|window| window == b"\r\n\r\n") {
            break pos + 4;
        }
    };

    let headers = String::from_utf8_lossy(&data[..header_end]).to_string();
    let request_line = headers.lines().next().expect("request line");
    let mut request_parts = request_line.split_whitespace();
    let method = request_parts.next().expect("method").to_string();
    let path = request_parts.next().expect("path").to_string();
    let content_length = headers
        .lines()
        .find_map(|line| line.strip_prefix("content-length: "))
        .or_else(|| {
            headers
                .lines()
                .find_map(|line| line.strip_prefix("Content-Length: "))
        })
        .and_then(|value| value.parse::<usize>().ok())
        .unwrap_or(0);

    while data.len() < header_end + content_length {
        let n = stream.read(&mut buf).await.expect("read body");
        assert_ne!(n, 0, "connection closed before body");
        data.extend_from_slice(&buf[..n]);
    }

    let body = String::from_utf8_lossy(&data[header_end..header_end + content_length]).into();
    let response = format!(
        "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
        response_body.len(),
        response_body
    );
    stream
        .write_all(response.as_bytes())
        .await
        .expect("write response");

    CapturedRequest {
        method,
        path,
        headers,
        body,
    }
}

#[tokio::test]
async fn delete_clips_posts_current_web_trash_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .delete_clips(&["clip-a".to_string(), "clip-b".to_string()])
        .await
        .expect("delete clips");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/gen/trash");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "trash": true,
            "clip_ids": ["clip-a", "clip-b"]
        })
    );
}

#[tokio::test]
async fn restore_clips_posts_current_web_trash_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .restore_clips(&["clip-a".to_string()])
        .await
        .expect("restore clips");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/gen/trash");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "trash": false,
            "clip_ids": ["clip-a"]
        })
    );
}

#[tokio::test]
async fn get_clips_batches_feed_ids_by_pairs_contract() {
    let clip_a = r#"{"id":"clip-a","title":"A","status":"complete","model_name":"chirp-v4-5","created_at":"2026-06-30T00:00:00Z"}"#;
    let clip_b = r#"{"id":"clip-b","title":"B","status":"complete","model_name":"chirp-v4-5","created_at":"2026-06-30T00:00:00Z"}"#;
    let clip_c = r#"{"id":"clip-c","title":"C","status":"complete","model_name":"chirp-v4-5","created_at":"2026-06-30T00:00:00Z"}"#;
    let first_response = format!("[{clip_a},{clip_b}]");
    let second_response = format!("[{clip_c}]");
    let server =
        MockServer::json_sequence(&[first_response.as_str(), second_response.as_str()]).await;
    let client = server.client();

    let clips = client
        .get_clips(&[
            "clip-a".to_string(),
            "clip-b".to_string(),
            "clip-c".to_string(),
        ])
        .await
        .expect("get clips");

    assert_eq!(clips.len(), 3);
    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 2);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].path, "/api/feed/?ids=clip-a,clip-b");
    assert_eq!(requests[1].method, "GET");
    assert_eq!(requests[1].path, "/api/feed/?ids=clip-c");
}

#[tokio::test]
async fn feed_posts_v3_workspace_filter_contract() {
    let server = MockServer::json(r#"{"clips":[],"next_cursor":"next","has_more":true}"#).await;
    let client = server.client();

    let response = client.feed(Some("cursor-1".into())).await.expect("feed");

    assert!(response.has_more);
    assert_eq!(response.next_cursor.as_deref(), Some("next"));
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/feed/v3");
    let body = serde_json::from_str::<serde_json::Value>(&request.body).expect("request json");
    assert_eq!(body["cursor"], "cursor-1");
    assert_eq!(body["limit"], 20);
    assert_eq!(body["filters"]["workspace"]["presence"], "True");
    assert_eq!(body["filters"]["workspace"]["workspaceId"], "default");
    assert_eq!(body["filters"]["fromStudioProject"]["presence"], "False");
    assert_eq!(body["filters"]["stem"]["presence"], "False");
    assert_eq!(body["filters"]["trashed"], "False");
}

#[tokio::test]
async fn search_posts_v3_search_text_filter_contract() {
    let server = MockServer::json(r#"{"clips":[]}"#).await;
    let client = server.client();

    client.search("summer pop").await.expect("search");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/feed/v3");
    let body = serde_json::from_str::<serde_json::Value>(&request.body).expect("request json");
    assert_eq!(body["limit"], 50);
    assert_eq!(body["filters"]["searchText"], "summer pop");
    assert_eq!(body["filters"]["workspace"]["workspaceId"], "default");
}

#[tokio::test]
async fn clip_reaction_posts_current_web_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .set_clip_reaction("clip-a", Some(ClipReaction::Dislike))
        .await
        .expect("set clip reaction");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/gen/clip-a/update_reaction_type/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "reaction": "DISLIKE",
            "recommendation_metadata": {}
        })
    );
}

#[tokio::test]
async fn set_clip_metadata_posts_current_web_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .set_metadata(
            "clip-a",
            &SetMetadataRequest {
                title: Some("Renamed".into()),
                lyrics: None,
                caption: Some("Caption".into()),
                image_url: None,
                is_audio_upload_tos_accepted: None,
                remove_image_cover: None,
                remove_video_cover: None,
            },
        )
        .await
        .expect("set metadata");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/gen/clip-a/set_metadata/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "title": "Renamed",
            "caption": "Caption"
        })
    );
}

#[tokio::test]
async fn set_clip_visibility_posts_current_web_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .set_visibility("clip-a", false)
        .await
        .expect("set visibility");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/gen/clip-a/set_visibility/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "is_public": false })
    );
}

#[tokio::test]
async fn generate_posts_current_web_contract() {
    let server = MockServer::json(
        r#"{"clips":[{"id":"clip-1","title":"Demo","status":"submitted","model_name":"chirp-v4-5","created_at":"2026-06-30T00:00:00Z"}]}"#,
    )
    .await;
    let client = server.client();

    let mut generate = GenerateRequest::new("chirp-v4-5", "custom");
    generate.set_challenge_token(Some("captcha-token".into()));
    generate.title = Some("Demo".into());
    generate.tags = Some("pop, upbeat".into());
    generate.gpt_description_prompt = Some("first line\nsecond line".into());
    generate.metadata.lyrics_model = Some("default".into());

    let clips = client.generate(&generate).await.expect("generate");

    assert_eq!(clips.len(), 1);
    assert_eq!(clips[0].id, "clip-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/generate/v2-web/");
    let body = serde_json::from_str::<serde_json::Value>(&request.body).expect("request json");
    assert_eq!(body["token"], "captcha-token");
    assert_eq!(body["generation_type"], "TEXT");
    assert_eq!(body["mv"], "chirp-v4-5");
    assert_eq!(body["prompt"], "");
    assert_eq!(body["gpt_description_prompt"], "first line\nsecond line");
    assert_eq!(body["token_provider"], 1);
    assert_eq!(body["metadata"]["create_mode"], "custom");
    assert_eq!(body["metadata"]["lyrics_model"], "default");
    assert_eq!(body["metadata"]["web_client_pathname"], "/create");
    assert!(
        body["transaction_uuid"]
            .as_str()
            .is_some_and(|id| !id.is_empty())
    );
    assert!(
        body["metadata"]["create_session_token"]
            .as_str()
            .is_some_and(|id| !id.is_empty())
    );
}

#[tokio::test]
async fn cover_posts_generate_v2_cover_contract() {
    let server = MockServer::json(
        r#"{"clips":[{"id":"cover-1","title":"Cover","status":"submitted","model_name":"chirp-fenix","created_at":"2026-06-30T00:00:00Z"}]}"#,
    )
    .await;
    let client = server.client();

    let clips = client
        .cover("clip-a", "chirp-fenix", Some("pop"))
        .await
        .expect("cover");

    assert_eq!(clips[0].id, "cover-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/generate/v2-web/");
    let body = serde_json::from_str::<serde_json::Value>(&request.body).expect("request json");
    assert_eq!(body["mv"], "chirp-fenix");
    assert_eq!(body["tags"], "pop");
    assert_eq!(body["cover_clip_id"], "clip-a");
    assert_eq!(body["metadata"]["create_mode"], "cover");
}

#[tokio::test]
async fn remaster_posts_generate_v2_remaster_contract() {
    let server = MockServer::json(
        r#"{"clips":[{"id":"remaster-1","title":"Remaster","status":"submitted","model_name":"chirp-flounder","created_at":"2026-06-30T00:00:00Z"}]}"#,
    )
    .await;
    let client = server.client();

    let clips = client
        .remaster("clip-a", "chirp-flounder")
        .await
        .expect("remaster");

    assert_eq!(clips[0].id, "remaster-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/generate/upsample");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "clip_id": "clip-a",
            "model_name": "chirp-flounder",
            "variation_category": "normal"
        })
    );
}

#[tokio::test]
async fn concat_posts_current_web_contract() {
    let server = MockServer::json(
        r#"{"id":"concat-1","title":"Concat","status":"submitted","model_name":"chirp-fenix","created_at":"2026-06-30T00:00:00Z"}"#,
    )
    .await;
    let client = server.client();

    let clip = client.concat("clip-a").await.expect("concat");

    assert_eq!(clip.id, "concat-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/generate/concat/v2/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "clip_id": "clip-a" })
    );
}

#[tokio::test]
async fn speed_adjust_posts_current_web_contract() {
    let server = MockServer::json(
        r#"{"id":"speed-1","title":"Song (0.94x)","status":"processing","model_name":"chirp-fenix","audio_url":"https://cdn.example/speed-1.mp3","created_at":"2026-06-30T00:00:00Z"}"#,
    )
    .await;
    let client = server.client();

    let clip = client
        .adjust_speed("clip-a", 0.9439, true, "Song (0.94x)")
        .await
        .expect("adjust speed");

    assert_eq!(clip.id, "speed-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/clips/adjust-speed/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "clip_id": "clip-a",
            "speed_multiplier": 0.9439,
            "keep_pitch": true,
            "title": "Song (0.94x)"
        })
    );
}

#[tokio::test]
async fn stems_posts_current_web_contract() {
    let server = MockServer::json_sequence(&[
        r#"[{"id":"clip-a","title":"Source Song","status":"complete","model_name":"chirp-fenix","created_at":"2026-06-30T00:00:00Z"}]"#,
        r#"{"clips":[{"id":"stem-1","title":"Source Song (Vocals)","status":"submitted","model_name":"chirp-stem","created_at":"2026-06-30T00:00:00Z"},{"id":"stem-2","title":"Source Song (Drums)","status":"submitted","model_name":"chirp-stem","created_at":"2026-06-30T00:00:00Z"}]}"#,
    ])
    .await;
    let client = server.client();

    let clips = client.stems("clip-a").await.expect("stems");

    assert_eq!(clips.len(), 2);
    assert_eq!(clips[0].id, "stem-1");
    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 2);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].path, "/api/feed/?ids=clip-a");
    assert_eq!(requests[1].method, "POST");
    assert_eq!(requests[1].path, "/api/generate/v2-web/");
    let body = serde_json::from_str::<serde_json::Value>(&requests[1].body).expect("request json");
    assert_eq!(body["token"], serde_json::Value::Null);
    assert_eq!(body["token_provider"], serde_json::Value::Null);
    assert_eq!(body["task"], "gen_stem");
    assert_eq!(body["mv"], "chirp-v3-0");
    assert_eq!(body["title"], "Source Song");
    assert_eq!(body["prompt"], "");
    assert_eq!(body["make_instrumental"], true);
    assert_eq!(body["continue_clip_id"], "clip-a");
    assert_eq!(body["stem_type_id"], 91);
    assert_eq!(body["stem_type_group_name"], "Twelve");
    assert_eq!(body["stem_task"], "twelve");
    assert_eq!(body["metadata"]["create_mode"], "custom");
    assert_eq!(body["metadata"]["is_remix"], true);
}

#[tokio::test]
async fn lyrics_generation_posts_and_polls_current_web_contract() {
    let server = MockServer::json_sequence(&[
        r#"{"id":"lyrics-job-1"}"#,
        r#"{"text":"[Verse]\nHello","title":"Demo","status":"complete","tags":["pop"]}"#,
    ])
    .await;
    let client = server.client();

    let result = client
        .generate_lyrics("write a pop hook")
        .await
        .expect("lyrics");

    assert_eq!(result.status, "complete");
    assert_eq!(result.tags, vec!["pop"]);
    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 2);
    assert_eq!(requests[0].method, "POST");
    assert_eq!(requests[0].path, "/api/generate/lyrics/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&requests[0].body).expect("request json"),
        serde_json::json!({ "prompt": "write a pop hook" })
    );
    assert_eq!(requests[1].method, "GET");
    assert_eq!(requests[1].path, "/api/generate/lyrics/lyrics-job-1");
    assert_eq!(requests[1].body, "");
}

#[tokio::test]
async fn aligned_lyrics_gets_current_web_contract() {
    let server = MockServer::json(
        r#"{"aligned_words":[{"word":"Hello","start_s":0.0,"end_s":0.5,"success":true,"p_align":0.99}]}"#,
    )
    .await;
    let client = server.client();

    let words = client
        .aligned_lyrics("clip-a")
        .await
        .expect("aligned lyrics");

    assert_eq!(words[0].word, "Hello");
    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(request.path, "/api/gen/clip-a/aligned_lyrics/v2/");
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn playlist_reaction_posts_current_web_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .set_playlist_reaction("playlist-1", Some(PlaylistReaction::Like))
        .await
        .expect("set playlist reaction");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(
        request.path,
        "/api/playlist_reaction/playlist-1/update_reaction_type/"
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "reaction": "LIKE" })
    );
}

#[tokio::test]
async fn list_playlists_gets_me_page_contract() {
    let server = MockServer::json(
        r#"{"playlists":[{"id":"playlist-1","name":"Road Trip"}],"numTotalResults":1,"currentPage":2}"#,
    )
    .await;
    let client = server.client();

    let response = client.list_playlists(2).await.expect("list playlists");

    assert_eq!(response.current_page, 2);
    assert_eq!(response.playlists[0].id, "playlist-1");
    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(request.path, "/api/playlist/me?page=2");
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn create_playlist_with_metadata_uses_create_set_metadata_then_detail_contract() {
    let server = MockServer::json_sequence(&[
        r#"{"id":"playlist-1","name":"Road Trip"}"#,
        "{}",
        r#"{"playlist":{"id":"playlist-1","name":"Road Trip","description":"Drive set","image_url":"https://cdn.example/cover.jpg"}}"#,
    ])
    .await;
    let client = server.client();

    let playlist = client
        .create_playlist(
            "Road Trip",
            Some("Drive set"),
            Some("https://cdn.example/cover.jpg"),
        )
        .await
        .expect("create playlist");

    assert_eq!(playlist.id, "playlist-1");
    assert_eq!(playlist.description.as_deref(), Some("Drive set"));

    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 3);
    assert_eq!(requests[0].method, "POST");
    assert_eq!(requests[0].path, "/api/playlist/create/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&requests[0].body).expect("create json"),
        serde_json::json!({ "name": "Road Trip" })
    );
    assert_eq!(requests[1].method, "POST");
    assert_eq!(requests[1].path, "/api/playlist/set_metadata");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&requests[1].body).expect("metadata json"),
        serde_json::json!({
            "playlist_id": "playlist-1",
            "description": "Drive set",
            "image_url": "https://cdn.example/cover.jpg"
        })
    );
    assert_eq!(requests[2].method, "GET");
    assert_eq!(requests[2].path, "/api/playlist/v2/playlist-1");
    assert_eq!(requests[2].body, "");
}

#[tokio::test]
async fn add_clips_to_playlist_posts_v2_tracks_add_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .add_clips_to_playlist("playlist-1", &["clip-a".to_string(), "clip-b".to_string()])
        .await
        .expect("add clips");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/playlist/v2/playlist-1/tracks/add");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "clip_ids": ["clip-a", "clip-b"] })
    );
}

#[tokio::test]
async fn remove_clips_from_playlist_posts_v2_tracks_remove_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .remove_clips_from_playlist("playlist-1", &["clip-a".to_string(), "clip-b".to_string()])
        .await
        .expect("remove clips");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/playlist/v2/playlist-1/tracks/remove");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "clip_ids": ["clip-a", "clip-b"] })
    );
}

#[tokio::test]
async fn reorder_playlist_clip_posts_positions_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .reorder_playlist_clip("playlist-1", "clip-a", 3)
        .await
        .expect("reorder clip");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(
        request.path,
        "/api/playlist/v2/playlist-1/tracks/reorder-by-index"
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "positions": [{ "clip_id": "clip-a", "index": 3 }] })
    );
}

#[tokio::test]
async fn set_playlist_visibility_patches_v2_metadata_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .set_playlist_visibility("playlist-1", false)
        .await
        .expect("set visibility");

    let request = server.captured().await;
    assert_eq!(request.method, "PATCH");
    assert_eq!(request.path, "/api/playlist/v2/playlist-1");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "metadata": { "is_public": false } })
    );
}

#[tokio::test]
async fn trash_playlist_posts_undo_false_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .trash_playlist("playlist-1")
        .await
        .expect("trash playlist");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/playlist/v2/playlist-1/trash");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "undo": false })
    );
}

#[tokio::test]
async fn restore_playlist_posts_undo_true_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .restore_playlist("playlist-1")
        .await
        .expect("restore playlist");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/playlist/v2/playlist-1/trash");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "undo": true })
    );
}

#[tokio::test]
async fn save_and_unsave_playlist_use_v2_save_contract() {
    let save_server = MockServer::json("{}").await;
    let save_client = save_server.client();

    save_client
        .save_playlist("playlist-1")
        .await
        .expect("save playlist");

    let save_request = save_server.captured().await;
    assert_eq!(save_request.method, "POST");
    assert_eq!(save_request.path, "/api/playlist/v2/playlist-1/save");
    assert_eq!(save_request.body, "");

    let unsave_server = MockServer::json("{}").await;
    let unsave_client = unsave_server.client();

    unsave_client
        .unsave_playlist("playlist-1")
        .await
        .expect("unsave playlist");

    let unsave_request = unsave_server.captured().await;
    assert_eq!(unsave_request.method, "DELETE");
    assert_eq!(unsave_request.path, "/api/playlist/v2/playlist-1/save");
    assert_eq!(unsave_request.body, "");
}

#[tokio::test]
async fn create_persona_posts_current_web_contract() {
    let server = MockServer::json(r#"{"id":"persona-1","name":"Lead Voice"}"#).await;
    let client = server.client();

    let persona = client
        .create_persona(&CreatePersonaRequest {
            root_clip_id: Some("clip-a".into()),
            name: Some("Lead Voice".into()),
            description: Some("Warm".into()),
            image_s3_id: None,
            is_public: Some(false),
            is_suno_persona: None,
            persona_type: None,
            vox_audio_id: None,
            vocal_start_s: None,
            vocal_end_s: None,
            user_input_styles: None,
            source: None,
            singer_skill_level: None,
            clips: None,
            is_voice_recording: None,
            voice_recording_id: None,
            verification_id: None,
        })
        .await
        .expect("create persona");

    assert_eq!(persona.id, "persona-1");
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/persona/create/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "root_clip_id": "clip-a",
            "name": "Lead Voice",
            "description": "Warm",
            "is_public": false
        })
    );
}

#[tokio::test]
async fn set_persona_love_fetches_detail_then_toggles_when_needed() {
    let server = MockServer::json_sequence(&[
        r#"{"id":"persona-1","name":"Lead Voice","is_loved":false}"#,
        r#"{"loved":true}"#,
    ])
    .await;
    let client = server.client();

    let response = client
        .set_persona_love("persona-1", true)
        .await
        .expect("set persona love");

    assert!(response.loved);
    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 2);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].path, "/api/persona/get-persona/persona-1/");
    assert_eq!(requests[1].method, "POST");
    assert_eq!(requests[1].path, "/api/persona/persona-1/toggle_love/");
    assert_eq!(requests[1].body, "");
}

#[tokio::test]
async fn set_persona_love_skips_toggle_when_state_already_matches() {
    let server =
        MockServer::json(r#"{"id":"persona-1","name":"Lead Voice","is_loved":true}"#).await;
    let client = server.client();

    let response = client
        .set_persona_love("persona-1", true)
        .await
        .expect("set persona love");

    assert!(response.loved);
    let requests = server.captured_all().await;
    assert_eq!(requests.len(), 1);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].path, "/api/persona/get-persona/persona-1/");
}

#[tokio::test]
async fn set_persona_visibility_puts_current_web_contract() {
    let server =
        MockServer::json(r#"{"id":"persona-1","name":"Lead Voice","is_public":true}"#).await;
    let client = server.client();

    let persona = client
        .set_persona_visibility("persona-1", true)
        .await
        .expect("set persona visibility");

    assert_eq!(persona.is_public, Some(true));
    let request = server.captured().await;
    assert_eq!(request.method, "PUT");
    assert_eq!(
        request.path,
        "/api/persona/set_visibility/persona-1/?is_public=true"
    );
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn edit_persona_puts_current_web_contract() {
    let server = MockServer::json(
        r#"{"id":"persona-1","name":"Lead Voice","description":"Warm","is_public":false}"#,
    )
    .await;
    let client = server.client();

    let persona = client
        .edit_persona(&EditPersonaRequest {
            persona_id: "persona-1".into(),
            name: Some("Lead Voice".into()),
            description: Some("Warm".into()),
            is_public: Some(false),
            persona_type: Some("vox".into()),
            user_input_styles: Some("soul".into()),
            vox_audio_id: Some("processed-1".into()),
            vocal_start_s: Some(0.43),
            vocal_end_s: Some(22.56),
        })
        .await
        .expect("edit persona");

    assert_eq!(persona.description.as_deref(), Some("Warm"));
    let request = server.captured().await;
    assert_eq!(request.method, "PUT");
    assert_eq!(request.path, "/api/persona/edit-persona/persona-1/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "persona_id": "persona-1",
            "name": "Lead Voice",
            "description": "Warm",
            "is_public": false,
            "persona_type": "vox",
            "user_input_styles": "soul",
            "vox_audio_id": "processed-1",
            "vocal_start_s": 0.43,
            "vocal_end_s": 22.56
        })
    );
}

#[tokio::test]
async fn get_persona_clips_uses_current_web_paginated_contract() {
    let server = MockServer::json(
        r#"{"persona":{"id":"persona-1","name":"Lead Voice","persona_clips":[{"clip":{"id":"clip-1","title":"Song","status":"complete","model_name":"chirp","created_at":"2026-06-30T00:00:00Z"}}]},"total_results":1,"current_page":2,"is_following":false}"#,
    )
    .await;
    let client = server.client();

    let response = client
        .get_persona_clips("persona-1", 2)
        .await
        .expect("get persona clips");

    assert_eq!(response.persona.persona_clips[0].clip.id, "clip-1");
    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(
        request.path,
        "/api/persona/get-persona-paginated/persona-1/?page=2"
    );
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn get_processed_clip_uses_current_web_contract() {
    let server = MockServer::json(
        r#"{"id":"processed-1","status":"complete","vocal_start_s":0.0,"vocal_end_s":19.92,"vocal_audio_url":"https://cdn1.suno.ai/processed_vocals.m4a"}"#,
    )
    .await;
    let client = server.client();

    let processed = client
        .get_processed_clip("processed-1")
        .await
        .expect("get processed clip");

    assert_eq!(processed.status, "complete");
    assert_eq!(
        processed.vocal_audio_url.as_deref(),
        Some("https://cdn1.suno.ai/processed_vocals.m4a")
    );
    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(request.path, "/api/processed_clip/processed-1");
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn trash_personas_puts_current_web_bulk_trash_contract() {
    let server = MockServer::json(
        r#"{"updated_persona_ids":["persona-1"],"voice_persona_count":4,"max_voice_personas":1000}"#,
    )
    .await;
    let client = server.client();

    let response = client
        .trash_personas(&["persona-1".to_string()])
        .await
        .expect("trash persona");

    assert_eq!(response.updated_persona_ids, vec!["persona-1"]);
    let request = server.captured().await;
    assert_eq!(request.method, "PUT");
    assert_eq!(request.path, "/api/persona/bulk-trash-personas/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "persona_ids": ["persona-1"],
            "undo": false,
            "hide": false
        })
    );
}

#[tokio::test]
async fn restore_personas_puts_current_web_bulk_restore_contract() {
    let server = MockServer::json(
        r#"{"updated_persona_ids":["persona-1"],"voice_persona_count":5,"max_voice_personas":1000}"#,
    )
    .await;
    let client = server.client();

    client
        .restore_personas(&["persona-1".to_string()])
        .await
        .expect("restore persona");

    let request = server.captured().await;
    assert_eq!(request.method, "PUT");
    assert_eq!(request.path, "/api/persona/bulk-trash-personas/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "persona_ids": ["persona-1"],
            "undo": true,
            "hide": false
        })
    );
}

#[tokio::test]
async fn purge_personas_puts_current_web_bulk_delete_contract() {
    let server = MockServer::json(
        r#"{"updated_persona_ids":["persona-1"],"voice_persona_count":4,"max_voice_personas":1000}"#,
    )
    .await;
    let client = server.client();

    client
        .purge_personas(&["persona-1".to_string()])
        .await
        .expect("purge persona");

    let request = server.captured().await;
    assert_eq!(request.method, "PUT");
    assert_eq!(request.path, "/api/persona/bulk-trash-personas/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "persona_ids": ["persona-1"],
            "undo": false,
            "hide": true
        })
    );
}

#[tokio::test]
async fn list_personas_uses_scope_page_and_continuation_query() {
    let server = MockServer::json(r#"{"personas":[],"total_results":0,"current_page":2}"#).await;
    let client = server.client();

    client
        .list_personas(PersonaListScope::Loved, 2, Some("next-token"))
        .await
        .expect("list personas");

    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(
        request.path,
        "/api/persona/get-loved-personas/?page=2&continuation_token=next-token"
    );
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn create_audio_upload_posts_current_web_contract() {
    let server = MockServer::json(
        r#"{"id":"upload-1","url":"https://s3.example/upload","fields":{"key":"audio/upload-1","policy":"policy-1"}}"#,
    )
    .await;
    let client = server.client();

    let upload = client
        .create_audio_upload(&CreateAudioUploadRequest {
            spec: CreateAudioUploadSpec {
                extension: "mp3".into(),
                is_stem_mix: false,
                upload_type: "file_upload".into(),
            },
        })
        .await
        .expect("create audio upload");

    assert_eq!(upload.id, "upload-1");
    assert_eq!(
        upload.fields.get("key").map(String::as_str),
        Some("audio/upload-1")
    );
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/uploads/audio/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "spec": {
                "extension": "mp3",
                "is_stem_mix": false,
                "upload_type": "file_upload"
            }
        })
    );
}

#[tokio::test]
async fn finish_audio_upload_posts_current_web_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .finish_audio_upload(
            "upload-1",
            &FinishAudioUploadRequest {
                upload_type: "file_upload".into(),
                upload_filename: "demo.mp3".into(),
            },
        )
        .await
        .expect("finish audio upload");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/uploads/audio/upload-1/upload-finish/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({
            "upload_type": "file_upload",
            "upload_filename": "demo.mp3"
        })
    );
}

#[tokio::test]
async fn get_audio_upload_fetches_current_status_contract() {
    let server = MockServer::json(
        r#"{"id":"upload-1","status":"complete","title":"Demo","image_url":"https://cdn.example/cover.jpg","has_vocal":true,"copyright_muted":false}"#,
    )
    .await;
    let client = server.client();

    let status = client
        .get_audio_upload("upload-1")
        .await
        .expect("get audio upload");

    assert_eq!(status.id.as_deref(), Some("upload-1"));
    assert_eq!(status.status.as_deref(), Some("complete"));
    assert_eq!(status.has_vocal, Some(true));
    let request = server.captured().await;
    assert_eq!(request.method, "GET");
    assert_eq!(request.path, "/api/uploads/audio/upload-1/");
    assert_eq!(request.body, "");
}

#[tokio::test]
async fn initialize_audio_clip_posts_current_web_contract() {
    let server = MockServer::json(r#"{"clip_id":"clip-1"}"#).await;
    let client = server.client();

    let response = client
        .initialize_audio_clip(
            "upload-1",
            &InitializeAudioClipRequest {
                downbeats: Some(vec![0.0, 1.25]),
                user_reviewed_tags: None,
            },
        )
        .await
        .expect("initialize audio clip");

    assert_eq!(response.clip_id.as_deref(), Some("clip-1"));
    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/api/uploads/audio/upload-1/initialize-clip/");
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&request.body).expect("request json"),
        serde_json::json!({ "downbeats": [0.0, 1.25] })
    );
}

#[tokio::test]
async fn upload_presigned_audio_form_posts_s3_multipart_contract() {
    let server = MockServer::json("{}").await;
    let client = server.client();

    client
        .upload_presigned_audio_form(
            &format!("{}/s3-upload", server.base_url),
            &[
                ("key".into(), "audio/upload-1".into()),
                ("policy".into(), "p".into()),
            ]
            .into_iter()
            .collect(),
            "demo.mp3",
            b"audio-bytes".to_vec(),
        )
        .await
        .expect("upload presigned form");

    let request = server.captured().await;
    assert_eq!(request.method, "POST");
    assert_eq!(request.path, "/s3-upload");
    assert!(request.headers.contains("multipart/form-data"));
    assert!(request.body.contains("name=\"key\""));
    assert!(request.body.contains("audio/upload-1"));
    assert!(request.body.contains("name=\"file\""));
    assert!(request.body.contains("filename=\"demo.mp3\""));
    assert!(request.body.contains("audio-bytes"));
}
