//! In-process HTTP API tests using tower::ServiceExt::oneshot.
//! No TCP socket required — requests go directly to the Axum router.

use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use mps::commands::serve::{build_router, AppState};
use mps::config::{Config, ServeConfig};
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use tower::ServiceExt;

// ── Test helpers ──────────────────────────────────────────────────────────────

fn make_state(dir: &TempDir, token: &str) -> Arc<AppState> {
    let storage = dir.path().join("storage");
    std::fs::create_dir_all(&storage).unwrap();
    let config = Config {
        mps_dir: dir.path().to_path_buf(),
        storage_dir: storage,
        log_file: dir.path().join("mps.log"),
        git_remote: "origin".into(),
        git_branch: "master".into(),
        default_command: "list".into(),
        type_aliases: Default::default(),
        command_aliases: Default::default(),
        custom_tags: vec![],
        notify: Default::default(),
        serve: ServeConfig {
            port: 3000,
            host: "127.0.0.1".into(),
            token: token.to_string(),
        },
    };
    Arc::new(AppState {
        config,
        config_path: PathBuf::from("/tmp/test_config.yaml"),
    })
}

async fn body_json(body: Body) -> serde_json::Value {
    let bytes = body.collect().await.unwrap().to_bytes();
    serde_json::from_slice(&bytes).unwrap()
}

async fn body_str(body: Body) -> String {
    let bytes = body.collect().await.unwrap().to_bytes();
    String::from_utf8(bytes.to_vec()).unwrap()
}

fn get_req(uri: &str) -> Request<Body> {
    Request::builder()
        .method("GET")
        .uri(uri)
        .body(Body::empty())
        .unwrap()
}

fn get_req_auth(uri: &str, token: &str) -> Request<Body> {
    Request::builder()
        .method("GET")
        .uri(uri)
        .header("Authorization", format!("Bearer {}", token))
        .body(Body::empty())
        .unwrap()
}

fn post_json(uri: &str, body: serde_json::Value) -> Request<Body> {
    Request::builder()
        .method("POST")
        .uri(uri)
        .header("Content-Type", "application/json")
        .body(Body::from(serde_json::to_vec(&body).unwrap()))
        .unwrap()
}

fn post_json_auth(uri: &str, body: serde_json::Value, token: &str) -> Request<Body> {
    Request::builder()
        .method("POST")
        .uri(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", format!("Bearer {}", token))
        .body(Body::from(serde_json::to_vec(&body).unwrap()))
        .unwrap()
}

fn patch_json(uri: &str, body: serde_json::Value) -> Request<Body> {
    Request::builder()
        .method("PATCH")
        .uri(uri)
        .header("Content-Type", "application/json")
        .body(Body::from(serde_json::to_vec(&body).unwrap()))
        .unwrap()
}

fn delete_req(uri: &str) -> Request<Body> {
    Request::builder()
        .method("DELETE")
        .uri(uri)
        .body(Body::empty())
        .unwrap()
}

// ── 1. Health ─────────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_health_returns_ok() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/health")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["status"], "ok");
    assert!(j["version"].as_str().unwrap().contains("1.7"));
}

#[tokio::test]
async fn test_health_no_auth_required() {
    let dir = TempDir::new().unwrap();
    // Even with a token configured, /health is public — well, it goes through
    // the same auth middleware. Test that health works without auth when token is empty.
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/health")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

// ── 2. Auth ───────────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_auth_missing_token_returns_401() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, "secret"));
    let resp = app.oneshot(get_req("/health")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
    let j = body_json(resp.into_body()).await;
    assert!(j["error"].as_str().unwrap().contains("unauthorized"));
}

#[tokio::test]
async fn test_auth_wrong_token_returns_401() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, "secret"));
    let resp = app
        .oneshot(get_req_auth("/health", "wrong"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_auth_correct_token_returns_200() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, "secret"));
    let resp = app
        .oneshot(get_req_auth("/health", "secret"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_auth_no_token_configured_always_200() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/elements")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

// ── 3. GET /elements ──────────────────────────────────────────────────────────

#[tokio::test]
async fn test_list_empty_store_returns_empty_array() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/elements")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let j = body_json(resp.into_body()).await;
    assert!(j.as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_list_returns_appended_element() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    let app = build_router(state.clone());
    // Append a task
    let post_resp = app
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "write tests"}),
        ))
        .await
        .unwrap();
    assert_eq!(post_resp.status(), StatusCode::CREATED);

    // Now list
    let app2 = build_router(state);
    let resp = app2.oneshot(get_req("/elements")).await.unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["type"], "task");
    assert_eq!(arr[0]["body"], "write tests");
}

#[tokio::test]
async fn test_list_type_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    // Append task and note
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "a task"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "a note"}),
        ))
        .await
        .unwrap();

    // Filter by type=task
    let resp = build_router(state)
        .oneshot(get_req("/elements?type=task"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["type"], "task");
}

#[tokio::test]
async fn test_list_tag_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "tagged", "tags": ["work"]}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "no tag"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/elements?tag=work"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["body"], "tagged");
}

#[tokio::test]
async fn test_list_all_flag() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "alltest"}),
        ))
        .await
        .unwrap();
    let resp = build_router(state)
        .oneshot(get_req("/elements?all=true"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert!(!j.as_array().unwrap().is_empty());
}

// ── 4. POST /elements ─────────────────────────────────────────────────────────

#[tokio::test]
async fn test_append_valid_task_returns_201_with_ref() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "new task"}),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
    let j = body_json(resp.into_body()).await;
    assert!(j["ref"].as_str().is_some());
}

#[tokio::test]
async fn test_append_invalid_type_returns_400() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "invalid_type", "body": "body"}),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let j = body_json(resp.into_body()).await;
    assert!(j["error"].as_str().unwrap().contains("unknown type"));
}

#[tokio::test]
async fn test_append_missing_type_returns_422() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"body": "body without type"}),
        ))
        .await
        .unwrap();
    // Axum returns 422 for JSON deserialization failures (missing required field)
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
}

#[tokio::test]
async fn test_append_all_element_types() {
    let dir = TempDir::new().unwrap();
    for kind in &["task", "note", "log", "reminder", "character"] {
        let state = make_state(&dir, "");
        let resp = build_router(state)
            .oneshot(post_json(
                "/elements",
                serde_json::json!({"type": kind, "body": format!("a {}", kind)}),
            ))
            .await
            .unwrap();
        assert_eq!(
            resp.status(),
            StatusCode::CREATED,
            "failed for type {}",
            kind
        );
    }
}

#[tokio::test]
async fn test_append_with_tags_and_status() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    let resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({
                "type": "task",
                "body": "tagged task",
                "tags": ["work", "urgent"],
                "status": "done"
            }),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);

    let resp2 = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let j = body_json(resp2.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    let tags: Vec<&str> = arr[0]["tags"]
        .as_array()
        .unwrap()
        .iter()
        .map(|v| v.as_str().unwrap())
        .collect();
    assert!(tags.contains(&"work"));
    assert_eq!(arr[0]["status"], "done");
}

#[tokio::test]
async fn test_append_with_auth_token() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, "tok123"));
    let resp = app
        .oneshot(post_json_auth(
            "/elements",
            serde_json::json!({"type": "note", "body": "auth note"}),
            "tok123",
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
}

// ── 5. PATCH /elements/:ref ───────────────────────────────────────────────────

#[tokio::test]
async fn test_patch_marks_task_done() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    // Append
    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "patch me"}),
        ))
        .await
        .unwrap();
    let post_j = body_json(post_resp.into_body()).await;
    let epoch_ref = post_j["ref"].as_str().unwrap().to_string();

    // Patch
    let patch_resp = build_router(state.clone())
        .oneshot(patch_json(
            &format!("/elements/{}", epoch_ref),
            serde_json::json!({"status": "done"}),
        ))
        .await
        .unwrap();
    assert_eq!(patch_resp.status(), StatusCode::OK);
    let patch_j = body_json(patch_resp.into_body()).await;
    assert_eq!(patch_j["updated"], true);

    // Verify
    let list_resp = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let list_j = body_json(list_resp.into_body()).await;
    let arr = list_j.as_array().unwrap();
    assert_eq!(arr[0]["status"], "done");
}

#[tokio::test]
async fn test_patch_nonexistent_ref_returns_404() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(patch_json(
            "/elements/99991231.9999",
            serde_json::json!({"status": "done"}),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

// ── 6. DELETE /elements/:ref ──────────────────────────────────────────────────

#[tokio::test]
async fn test_delete_element() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    // Append
    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "delete me"}),
        ))
        .await
        .unwrap();
    let j = body_json(post_resp.into_body()).await;
    let epoch_ref = j["ref"].as_str().unwrap().to_string();

    // Delete
    let del_resp = build_router(state.clone())
        .oneshot(delete_req(&format!("/elements/{}", epoch_ref)))
        .await
        .unwrap();
    assert_eq!(del_resp.status(), StatusCode::OK);
    let del_j = body_json(del_resp.into_body()).await;
    assert_eq!(del_j["deleted"], true);

    // Verify gone
    let list_resp = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let list_j = body_json(list_resp.into_body()).await;
    assert!(list_j.as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_delete_nonexistent_returns_404() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(delete_req("/elements/99991231.9999"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

// ── 7. GET /search ────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_search_empty_query_returns_all() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "hello world"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "foo bar"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    // Empty query matches all (both elements)
    assert_eq!(j.as_array().unwrap().len(), 2);
}

#[tokio::test]
async fn test_search_query_filters() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "hello world"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "foo bar"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search?q=hello"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j.as_array().unwrap().len(), 1);
    assert_eq!(j[0]["body"], "hello world");
}

#[tokio::test]
async fn test_search_type_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "a task"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "a note"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search?type=note"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["type"], "note");
}

#[tokio::test]
async fn test_search_tag_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "tagged", "tags": ["rust"]}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "untagged"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search?tag=rust"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j.as_array().unwrap().len(), 1);
    assert_eq!(j[0]["body"], "tagged");
}

#[tokio::test]
async fn test_search_case_insensitive() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "Hello World"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search?q=hello+world"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j.as_array().unwrap().len(), 1);
}

// ── 8. GET /stats ─────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_stats_empty_store() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/stats")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["tasks"]["total"], 0);
    assert_eq!(j["notes"], 0);
}

#[tokio::test]
async fn test_stats_counts_after_appends() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "t1"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "t2", "status": "done"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "n1"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/stats"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["tasks"]["total"], 2);
    assert_eq!(j["notes"], 1);
}

#[tokio::test]
async fn test_stats_done_vs_open() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "open task"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "done task", "status": "done"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/stats"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["tasks"]["open"], 1);
    assert_eq!(j["tasks"]["done"], 1);
}

#[tokio::test]
async fn test_stats_all_flag() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "reminder", "body": "r1"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/stats?all=true"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["reminders"], 1);
}

// ── 9. GET /export ────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_export_json_returns_array() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "exportable"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/export?format=json"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let j = body_json(resp.into_body()).await;
    assert!(j.is_array());
    assert_eq!(j.as_array().unwrap().len(), 1);
    assert_eq!(j[0]["body"], "exportable");
}

#[tokio::test]
async fn test_export_csv_returns_text() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "csv task"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/export?format=csv"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let text = body_str(resp.into_body()).await;
    // Should contain CSV header
    assert!(text.contains("ref") || text.contains("type") || text.contains("body"));
    assert!(text.contains("csv task"));
}

#[tokio::test]
async fn test_export_empty_store_returns_empty_array() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/export?format=json")).await.unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j.as_array().unwrap().len(), 0);
}

#[tokio::test]
async fn test_export_type_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "t"}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "n"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/export?format=json&type=task"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["type"], "task");
}

// ── 10. GET /tags ─────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_tags_returns_frequency_map() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "t1", "tags": ["work", "rust"]}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "n1", "tags": ["work"]}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/tags"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let map = j.as_object().unwrap();
    assert_eq!(map["work"], 2);
    assert_eq!(map["rust"], 1);
}

#[tokio::test]
async fn test_tags_empty_store_returns_empty_map() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app.oneshot(get_req("/tags")).await.unwrap();
    let j = body_json(resp.into_body()).await;
    assert!(j.as_object().unwrap().is_empty());
}

#[tokio::test]
async fn test_tags_all_flag_scans_all_dates() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "n", "tags": ["alpha"]}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/tags?all=true"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let map = j.as_object().unwrap();
    assert!(map.contains_key("alpha"));
}

#[tokio::test]
async fn test_tags_type_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "t", "tags": ["task-tag"]}),
        ))
        .await
        .unwrap();
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "n", "tags": ["note-tag"]}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/tags?type=task"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let map = j.as_object().unwrap();
    assert!(map.contains_key("task-tag"));
    assert!(!map.contains_key("note-tag"));
}

// ── 11. Full workflow end-to-end ──────────────────────────────────────────────

#[tokio::test]
async fn test_full_crud_workflow() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    // Append task
    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "workflow task", "tags": ["wf"]}),
        ))
        .await
        .unwrap();
    assert_eq!(post_resp.status(), StatusCode::CREATED);
    let post_j = body_json(post_resp.into_body()).await;
    let epoch_ref = post_j["ref"].as_str().unwrap().to_string();
    assert!(!epoch_ref.is_empty());

    // List — confirm present
    let list_j = body_json(
        build_router(state.clone())
            .oneshot(get_req("/elements"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert_eq!(list_j.as_array().unwrap().len(), 1);
    assert_eq!(list_j[0]["status"], "open");

    // Search — found
    let search_j = body_json(
        build_router(state.clone())
            .oneshot(get_req("/search?q=workflow"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert_eq!(search_j.as_array().unwrap().len(), 1);

    // Patch — mark done
    let patch_resp = build_router(state.clone())
        .oneshot(patch_json(
            &format!("/elements/{}", epoch_ref),
            serde_json::json!({"status": "done"}),
        ))
        .await
        .unwrap();
    assert_eq!(patch_resp.status(), StatusCode::OK);

    // Stats — 1 done
    let stats_j = body_json(
        build_router(state.clone())
            .oneshot(get_req("/stats"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert_eq!(stats_j["tasks"]["done"], 1);
    assert_eq!(stats_j["tasks"]["open"], 0);

    // Delete
    let del_resp = build_router(state.clone())
        .oneshot(delete_req(&format!("/elements/{}", epoch_ref)))
        .await
        .unwrap();
    assert_eq!(del_resp.status(), StatusCode::OK);

    // List — empty
    let final_list = body_json(
        build_router(state)
            .oneshot(get_req("/elements"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert!(final_list.as_array().unwrap().is_empty());
}

// ── 12. Edge cases ────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_search_no_match_returns_empty() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "hello"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/search?q=zzznomatch"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert!(j.as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_list_ref_field_present() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "ref check"}),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    let arr = j.as_array().unwrap();
    assert!(arr[0].get("ref").is_some());
    assert!(arr[0].get("date").is_some());
    assert!(arr[0].get("type").is_some());
}

#[tokio::test]
async fn test_append_log_with_start_end() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    let resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({
                "type": "log",
                "body": "worked",
                "start": "09:00",
                "end": "10:30"
            }),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);

    let list_j = body_json(
        build_router(state)
            .oneshot(get_req("/elements"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    let arr = list_j.as_array().unwrap();
    assert_eq!(arr[0]["start"], "09:00");
    assert_eq!(arr[0]["end"], "10:30");
}

#[tokio::test]
async fn test_append_reminder_with_at() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({
                "type": "reminder",
                "body": "call doctor",
                "at": "3pm"
            }),
        ))
        .await
        .unwrap();

    let list_j = body_json(
        build_router(state)
            .oneshot(get_req("/elements"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert_eq!(list_j[0]["at"], "3pm");
}

#[tokio::test]
async fn test_append_character_with_name() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({
                "type": "character",
                "body": "kind person",
                "name": "Alice"
            }),
        ))
        .await
        .unwrap();

    let list_j = body_json(
        build_router(state)
            .oneshot(get_req("/elements"))
            .await
            .unwrap()
            .into_body(),
    )
    .await;
    assert_eq!(list_j[0]["name"], "Alice");
}

#[tokio::test]
async fn test_export_since_filter() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "since test"}),
        ))
        .await
        .unwrap();

    // Since today — should include today's element
    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
    let resp = build_router(state)
        .oneshot(get_req(&format!("/export?format=json&since={}", today)))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert!(!j.as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_stats_log_duration() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");
    build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({
                "type": "log",
                "body": "coding",
                "start": "09:00",
                "end": "10:00"
            }),
        ))
        .await
        .unwrap();

    let resp = build_router(state)
        .oneshot(get_req("/stats"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["logs"]["total"], 1);
    assert_eq!(j["logs"]["duration_minutes"], 60);
}

#[tokio::test]
async fn test_multiple_elements_multiple_types() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    for (kind, body) in &[
        ("task", "task body"),
        ("note", "note body"),
        ("log", "log body"),
        ("reminder", "reminder body"),
        ("character", "character body"),
    ] {
        build_router(state.clone())
            .oneshot(post_json(
                "/elements",
                serde_json::json!({"type": kind, "body": body}),
            ))
            .await
            .unwrap();
    }

    let resp = build_router(state)
        .oneshot(get_req("/stats"))
        .await
        .unwrap();
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["tasks"]["total"], 1);
    assert_eq!(j["notes"], 1);
    assert_eq!(j["logs"]["total"], 1);
    assert_eq!(j["reminders"], 1);
    assert_eq!(j["characters"], 1);
}

// ── 13. GET / (UI) ────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_ui_returns_html() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(
            Request::builder()
                .method("GET")
                .uri("/")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let ct = resp.headers().get("content-type").unwrap().to_str().unwrap();
    assert!(ct.contains("text/html"));
    let text = body_str(resp.into_body()).await;
    assert!(text.contains("<!DOCTYPE html>"));
    assert!(text.contains("mps"));
}

#[tokio::test]
async fn test_ui_accessible_without_auth_when_no_token() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(
            Request::builder()
                .method("GET")
                .uri("/")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

// ── 14. GET /elements/:ref ────────────────────────────────────────────────────

#[tokio::test]
async fn test_get_single_element_by_epoch_ref() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "get by ref"}),
        ))
        .await
        .unwrap();
    let pj = body_json(post_resp.into_body()).await;
    let epoch_ref = pj["ref"].as_str().unwrap().to_string();

    let resp = build_router(state)
        .oneshot(get_req(&format!("/elements/{}", epoch_ref)))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let j = body_json(resp.into_body()).await;
    assert_eq!(j["body"], "get by ref");
    assert_eq!(j["type"], "task");
    assert_eq!(j["ref"], epoch_ref);
}

#[tokio::test]
async fn test_get_single_element_not_found() {
    let dir = TempDir::new().unwrap();
    let app = build_router(make_state(&dir, ""));
    let resp = app
        .oneshot(get_req("/elements/99991231.9999"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

// ── 15. PATCH body editing ────────────────────────────────────────────────────

#[tokio::test]
async fn test_patch_updates_body() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    // Append
    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "original body"}),
        ))
        .await
        .unwrap();
    let pj = body_json(post_resp.into_body()).await;
    let epoch_ref = pj["ref"].as_str().unwrap().to_string();

    // Patch body
    let patch_resp = build_router(state.clone())
        .oneshot(patch_json(
            &format!("/elements/{}", epoch_ref),
            serde_json::json!({"body": "updated body"}),
        ))
        .await
        .unwrap();
    assert_eq!(patch_resp.status(), StatusCode::OK);

    // Verify via list
    let list_resp = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let lj = body_json(list_resp.into_body()).await;
    assert_eq!(lj[0]["body"], "updated body");
}

#[tokio::test]
async fn test_patch_body_and_status_together() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "task", "body": "initial task"}),
        ))
        .await
        .unwrap();
    let pj = body_json(post_resp.into_body()).await;
    let epoch_ref = pj["ref"].as_str().unwrap().to_string();

    // Patch body AND status
    let patch_resp = build_router(state.clone())
        .oneshot(patch_json(
            &format!("/elements/{}", epoch_ref),
            serde_json::json!({"body": "revised task", "status": "done"}),
        ))
        .await
        .unwrap();
    assert_eq!(patch_resp.status(), StatusCode::OK);

    let list_resp = build_router(state)
        .oneshot(get_req("/elements"))
        .await
        .unwrap();
    let lj = body_json(list_resp.into_body()).await;
    assert_eq!(lj[0]["body"], "revised task");
    assert_eq!(lj[0]["status"], "done");
}

#[tokio::test]
async fn test_patch_no_fields_returns_400() {
    let dir = TempDir::new().unwrap();
    let state = make_state(&dir, "");

    let post_resp = build_router(state.clone())
        .oneshot(post_json(
            "/elements",
            serde_json::json!({"type": "note", "body": "body"}),
        ))
        .await
        .unwrap();
    let pj = body_json(post_resp.into_body()).await;
    let epoch_ref = pj["ref"].as_str().unwrap().to_string();

    // PATCH with empty JSON
    let patch_resp = build_router(state)
        .oneshot(patch_json(
            &format!("/elements/{}", epoch_ref),
            serde_json::json!({}),
        ))
        .await
        .unwrap();
    assert_eq!(patch_resp.status(), StatusCode::BAD_REQUEST);
}
