mod common;

use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::util::ServiceExt;

use doctate_common::UPLOAD_PATH;
use doctate_server::paths::{CLOSE_MARKER, CloseMarker, write_close_marker};

use common::{
    TestConfig, body_json, get_with_api_key, get_with_api_key_if_none_match, header_str,
    multipart_upload_body, test_user,
};

const TEST_KEY: &str = "key-dr_test";

fn test_config() -> std::sync::Arc<doctate_server::config::Config> {
    TestConfig::new()
        .with_label("upload")
        .with_user(test_user("dr_test"))
        .build()
}

/// Wrap an `(content_type, body)` pair into the actual HTTP upload request.
/// Keeps the multipart boundary and the audio-upload path in sync for
/// every test in this file.
fn upload_request(content_type: &str, body: Vec<u8>, api_key: Option<&str>) -> Request<Body> {
    let mut b = Request::builder()
        .method("POST")
        .uri(UPLOAD_PATH)
        .header("Content-Type", content_type);
    if let Some(k) = api_key {
        b = b.header("X-API-Key", k);
    }
    b.body(Body::from(body)).unwrap()
}

#[tokio::test]
async fn upload_creates_new_case() {
    let config = test_config();
    let data_path = config.data_path.clone();
    let app = doctate_server::create_router(config);

    let case_id = "550e8400-e29b-41d4-a716-446655440000";
    let (content_type, body) =
        multipart_upload_body(case_id, "2026-04-13T10:30:00Z", b"fake audio data");

    let response = app
        .oneshot(upload_request(&content_type, body, Some(TEST_KEY)))
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);

    let json = body_json(response).await;
    assert_eq!(json["case_id"], case_id);
    assert_eq!(json["status"], "received");

    // Verify file on disk
    let file = data_path
        .join("dr_test")
        .join(case_id)
        .join("2026-04-13T10-30-00Z.m4a");
    assert!(file.exists(), "Audio file should exist at {file:?}");
}

#[tokio::test]
async fn upload_invalid_case_id_returns_400() {
    let app = doctate_server::create_router(test_config());

    let (content_type, body) =
        multipart_upload_body("../etc/passwd", "2026-04-13T10:30:00Z", b"audio");

    let response = app
        .oneshot(upload_request(&content_type, body, Some(TEST_KEY)))
        .await
        .unwrap();

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

#[tokio::test]
async fn upload_without_auth_returns_401() {
    let app = doctate_server::create_router(test_config());

    let (content_type, body) = multipart_upload_body(
        "550e8400-e29b-41d4-a716-446655440000",
        "2026-04-13T10:30:00Z",
        b"audio",
    );

    let response = app
        .oneshot(upload_request(&content_type, body, None))
        .await
        .unwrap();

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

#[tokio::test]
async fn upload_empty_audio_returns_400() {
    let app = doctate_server::create_router(test_config());

    let (content_type, body) = multipart_upload_body(
        "550e8400-e29b-41d4-a716-446655440000",
        "2026-04-13T10:30:00Z",
        b"", // empty audio
    );

    let response = app
        .oneshot(upload_request(&content_type, body, Some(TEST_KEY)))
        .await
        .unwrap();

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

#[tokio::test]
async fn upload_second_recording_same_case() {
    let config = test_config();
    let data_path = config.data_path.clone();
    let app = doctate_server::create_router(config);

    let case_id = "660e8400-e29b-41d4-a716-446655440000";

    // First upload
    let (ct, body) = multipart_upload_body(case_id, "2026-04-13T10:30:00Z", b"first recording");
    let response = app
        .clone()
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Second upload, same case, different timestamp
    let (ct, body) = multipart_upload_body(case_id, "2026-04-13T10:45:00Z", b"second recording");
    let response = app
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Both files should exist
    let case_dir = data_path.join("dr_test").join(case_id);
    assert!(case_dir.join("2026-04-13T10-30-00Z.m4a").exists());
    assert!(case_dir.join("2026-04-13T10-45-00Z.m4a").exists());
}

/// Scenario A: a new upload to a *closed* case must silently remove
/// the `.closed` marker and store the new audio. Reopens the case so
/// it reappears in /api/oneliners without any manual "undo".
#[tokio::test]
async fn upload_reopens_closed_case() {
    let config = test_config();
    let data_path = config.data_path.clone();
    let app = doctate_server::create_router(config);

    let case_id = "770e8400-e29b-41d4-a716-446655440000";

    // First upload — creates the case directory.
    let (ct, body) = multipart_upload_body(case_id, "2026-04-13T10:30:00Z", b"first recording");
    let response = app
        .clone()
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Close the case by writing the marker directly — simulates the
    // user hitting the close button in the web UI without having to
    // drive the /web/cases/{id}/delete handler here. Direct write also
    // means the watermark is NOT bumped here; that way the next GET
    // /api/oneliners pins down the pre-reopen ETag cleanly.
    let case_dir = data_path.join("dr_test").join(case_id);
    let marker = CloseMarker {
        closed_at: "2026-04-13T11:00:00Z".into(),
    };
    write_close_marker(&case_dir, &marker).await.unwrap();
    assert!(case_dir.join(CLOSE_MARKER).exists());

    // Capture the ETag before the reopen.
    let etag_before = {
        let resp = app
            .clone()
            .oneshot(get_with_api_key("/api/oneliners", TEST_KEY))
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let t = header_str(&resp, "etag");
        assert!(!t.is_empty(), "server must set ETag");
        t
    };

    // Second upload — must auto-reopen.
    let (ct, body) = multipart_upload_body(case_id, "2026-04-13T12:00:00Z", b"reopen recording");
    let response = app
        .clone()
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Marker is gone → case is visible again.
    assert!(
        !case_dir.join(CLOSE_MARKER).exists(),
        ".closed marker must be removed after re-upload"
    );
    // Both audios are on disk — the old one was not touched.
    assert!(case_dir.join("2026-04-13T10-30-00Z.m4a").exists());
    assert!(case_dir.join("2026-04-13T12-00-00Z.m4a").exists());

    // Auto-reopen must bump the watermark: sending the pre-reopen
    // If-None-Match must return 200 (not 304) so clients who were
    // offline during the reopen notice it on their next poll.
    let resp = app
        .oneshot(get_with_api_key_if_none_match(
            "/api/oneliners",
            TEST_KEY,
            &etag_before,
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "auto-reopen must invalidate the pre-reopen ETag"
    );
    let etag_after = header_str(&resp, "etag");
    assert!(!etag_after.is_empty(), "server must set ETag");
    assert_ne!(etag_after, etag_before);
}

/// Scenario B: a new upload to a case whose server-side directory has
/// been *hard-deleted* (e.g. by a future retention sweep or an
/// out-of-band cleanup) must transparently re-create the directory and
/// accept the audio. This guards the forward-looking scenario where the
/// client was offline long enough that the server has already purged
/// the case.
#[tokio::test]
async fn upload_resurrects_hard_deleted_case() {
    let config = test_config();
    let data_path = config.data_path.clone();
    let app = doctate_server::create_router(config);

    let case_id = "880e8400-e29b-41d4-a716-446655440000";

    // First upload.
    let (ct, body) = multipart_upload_body(case_id, "2026-04-13T10:30:00Z", b"original recording");
    let response = app
        .clone()
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Wipe the case directory entirely — no marker, no audio, nothing.
    let case_dir = data_path.join("dr_test").join(case_id);
    std::fs::remove_dir_all(&case_dir).unwrap();
    assert!(!case_dir.exists());

    // Second upload with the same client-generated UUID.
    let (ct, body) =
        multipart_upload_body(case_id, "2026-04-13T12:00:00Z", b"resurrected recording");
    let response = app
        .oneshot(upload_request(&ct, body, Some(TEST_KEY)))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::OK);

    // Directory is back, new audio is inside, old audio stays gone.
    assert!(case_dir.exists());
    assert!(case_dir.join("2026-04-13T12-00-00Z.m4a").exists());
    assert!(!case_dir.join("2026-04-13T10-30-00Z.m4a").exists());
}
