//! Per-recording delete endpoint: POST /web/cases/{case_id}/recordings/delete.
//!
//! Verifies the endpoint:
//! - removes the audio file and its `<stem>.json` metadata sidecar
//! - invalidates derived artefacts (oneliner.json, document.md, analysis_input.json)
//! - leaves other recordings in the same case untouched
//! - rejects path-traversal filenames with 400
//! - enforces case ownership (returns 404 on cross-user access)

mod common;

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

use common::{
    ANALYSIS_INPUT_FILE, DOCUMENT_FILE, ONELINER_FILENAME, TestConfig, csrf_form_post,
    login_with_csrf, paths, seed_case, seed_oneliner_ready, seed_oneliner_state,
    seed_recording_with_sidecars, test_user,
};

fn delete_body(filename: &str) -> String {
    format!("filename={filename}")
}

#[tokio::test]
async fn delete_recording_removes_file_and_sidecars_and_invalidates_derived() {
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);

    let victim = seed_recording_with_sidecars(&case_dir, "2026-04-19T10-00-00Z", "delete me");
    let survivor = seed_recording_with_sidecars(&case_dir, "2026-04-19T11-00-00Z", "keep me");

    // Derived artefacts the delete must also purge. The oneliner is
    // seeded as `Ready` (a real LLM-derived state) so the new sticky-
    // Manual wrapper sees a non-Manual variant and proceeds with the
    // delete — keeps the original contract „auto state must be wiped".
    seed_oneliner_ready(&case_dir, "knee, left, pain");
    std::fs::write(case_dir.join(DOCUMENT_FILE), b"# stale").unwrap();
    std::fs::write(case_dir.join(ANALYSIS_INPUT_FILE), b"{}").unwrap();

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body(&victim),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status().as_u16() / 100,
        3,
        "delete should redirect (3xx), got {}",
        resp.status()
    );
    let expected_location = format!("/web/cases/{case_id}/recordings");
    assert_eq!(
        resp.headers()
            .get(axum::http::header::LOCATION)
            .and_then(|v| v.to_str().ok()),
        Some(expected_location.as_str()),
        "delete should redirect back to the recording list, not the case list"
    );

    // Victim + its sidecar are gone.
    assert!(!case_dir.join(&victim).exists(), "m4a should be deleted");
    assert!(
        !case_dir.join("2026-04-19T10-00-00Z.json").exists(),
        "metadata sidecar should be deleted"
    );

    // Survivor is intact.
    assert!(case_dir.join(&survivor).exists(), "other m4a must remain");
    assert!(
        case_dir.join("2026-04-19T11-00-00Z.json").exists(),
        "other metadata sidecar must remain"
    );

    // Derived artefacts are invalidated.
    assert!(
        !case_dir.join(ONELINER_FILENAME).exists(),
        "oneliner.json must be cleared"
    );
    assert!(
        !case_dir.join(DOCUMENT_FILE).exists(),
        "document.md must be cleared"
    );
    assert!(
        !case_dir.join(ANALYSIS_INPUT_FILE).exists(),
        "analysis_input.json must be cleared"
    );
}

#[tokio::test]
async fn delete_recording_rejects_path_traversal() {
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "22222222-2222-2222-2222-222222222222";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording_with_sidecars(&case_dir, "2026-04-19T10-00-00Z", "safe");

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body("../../../etc/passwd.m4a"),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status(),
        StatusCode::BAD_REQUEST,
        "path traversal must be rejected"
    );

    // Safe recording is untouched.
    assert!(case_dir.join("2026-04-19T10-00-00Z.m4a").exists());
}

#[tokio::test]
async fn delete_recording_rejects_wrong_extension() {
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "33333333-3333-3333-3333-333333333333";
    seed_case(&cfg.data_path, "dr_a", case_id);

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body(DOCUMENT_FILE),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status(),
        StatusCode::BAD_REQUEST,
        "non-audio extension must be rejected"
    );
}

#[tokio::test]
async fn delete_recording_cross_user_returns_404() {
    // User B owns the case; User A tries to delete a recording in it.
    let cfg = TestConfig::new()
        .with_user(test_user("dr_a"))
        .with_user(test_user("dr_b"))
        .build();
    let case_id = "44444444-4444-4444-4444-444444444444";
    let case_dir = seed_case(&cfg.data_path, "dr_b", case_id);
    let filename =
        seed_recording_with_sidecars(&case_dir, "2026-04-19T10-00-00Z", "bob's recording");

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie_a, csrf_a) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie_a,
            &csrf_a,
            &delete_body(&filename),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status(),
        StatusCode::NOT_FOUND,
        "cross-user access must return 404, not 403 (IDOR hardening)"
    );
    assert!(
        case_dir.join(&filename).exists(),
        "Bob's file must not have been touched"
    );
}

#[tokio::test]
async fn delete_last_recording_clears_case() {
    // Deleting the only recording must succeed, not error out.
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "55555555-5555-5555-5555-555555555555";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    let only = seed_recording_with_sidecars(&case_dir, "2026-04-19T10-00-00Z", "lonely");
    std::fs::write(case_dir.join(ONELINER_FILENAME), b"{}").unwrap();

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body(&only),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status().as_u16() / 100,
        3,
        "last-recording delete should still redirect, got {}",
        resp.status()
    );
    let expected_location = format!("/web/cases/{case_id}/recordings");
    assert_eq!(
        resp.headers()
            .get(axum::http::header::LOCATION)
            .and_then(|v| v.to_str().ok()),
        Some(expected_location.as_str()),
        "last-recording delete should still redirect to the recording list"
    );

    assert!(!case_dir.join(&only).exists());
    assert!(!case_dir.join(ONELINER_FILENAME).exists());
}

#[tokio::test]
async fn delete_recording_accepts_failed_suffix() {
    // Recordings whose transcription failed are renamed to <ts>.m4a.failed.
    // The delete button is shown for them too — they should be deletable.
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "66666666-6666-6666-6666-666666666666";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    std::fs::write(case_dir.join("2026-04-19T10-00-00Z.m4a.failed"), b"x").unwrap();

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body("2026-04-19T10-00-00Z.m4a.failed"),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status().as_u16() / 100, 3);
    assert!(!case_dir.join("2026-04-19T10-00-00Z.m4a.failed").exists());
}

#[tokio::test]
async fn delete_recording_preserves_manual_oneliner_override() {
    // Bug regression: deleting a recording must NOT wipe a clinician's
    // manual oneliner override. The Manual variant is sticky for the
    // lifetime of the case — only `purge-closed` (whole-case delete)
    // is allowed to remove it.
    let cfg = TestConfig::new().with_user(test_user("dr_a")).build();
    let case_id = "77777777-7777-7777-7777-777777777777";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);

    let victim = seed_recording_with_sidecars(&case_dir, "2026-04-26T10-00-00Z", "raw text");
    seed_recording_with_sidecars(&case_dir, "2026-04-26T11-00-00Z", "more raw text");

    let manual_text = "55 J., Knieschmerz links — manuell vom Arzt";
    seed_oneliner_state(
        &case_dir,
        &OnelinerState::Manual {
            text: manual_text.into(),
            set_at: "2026-04-26T12:00:00Z".into(),
        },
    );

    let (app, store) = doctate_server::create_router_and_session_store(cfg);
    let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await;

    let resp = app
        .clone()
        .oneshot(csrf_form_post(
            paths::recording_delete(case_id),
            &cookie,
            &csrf,
            &delete_body(&victim),
        ))
        .await
        .unwrap();
    assert_eq!(
        resp.status().as_u16() / 100,
        3,
        "delete should still redirect on success"
    );

    // Recording is gone…
    assert!(!case_dir.join(&victim).exists(), "victim m4a deleted");
    // …but the Manual override is untouched.
    let bytes = std::fs::read(case_dir.join(ONELINER_FILENAME))
        .expect("oneliner.json must still exist after recording delete");
    let on_disk: OnelinerState =
        serde_json::from_slice(&bytes).expect("oneliner.json must still be valid JSON");
    match on_disk {
        OnelinerState::Manual { text, .. } => {
            assert_eq!(text, manual_text, "manual text must be byte-identical");
        }
        other => panic!("expected Manual after recording delete, got {other:?}"),
    }
}
