mod common;

use common::{
    request,
    request_binary,
    socket_path,
    start_daemon,
    wait_for_socket,
};

#[tokio::test]
async fn full_pipeline() {
    let sock = socket_path();
    let _ = std::fs::remove_file(&sock);

    let mut daemon = start_daemon(&sock);
    wait_for_socket(&sock).await;

    // Create terminal
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "create",
            "cols": 80,
            "rows": 24,
            "cmd_args": ["/bin/sh"],
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let id = resp["id"].as_str().unwrap().to_string();

    // List terminals
    let resp = request(
        &sock,
        &serde_json::json!({"cmd": "list"}),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let terms = resp["terminals"].as_array().unwrap();
    assert!(terms.iter().any(|t| t["id"] == id));

    // Send input
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "send",
            "id": id,
            "input": "echo hello\n",
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);

    // Wait for output
    tokio::time::sleep(
        std::time::Duration::from_millis(500),
    )
    .await;

    // Read text
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "text",
            "id": id,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let lines = resp["lines"].as_array().unwrap();
    let text: String = lines
        .iter()
        .filter_map(|l| l.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(
        text.contains("hello"),
        "expected 'hello' in output, got:\n{text}"
    );

    // Bottom-indexed text: 0:5 returns last 5 lines
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "text",
            "id": id,
            "start": 0,
            "end": 5,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let lines = resp["lines"].as_array().unwrap();
    assert_eq!(lines.len(), 5);

    // 0:24 returns all 24 lines and contains output
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "text",
            "id": id,
            "start": 0,
            "end": 24,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let lines = resp["lines"].as_array().unwrap();
    assert_eq!(lines.len(), 24);
    let text: String = lines
        .iter()
        .filter_map(|l| l.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(
        text.contains("hello"),
        "expected 'hello' in text, got:\n{text}"
    );

    // Cursor
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "cursor",
            "id": id,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    assert!(resp["row"].is_number());
    assert!(resp["col"].is_number());

    // Screen (PNG)
    let (header, png) = request_binary(
        &sock,
        &serde_json::json!({
            "cmd": "screenshot",
            "id": id,
        }),
    )
    .await;
    assert_eq!(header["ok"], true);
    assert_eq!(
        &png[..4],
        &[137, 80, 78, 71],
        "expected PNG magic bytes"
    );

    // Resize
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "resize",
            "id": id,
            "cols": 120,
            "rows": 40,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);

    // Kill
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "kill",
            "id": id,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);

    // Cleanup
    let _ = daemon.kill();
    let _ = daemon.wait();
    let _ = std::fs::remove_file(&sock);
}

#[tokio::test]
async fn attach_raw_bytes() {
    use tokio::io::{
        AsyncReadExt,
        AsyncWriteExt,
    };
    use tokio::net::UnixStream;

    let sock = socket_path();
    let _ = std::fs::remove_file(&sock);

    let mut daemon = start_daemon(&sock);
    wait_for_socket(&sock).await;

    // Create terminal
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "create",
            "cols": 80,
            "rows": 24,
            "cmd_args": ["/bin/sh"],
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let id = resp["id"].as_str().unwrap().to_string();

    // Attach — no JSON ack, just raw bytes after
    // the request line
    let stream =
        UnixStream::connect(&sock).await.unwrap();
    let (reader, mut writer) = stream.into_split();
    let req = serde_json::json!({
        "cmd": "attach", "id": id,
    });
    let mut buf = serde_json::to_vec(&req).unwrap();
    buf.push(b'\n');
    writer.write_all(&buf).await.unwrap();
    let mut reader =
        tokio::io::BufReader::new(reader);

    // Send a command through the raw stream
    writer
        .write_all(b"echo t4a_attach_test\n")
        .await
        .unwrap();

    // Read raw bytes until we see our marker
    let mut received = Vec::new();
    let mut buf = [0u8; 4096];
    let deadline = tokio::time::Instant::now()
        + std::time::Duration::from_secs(3);

    loop {
        let remaining = deadline
            - tokio::time::Instant::now();
        match tokio::time::timeout(
            remaining,
            reader.read(&mut buf),
        )
        .await
        {
            Ok(Ok(n)) if n > 0 => {
                received.extend_from_slice(&buf[..n]);
                let text =
                    String::from_utf8_lossy(&received);
                if text.contains("t4a_attach_test") {
                    break;
                }
            }
            _ => panic!(
                "timed out waiting for attach output, \
                 got: {}",
                String::from_utf8_lossy(&received)
            ),
        }
    }

    // Cleanup
    let _ = request(
        &sock,
        &serde_json::json!({
            "cmd": "kill", "id": id
        }),
    )
    .await;
    let _ = daemon.kill();
    let _ = daemon.wait();
    let _ = std::fs::remove_file(&sock);
}
