mod common;

use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

use axum::http::StatusCode;
use doctate_common::oneliners::OnelinerState;
use filetime::FileTime;
use tower::util::ServiceExt;

use common::{
    CLOSE_MARKER, TestConfig, body_json, get_with_api_key, get_with_api_key_if_none_match,
    header_str, seed_oneliner_ready, seed_oneliner_state, seed_recording_with_age, test_user,
    test_user_with_window_hours,
};

const TEST_SLUG: &str = "dr_oneliners";
const TEST_KEY: &str = "key-dr_oneliners";

fn test_config() -> (std::sync::Arc<doctate_server::config::Config>, PathBuf) {
    let cfg = TestConfig::new()
        .with_label("oneliners")
        .with_user(test_user(TEST_SLUG))
        .build();
    let dp = cfg.data_path.clone();
    (cfg, dp)
}

fn test_config_with_window_hours(
    hours: u32,
) -> (std::sync::Arc<doctate_server::config::Config>, PathBuf) {
    let cfg = TestConfig::new()
        .with_label("oneliners-win")
        .with_user(test_user_with_window_hours(TEST_SLUG, hours))
        .build();
    let dp = cfg.data_path.clone();
    (cfg, dp)
}

/// Seed a case with an aged m4a and optionally a ready oneliner.
/// Combines the common primitives — a single call is more ergonomic
/// here than three repeated triples across every test.
fn seed(
    user_root: &Path,
    case_id: &str,
    m4a_age: Duration,
    oneliner_text: Option<&str>,
) -> PathBuf {
    let case_dir = user_root.join(TEST_SLUG).join(case_id);
    std::fs::create_dir_all(&case_dir).unwrap();
    seed_recording_with_age(&case_dir, "2026-04-18T10-00-00Z", m4a_age);
    if let Some(text) = oneliner_text {
        seed_oneliner_ready(&case_dir, text);
    }
    case_dir
}

fn mark_deleted(case_dir: &Path) {
    std::fs::write(case_dir.join(CLOSE_MARKER), "{}").unwrap();
}

#[tokio::test]
async fn returns_401_without_api_key() {
    let (config, _dp) = test_config();
    let app = doctate_server::create_router(config);

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .method("GET")
                .uri("/api/oneliners")
                .body(axum::body::Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn empty_user_dir_returns_200_empty_list() {
    let (config, _dp) = test_config();
    let app = doctate_server::create_router(config);

    let response = app
        .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    let etag = header_str(&response, "etag");
    assert!(etag.starts_with("W/\""), "etag was {etag:?}");
    assert!(etag.ends_with("-72\""), "etag was {etag:?}");

    let body = body_json(response).await;
    assert_eq!(body["window_hours"], 72);
    assert!(body["oneliners"].as_array().unwrap().is_empty());
}

#[tokio::test]
async fn recent_case_with_oneliner_shows_up() {
    let (config, dp) = test_config();
    let case_id = "550e8400-e29b-41d4-a716-000000000001";
    seed(
        &dp,
        case_id,
        Duration::from_secs(60),
        Some("Kniegelenk re., V.a. Meniskus"),
    );
    let app = doctate_server::create_router(config);

    let response = app
        .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);
    let body = body_json(response).await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["case_id"], case_id);
    assert_eq!(arr[0]["oneliner"]["kind"], "ready");
    assert_eq!(arr[0]["oneliner"]["text"], "Kniegelenk re., V.a. Meniskus");
    assert!(arr[0]["created_at"].is_string());
    assert!(arr[0]["updated_at"].is_string());
}

#[tokio::test]
async fn case_without_oneliner_shows_null() {
    let (config, dp) = test_config();
    seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000002",
        Duration::from_secs(60),
        None,
    );
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert!(arr[0]["oneliner"].is_null());
}

#[tokio::test]
async fn case_older_than_default_window_excluded() {
    let (config, dp) = test_config();
    seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000003",
        Duration::from_secs(80 * 3600), // 80 h old — outside the 72 h default
        Some("old case"),
    );
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    assert!(body["oneliners"].as_array().unwrap().is_empty());
}

/// Demonstrates that the visibility decision lives in users.toml:
/// the same 40 h-old case is hidden under window_hours = 16 and
/// visible under window_hours = 48.
#[tokio::test]
async fn case_visibility_follows_user_window_hours() {
    let case_id = "550e8400-e29b-41d4-a716-000000000004";
    let age = Duration::from_secs(40 * 3600);

    let (config_small, dp_small) = test_config_with_window_hours(16);
    seed(&dp_small, case_id, age, Some("40h case"));
    let app_small = doctate_server::create_router(config_small);
    let body = body_json(
        app_small
            .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    assert_eq!(body["window_hours"], 16);
    assert!(body["oneliners"].as_array().unwrap().is_empty());

    let (config_wide, dp_wide) = test_config_with_window_hours(48);
    seed(&dp_wide, case_id, age, Some("40h case"));
    let app_wide = doctate_server::create_router(config_wide);
    let body = body_json(
        app_wide
            .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    assert_eq!(body["window_hours"], 48);
    assert_eq!(body["oneliners"].as_array().unwrap().len(), 1);
}

/// Regression guard: a lingering `?hours=N` from an old client is
/// silently ignored — the user-config value drives the response.
#[tokio::test]
async fn hours_query_param_is_ignored() {
    let (config, _dp) = test_config();
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners?hours=999", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    assert_eq!(body["window_hours"], 72);
}

#[tokio::test]
async fn matching_if_none_match_returns_304() {
    let (config, _dp) = test_config();
    let app = doctate_server::create_router(config);

    let first = app
        .clone()
        .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
        .await
        .unwrap();
    assert_eq!(first.status(), StatusCode::OK);
    let etag = header_str(&first, "etag");
    assert!(!etag.is_empty());

    let second = app
        .oneshot(get_with_api_key_if_none_match(
            "/api/oneliners",
            TEST_KEY,
            &etag,
        ))
        .await
        .unwrap();
    assert_eq!(second.status(), StatusCode::NOT_MODIFIED);
    assert_eq!(header_str(&second, "etag"), etag);
}

#[tokio::test]
async fn stale_if_none_match_returns_200() {
    let (config, _dp) = test_config();
    let app = doctate_server::create_router(config);

    let response = app
        .oneshot(get_with_api_key_if_none_match(
            "/api/oneliners",
            TEST_KEY,
            "W/\"99999999-72\"",
        ))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);
}

/// Two users with different `window_hours` must produce different
/// ETag suffixes, even on empty data — so an admin's edit to
/// users.toml invalidates client caches after the next restart.
#[tokio::test]
async fn etag_differs_between_users_with_different_window_hours() {
    let (config_a, _dp_a) = test_config_with_window_hours(16);
    let (config_b, _dp_b) = test_config_with_window_hours(48);

    let app_a = doctate_server::create_router(config_a);
    let app_b = doctate_server::create_router(config_b);

    let r_a = app_a
        .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
        .await
        .unwrap();
    let r_b = app_b
        .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
        .await
        .unwrap();

    let etag_a = header_str(&r_a, "etag");
    let etag_b = header_str(&r_b, "etag");
    assert_ne!(etag_a, etag_b);
    assert!(etag_a.ends_with("-16\""), "etag_a was {etag_a:?}");
    assert!(etag_b.ends_with("-48\""), "etag_b was {etag_b:?}");
}

#[tokio::test]
async fn deleted_case_is_excluded() {
    let (config, dp) = test_config();
    let case_dir = seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000005",
        Duration::from_secs(60),
        Some("deleted case"),
    );
    mark_deleted(&case_dir);
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    assert!(body["oneliners"].as_array().unwrap().is_empty());
}

#[tokio::test]
async fn multiple_cases_sorted_newest_first() {
    let (config, dp) = test_config();
    seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000010",
        Duration::from_secs(2 * 3600),
        Some("older"),
    );
    seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000011",
        Duration::from_secs(30),
        Some("newer"),
    );
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 2);
    assert_eq!(arr[0]["oneliner"]["text"], "newer");
    assert_eq!(arr[1]["oneliner"]["text"], "older");
}

/// Regression guard: if a case was first created long ago but got a
/// follow-up recording today, it must sort ahead of a case whose single
/// (older) recording happens to be newer than the first one. Sorting
/// must use `last_recording_at`, not `created_at`.
#[tokio::test]
async fn sorts_by_last_recording_not_created() {
    let (config, dp) = test_config();

    // Case A: first recording 20h ago (old created_at), second 1 min ago
    // (new last_recording_at).
    let case_a = "550e8400-e29b-41d4-a716-000000000020";
    let dir_a = dp.join(TEST_SLUG).join(case_a);
    std::fs::create_dir_all(&dir_a).unwrap();
    let old_m4a = dir_a.join("2026-04-17T12-00-00Z.m4a");
    std::fs::write(&old_m4a, b"x").unwrap();
    filetime::set_file_mtime(
        &old_m4a,
        FileTime::from_system_time(SystemTime::now() - Duration::from_secs(20 * 3600)),
    )
    .unwrap();
    let fresh_m4a = dir_a.join("2026-04-18T07-59-00Z.m4a");
    std::fs::write(&fresh_m4a, b"x").unwrap();
    filetime::set_file_mtime(
        &fresh_m4a,
        FileTime::from_system_time(SystemTime::now() - Duration::from_secs(60)),
    )
    .unwrap();
    seed_oneliner_ready(&dir_a, "A (fresh addendum)");

    // Case B: single recording 10h ago — created_at newer than A's, but
    // last_recording_at older than A's fresh addendum.
    seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000021",
        Duration::from_secs(10 * 3600),
        Some("B (single, middle-aged)"),
    );

    let app = doctate_server::create_router(config);
    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 2);
    assert_eq!(arr[0]["case_id"], case_a, "A must sort first");
    assert!(arr[0]["last_recording_at"].is_string());
}

#[tokio::test]
async fn empty_state_serializes_as_kind_empty() {
    let (config, dp) = test_config();
    let case_dir = seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000030",
        Duration::from_secs(60),
        None,
    );
    seed_oneliner_state(
        &case_dir,
        &OnelinerState::Empty {
            generated_at: "2026-04-18T10:00:00Z".into(),
        },
    );
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["oneliner"]["kind"], "empty");
    assert!(arr[0]["oneliner"].get("text").is_none());
}

#[tokio::test]
async fn error_state_serializes_as_kind_error() {
    let (config, dp) = test_config();
    let case_dir = seed(
        &dp,
        "550e8400-e29b-41d4-a716-000000000031",
        Duration::from_secs(60),
        None,
    );
    seed_oneliner_state(
        &case_dir,
        &OnelinerState::Error {
            generated_at: "2026-04-18T10:00:00Z".into(),
        },
    );
    let app = doctate_server::create_router(config);

    let body = body_json(
        app.oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap(),
    )
    .await;
    let arr = body["oneliners"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["oneliner"]["kind"], "error");
}
