//! Filesystem-fixture builders for case directories, recordings,
//! oneliner state files, and close markers.
//!
//! All functions use synchronous `std::fs` (tests run inside
//! `#[tokio::test]` but these writes are short and their output is
//! consumed sequentially, so there is no win from async I/O here). A
//! few of the oneliner_api tests use `tokio::fs` directly because they
//! need to run inside the same async flow; that's fine — the two
//! coexist.

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

use doctate_common::oneliners::OnelinerState;
use doctate_common::{ONELINER_FILENAME, RECORDING_META_SUFFIX, RecordingMeta, Transcript};
use doctate_server::paths::CLOSE_MARKER;
use filetime::FileTime;

/// Create `<data_path>/<slug>/<case_id>/` and return its path. This is
/// the only "has to be on disk for the route to see it" fixture —
/// recordings and derived artefacts layer on top.
pub fn seed_case(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
    let dir = data_path.join(slug).join(case_id);
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// Write `<stem>.m4a` with placeholder bytes, plus optionally a
/// `<stem>.json` sidecar that mirrors the transcribe outcome.
///
/// `transcript`:
/// - `None` → no sidecar written (recording stays in `Pending` state).
/// - `Some(text)` with whitespace-only `text` → sidecar with
///   `Transcript::Silent` (mirrors the worker's classification rule).
/// - `Some(text)` non-empty → sidecar with `Transcript::Content`.
///
/// Returns the audio filename (no path) so the caller can feed it back
/// to the delete endpoint.
pub fn seed_recording(case_dir: &Path, stem: &str, transcript: Option<&str>) -> String {
    let filename = format!("{stem}.m4a");
    std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap();
    if let Some(text) = transcript {
        seed_recording_meta(case_dir, stem, transcript_for(text), None);
    }
    filename
}

/// Like [`seed_recording`] but the sidecar carries a real duration
/// (`duration_seconds: Some(42)`). Used by tests that assert the
/// duration is rendered or cleaned up.
pub fn seed_recording_with_sidecars(case_dir: &Path, stem: &str, transcript: &str) -> String {
    let filename = format!("{stem}.m4a");
    std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap();
    seed_recording_meta(case_dir, stem, transcript_for(transcript), Some(42));
    filename
}

/// Write a fully-specified `<stem>.json` next to an already-seeded
/// `<stem>.m4a`. Use when a test needs precise control over the
/// transcript variant or duration; the higher-level helpers above
/// cover the common cases.
pub fn seed_recording_meta(
    case_dir: &Path,
    stem: &str,
    transcript: Transcript,
    duration_seconds: Option<u32>,
) {
    let meta = RecordingMeta {
        transcript,
        duration_seconds,
        loudness: None,
    };
    let bytes = serde_json::to_vec(&meta).unwrap();
    std::fs::write(
        case_dir.join(format!("{stem}.{RECORDING_META_SUFFIX}")),
        bytes,
    )
    .unwrap();
}

/// Map a free-text transcript to the `Transcript` variant the worker
/// would have produced for it: whitespace-only → `Silent`, otherwise
/// `Content`. Matches `worker::run`'s post-Whisper classification.
fn transcript_for(text: &str) -> Transcript {
    if text.trim().is_empty() {
        Transcript::Silent
    } else {
        Transcript::Content {
            text: text.to_owned(),
        }
    }
}

/// Write an m4a with an artificial `mtime` in the past (relative to
/// now). Used by the retention-sweep tests to simulate "recorded N days
/// ago" without a clock abstraction.
pub fn seed_recording_with_age(case_dir: &Path, stem: &str, age: Duration) {
    let filename = format!("{stem}.m4a");
    let path = case_dir.join(&filename);
    std::fs::write(&path, b"audio").unwrap();
    let target = SystemTime::now() - age;
    filetime::set_file_mtime(&path, FileTime::from_system_time(target)).unwrap();
}

/// Persist `OnelinerState::Ready { text }` with a fixed `generated_at`.
/// Used by UI-rendering tests that care about the displayed text, not
/// the timestamp.
pub fn seed_oneliner_ready(case_dir: &Path, text: &str) {
    let state = OnelinerState::Ready {
        text: text.to_owned(),
        generated_at: "2026-04-18T10:00:00Z".into(),
    };
    seed_oneliner_state(case_dir, &state);
}

/// Persist an arbitrary [`OnelinerState`] as `oneliner.json`.
pub fn seed_oneliner_state(case_dir: &Path, state: &OnelinerState) {
    let bytes = serde_json::to_vec(state).unwrap();
    std::fs::write(case_dir.join(ONELINER_FILENAME), bytes).unwrap();
}

/// Write a `.closed` marker with a caller-chosen `closed_at` string.
/// Tests that need a specific age pass a historical RFC3339 stamp; a
/// helper for that is [`past_rfc3339`].
pub fn write_closed_marker(case_dir: &Path, closed_at: &str) {
    let json = format!(r#"{{"closed_at":"{closed_at}"}}"#);
    std::fs::write(case_dir.join(CLOSE_MARKER), json).unwrap();
}

/// Format "now - age" as RFC3339 UTC. Used to mint historical
/// `closed_at` strings for retention-purge tests.
pub fn past_rfc3339(age: Duration) -> String {
    use time::format_description::well_known::Rfc3339;
    let t = time::OffsetDateTime::now_utc() - time::Duration::seconds(age.as_secs() as i64);
    t.format(&Rfc3339).unwrap()
}
