cueward-adapter-macos 0.3.1

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use chrono::{Duration as ChronoDuration, Utc};
use std::time::{Duration, Instant};
use serde_json::json;

use super::{
    build_feed_url, build_search_url, compute_next_reddit_request, normalize_post_url,
    normalize_subreddit, parse_listing_posts, parse_post_result, parse_subreddit_info,
    reddit_backoff, should_retry_status, RedditComment,
};
use super::scan::filter_comments;

#[test]
fn normalize_subreddit_accepts_prefixed_or_plain_names() {
    assert_eq!(normalize_subreddit("rust").unwrap(), "rust");
    assert_eq!(normalize_subreddit("r/rust").unwrap(), "rust");
    assert_eq!(normalize_subreddit(" R/Rust ").unwrap(), "rust");
}

#[test]
fn normalize_subreddit_rejects_invalid_names() {
    assert!(normalize_subreddit("").is_err());
    assert!(normalize_subreddit("r/").is_err());
    assert!(normalize_subreddit("r/rust/top").is_err());
}

#[test]
fn normalize_post_url_rewrites_supported_hosts_to_old_reddit_json() {
    let url = normalize_post_url(
        "https://www.reddit.com/r/rust/comments/abc123/example_title/?utm_source=test",
    )
    .unwrap();

    assert_eq!(
        url,
        "https://old.reddit.com/r/rust/comments/abc123/example_title.json?limit=500"
    );
}

#[test]
fn normalize_post_url_ignores_query_and_fragment_without_trailing_slash() {
    let url = normalize_post_url(
        "https://www.reddit.com/r/rust/comments/abc123/example_title?context=3#top",
    )
    .unwrap();

    assert_eq!(
        url,
        "https://old.reddit.com/r/rust/comments/abc123/example_title.json?limit=500"
    );
}

#[test]
fn build_search_url_handles_global_and_subreddit_search() {
    assert_eq!(
        build_search_url("async rust", None, 10),
        "https://old.reddit.com/search.json?q=async%20rust&limit=10&sort=relevance"
    );
    assert_eq!(
        build_search_url("async rust", Some("rust"), 25),
        "https://old.reddit.com/r/rust/search.json?q=async%20rust&restrict_sr=on&limit=25&sort=relevance"
    );
    assert_eq!(build_feed_url("rust", 20), "https://old.reddit.com/r/rust.json?limit=20");
}

#[test]
fn reddit_backoff_is_small_and_linear() {
    assert_eq!(reddit_backoff(0), Duration::from_secs(2));
    assert_eq!(reddit_backoff(1), Duration::from_secs(4));
    assert_eq!(reddit_backoff(2), Duration::from_secs(6));
}

#[test]
fn compute_next_reddit_request_reserves_minimum_delay() {
    let now = Instant::now();
    let (delay, next_allowed) =
        compute_next_reddit_request(now, Some(now + Duration::from_millis(250)));

    assert!(delay.is_some());
    assert!(next_allowed > now);
}

#[test]
fn retryable_statuses_cover_429_and_server_errors() {
    assert!(should_retry_status(429));
    assert!(should_retry_status(500));
    assert!(should_retry_status(503));
    assert!(!should_retry_status(404));
}

#[test]
fn parse_subreddit_info_reads_about_payload() {
    let payload = json!({
        "kind": "t5",
        "data": {
            "display_name": "rust",
            "display_name_prefixed": "r/rust",
            "title": "The Rust Programming Language",
            "public_description": "Fearless concurrency",
            "subscribers": 123
        }
    });

    let info = parse_subreddit_info(&payload).unwrap();

    assert_eq!(info.name, "rust");
    assert_eq!(info.display_name, "r/rust");
    assert_eq!(info.title.as_deref(), Some("The Rust Programming Language"));
    assert_eq!(info.description.as_deref(), Some("Fearless concurrency"));
    assert_eq!(info.subscribers, Some(123));
}

#[test]
fn parse_subreddit_info_ignores_negative_subscriber_count() {
    let payload = json!({
        "kind": "t5",
        "data": {
            "display_name": "rust",
            "display_name_prefixed": "r/rust",
            "subscribers": -1
        }
    });

    let info = parse_subreddit_info(&payload).unwrap();

    assert_eq!(info.subscribers, None);
}

#[test]
fn parse_listing_posts_reads_posts_from_listing_payload() {
    let payload = json!({
        "kind": "Listing",
        "data": {
            "children": [{
                "kind": "t3",
                "data": {
                    "id": "abc123",
                    "title": "Rust 1.90 released",
                    "author": "example_user",
                    "subreddit": "rust",
                    "url": "https://www.rust-lang.org/",
                    "permalink": "/r/rust/comments/abc123/rust_190_released/",
                    "score": 420,
                    "num_comments": 37,
                    "created_utc": 1760000000,
                    "selftext": ""
                }
            }]
        }
    });

    let posts = parse_listing_posts(&payload).unwrap();

    assert_eq!(posts.len(), 1);
    assert_eq!(posts[0].id, "abc123");
    assert_eq!(
        posts[0].permalink,
        "https://reddit.com/r/rust/comments/abc123/rust_190_released/"
    );
}

#[test]
fn parse_post_result_returns_post_and_top_level_comments_only() {
    let payload = json!([
        {
            "kind": "Listing",
            "data": {
                "children": [{
                    "kind": "t3",
                    "data": {
                        "id": "abc123",
                        "title": "Rust 1.90 released",
                        "author": "example_user",
                        "subreddit": "rust",
                        "url": "https://www.rust-lang.org/",
                        "permalink": "/r/rust/comments/abc123/rust_190_released/",
                        "score": 420,
                        "num_comments": 37,
                        "created_utc": 1760000000,
                        "selftext": "release notes"
                    }
                }]
            }
        },
        {
            "kind": "Listing",
            "data": {
                "children": [
                    {
                        "kind": "t1",
                        "data": {
                            "id": "c1",
                            "author": "commenter1",
                            "body": "great release",
                            "score": 12,
                            "created_utc": 1760000100,
                            "permalink": "/r/rust/comments/abc123/rust_190_released/c1/"
                        }
                    },
                    {
                        "kind": "more",
                        "data": { "count": 3 }
                    }
                ]
            }
        }
    ]);

    let result = parse_post_result(&payload).unwrap();

    assert_eq!(result.post.id, "abc123");
    assert_eq!(result.comments.len(), 1);
    assert_eq!(result.comments[0].id, "c1");
}

#[test]
fn filter_comments_drops_deleted_short_and_old_comments() {
    let now = Utc::now();
    let comments = vec![
        RedditComment {
            id: "c1".to_string(),
            author: "[deleted]".to_string(),
            body: "this comment is long enough but deleted".to_string(),
            score: 1,
            created_utc: now.timestamp(),
            permalink: "https://reddit.com/c1".to_string(),
        },
        RedditComment {
            id: "c2".to_string(),
            author: "real_user".to_string(),
            body: "too short".to_string(),
            score: 1,
            created_utc: now.timestamp(),
            permalink: "https://reddit.com/c2".to_string(),
        },
        RedditComment {
            id: "c3".to_string(),
            author: "real_user".to_string(),
            body: "this comment is long enough but too old to keep around".to_string(),
            score: 1,
            created_utc: (now - ChronoDuration::days(31)).timestamp(),
            permalink: "https://reddit.com/c3".to_string(),
        },
        RedditComment {
            id: "c4".to_string(),
            author: "real_user".to_string(),
            body: "this comment is long enough and recent so it should survive".to_string(),
            score: 1,
            created_utc: now.timestamp(),
            permalink: "https://reddit.com/c4".to_string(),
        },
    ];

    let filtered = filter_comments(comments, now);

    assert_eq!(filtered.len(), 1);
    assert_eq!(filtered[0].id, "c4");
}