use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};

use lumotia_core::error::{Error, Result};
use lumotia_core::model_registry::{find_model, ModelFile};
use lumotia_core::types::{DownloadProgress, ModelId};

static ACTIVE_DOWNLOADS: LazyLock<Mutex<HashSet<String>>> =
    LazyLock::new(|| Mutex::new(HashSet::new()));

struct DownloadReservation {
    id: String,
}

impl DownloadReservation {
    fn acquire(id: &ModelId) -> Result<Self> {
        let id = id.as_str().to_string();
        let mut active = ACTIVE_DOWNLOADS
            .lock()
            .map_err(|_| Error::DownloadFailed("download lock poisoned".into()))?;
        if !active.insert(id.clone()) {
            return Err(Error::DownloadFailed(format!(
                "download already in progress for {id}"
            )));
        }
        Ok(Self { id })
    }
}

impl Drop for DownloadReservation {
    fn drop(&mut self) {
        if let Ok(mut active) = ACTIVE_DOWNLOADS.lock() {
            active.remove(&self.id);
        }
    }
}

/// Resolve the models storage directory.
/// Windows: %LOCALAPPDATA%/lumotia/models
/// Unix: ~/.lumotia/models
pub fn models_dir() -> PathBuf {
    lumotia_core::paths::app_paths().models_dir()
}

/// Get the directory path where a specific model's files are stored.
pub fn model_dir(id: &ModelId) -> PathBuf {
    lumotia_core::paths::app_paths().speech_model_dir(id)
}

/// Check whether all files for a model have been downloaded.
pub fn is_downloaded(id: &ModelId) -> bool {
    let entry = match find_model(id) {
        Some(e) => e,
        None => return false,
    };
    let dir = model_dir(id);
    entry.files.iter().all(|f| dir.join(f.filename).exists())
        && verified_manifest_matches(entry, &dir)
}

/// List all downloaded model IDs.
pub fn list_downloaded() -> Vec<ModelId> {
    lumotia_core::model_registry::all_models()
        .iter()
        .filter(|m| is_downloaded(&m.id))
        .map(|m| m.id.clone())
        .collect()
}

/// Download all files for a model, calling the progress callback per chunk.
/// Files are downloaded to a .part suffix and atomically renamed on completion.
///
/// For files that declare a `sha256` checksum we validate an existing
/// complete file before skipping the download — a truncated or
/// tampered file gets redownloaded automatically (pattern ported from
/// `lumotia-llm`'s model_manager, item #8 in the Whisper ecosystem brief).
pub async fn download(
    id: &ModelId,
    progress: impl Fn(DownloadProgress) + Send + 'static,
) -> Result<()> {
    let _reservation = DownloadReservation::acquire(id)?;
    let entry = find_model(id).ok_or_else(|| Error::ModelNotFound(id.clone()))?;

    let dir = model_dir(id);
    std::fs::create_dir_all(&dir)?;

    for file in &entry.files {
        let dest = dir.join(file.filename);
        if dest.exists() {
            // Validate the existing file. If the hash doesn't match,
            // the file is corrupt (partial download, tampering, bit
            // rot) and we must re-fetch it to avoid crashing on
            // model load later.
            match sha256_of_file(&dest) {
                Ok(actual) if actual.eq_ignore_ascii_case(file.sha256) => continue,
                Ok(_actual) => {
                    // Rev-5 reversibility kill (atomiser 2026-05-12):
                    // the previous implementation called
                    // `remove_file(&dest)` here before the network
                    // round-trip. A network blip / power loss between
                    // the unlink and `rename(tmp, dest)` in
                    // `download_file` left users with neither the old
                    // (corrupt) model file nor a fresh one — Whisper
                    // models are 1.5–3 GB and the user is offline by
                    // hypothesis.
                    //
                    // We now leave the existing file in place.
                    // `download_file` writes to a `.part` sibling and
                    // only `rename`s over `dest` once the SHA matches.
                    // The bad file is atomically replaced on success;
                    // on failure the user keeps what they had.
                }
                Err(e) => {
                    return Err(Error::DownloadFailed(format!(
                        "failed to verify existing {}: {e}",
                        file.filename
                    )));
                }
            }
        }
        download_file(file, &dest, id, &progress).await?;
    }

    write_verified_manifest(entry, &dir)?;
    Ok(())
}

fn verified_manifest_path(dir: &Path) -> PathBuf {
    dir.join(".lumotia-verified")
}

fn verified_manifest_matches(entry: &lumotia_core::model_registry::ModelEntry, dir: &Path) -> bool {
    let manifest = match std::fs::read_to_string(verified_manifest_path(dir)) {
        Ok(contents) => contents,
        Err(_) => return false,
    };

    for file in &entry.files {
        let path = dir.join(file.filename);
        let size = match std::fs::metadata(&path) {
            Ok(metadata) => metadata.len(),
            Err(_) => return false,
        };
        let expected_line = format!("{}\t{}\t{}", file.filename, file.sha256, size);
        if !manifest.lines().any(|line| line == expected_line) {
            return false;
        }
    }
    true
}

fn write_verified_manifest(
    entry: &lumotia_core::model_registry::ModelEntry,
    dir: &Path,
) -> std::io::Result<()> {
    let mut lines = Vec::with_capacity(entry.files.len() + 1);
    lines.push("version\t1".to_string());
    for file in &entry.files {
        let size = std::fs::metadata(dir.join(file.filename))?.len();
        lines.push(format!("{}\t{}\t{}", file.filename, file.sha256, size));
    }
    let final_path = verified_manifest_path(dir);
    let body = format!("{}\n", lines.join("\n"));

    // Rev-5 atomicity (atomiser 2026-05-12): the previous
    // implementation called `fs::write(final_path, body)` directly,
    // which truncates-then-writes. A power loss or kill mid-write
    // leaves the manifest empty or partial; on next boot
    // `verified_manifest_matches` reports `false` and the loader
    // re-downloads the model even though all the GB-sized files are
    // intact on disk.
    //
    // Write to a sibling `.tmp` first, fsync the data, then `rename`
    // — POSIX `rename` is atomic within a directory, so a reader sees
    // either the old manifest or the new one, never a partial.
    let tmp_path = final_path.with_extension("tmp");
    {
        use std::io::Write;
        let mut tmp = std::fs::File::create(&tmp_path)?;
        tmp.write_all(body.as_bytes())?;
        tmp.flush()?;
        // Best-effort fsync. If the platform/filesystem can't sync
        // (e.g. some test runners on tmpfs), the rename is still
        // atomic at the directory entry level, which is the property
        // we actually care about for "no torn manifest".
        let _ = tmp.sync_all();
    }
    std::fs::rename(&tmp_path, &final_path)
}

/// Non-streaming SHA256 of a file on disk. Used by `download()` to
/// validate an existing complete file before trusting it.
fn sha256_of_file(path: &Path) -> std::io::Result<String> {
    use sha2::{Digest, Sha256};

    let mut hasher = Sha256::new();
    let mut file = std::fs::File::open(path)?;
    let mut buffer = [0u8; 8192];
    loop {
        let n = std::io::Read::read(&mut file, &mut buffer)?;
        if n == 0 {
            break;
        }
        hasher.update(&buffer[..n]);
    }
    Ok(format!("{:x}", hasher.finalize()))
}

/// Download a single file with HTTP Range resume and optional SHA256 verification.
///
/// Resume pattern from Buzz (chidiwilliams/buzz): if a .part file exists,
/// send a Range header to resume from where we left off. SHA256 is checked
/// incrementally during download — no second pass over the file.
async fn download_file(
    file: &ModelFile,
    dest: &Path,
    model_id: &ModelId,
    progress: &(impl Fn(DownloadProgress) + Send),
) -> Result<()> {
    use futures_util::StreamExt;
    use sha2::{Digest, Sha256};

    let part_path = dest.with_extension(
        dest.extension()
            .map(|e| format!("{}.part", e.to_string_lossy()))
            .unwrap_or_else(|| "part".to_string()),
    );

    let client = reqwest::Client::builder()
        .connect_timeout(std::time::Duration::from_secs(30))
        .build()
        .map_err(|e| Error::DownloadFailed(e.to_string()))?;

    // Check for existing partial download (resume support)
    let existing_bytes = if part_path.exists() {
        std::fs::metadata(&part_path).map(|m| m.len()).unwrap_or(0)
    } else {
        0
    };

    let mut request = client.get(file.url);

    let resuming = existing_bytes > 0;
    if resuming {
        request = request.header("Range", format!("bytes={existing_bytes}-"));
    }

    let response = request
        .send()
        .await
        .map_err(|e| Error::DownloadFailed(e.to_string()))?;

    // If we requested Range but the server returned 200 (full file), the
    // server does not support resume. Rather than blindly appending a
    // full file on top of our partial bytes (which would produce a
    // corrupt result), restart cleanly. This mirrors the lumotia-llm
    // ResumeUnsupported branch — item #8 of the brief.
    //
    // For the non-resume path, we still have to validate the status:
    // reqwest does not error on 4xx/5xx by default, so without this
    // check a 404 or 500 would be streamed into `.part` and renamed
    // over the destination as if the download succeeded
    // (2026-04-22 review MAJOR).
    let actually_resuming = if resuming {
        match response.status().as_u16() {
            206 => true,
            200 => {
                // Server ignored our Range header — treat as fresh start.
                // The old .part bytes are discarded below.
                false
            }
            other => {
                return Err(Error::DownloadFailed(format!(
                    "resume request returned unexpected status {other}"
                )));
            }
        }
    } else {
        if !response.status().is_success() {
            return Err(Error::DownloadFailed(format!(
                "download returned HTTP {} for {}",
                response.status(),
                file.filename
            )));
        }
        false
    };

    let total_bytes = if actually_resuming {
        // Content-Range: bytes START-END/TOTAL — extract TOTAL
        response
            .headers()
            .get("content-range")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.rsplit('/').next())
            .and_then(|s| s.parse::<u64>().ok())
            .unwrap_or(0)
    } else {
        response.content_length().unwrap_or(0)
    };

    let mut stream = response.bytes_stream();
    let mut downloaded: u64 = if actually_resuming { existing_bytes } else { 0 };
    let mut last_percent: u8 = 0;

    // Open file for append (resume) or create (fresh start)
    let mut out = if actually_resuming {
        std::fs::OpenOptions::new().append(true).open(&part_path)?
    } else {
        std::fs::File::create(&part_path)?
    };

    let mut hasher = Sha256::new();
    if actually_resuming {
        let mut partial = std::fs::File::open(&part_path)?;
        let mut buffer = [0u8; 8192];
        loop {
            let n = std::io::Read::read(&mut partial, &mut buffer)?;
            if n == 0 {
                break;
            }
            hasher.update(&buffer[..n]);
        }
    }

    while let Some(chunk) = stream.next().await {
        let chunk = chunk.map_err(|e| Error::DownloadFailed(e.to_string()))?;
        std::io::Write::write_all(&mut out, &chunk)?;
        hasher.update(&chunk);
        downloaded += chunk.len() as u64;

        let percent = if total_bytes > 0 {
            ((downloaded as f64 / total_bytes as f64) * 100.0) as u8
        } else {
            0
        };

        if percent != last_percent {
            last_percent = percent;
            progress(DownloadProgress {
                model_id: model_id.clone(),
                file_name: file.filename.to_string(),
                bytes_downloaded: downloaded,
                total_bytes,
                percent,
            });
        }
    }

    drop(out);

    let actual = format!("{:x}", hasher.finalize());
    if actual != file.sha256 {
        let _ = std::fs::remove_file(&part_path);
        return Err(Error::DownloadFailed(format!(
            "SHA256 mismatch for {}: expected {}, got {}",
            file.filename, file.sha256, actual
        )));
    }

    // Atomic rename — file is complete and verified
    std::fs::rename(&part_path, dest)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use sha2::Digest;
    use tempfile::tempdir;
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpListener;

    #[test]
    fn model_dir_returns_correct_path() {
        let id = ModelId::new("whisper-tiny-en");
        let path = model_dir(&id);
        assert!(path.to_string_lossy().contains("whisper-tiny-en"));
    }

    #[test]
    fn is_downloaded_returns_false_for_missing_model() {
        let id = ModelId::new("nonexistent-model");
        assert!(!is_downloaded(&id));
    }

    #[test]
    fn list_downloaded_returns_empty_when_no_models() {
        let list = list_downloaded();
        // In test environment, no models are downloaded
        // This just verifies the function doesn't panic
        assert!(list.len() <= lumotia_core::model_registry::all_models().len());
    }

    #[test]
    fn sha256_of_file_matches_sha2() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("f.bin");
        std::fs::write(&path, b"hello world").unwrap();
        let expected = format!("{:x}", sha2::Sha256::digest(b"hello world"));
        assert_eq!(sha256_of_file(&path).unwrap(), expected);
    }

    /// A minimal HTTP server that sends a Range response when a Range
    /// header is present and otherwise sends the full body. Ported from
    /// crates/llm/src/model_manager.rs to give the transcription
    /// download stack the same fixture-backed coverage.
    async fn spawn_range_server(content: Vec<u8>) -> std::net::SocketAddr {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();

        tokio::spawn(async move {
            let (mut socket, _) = listener.accept().await.unwrap();
            let mut buf = vec![0u8; 2048];
            let size = socket.read(&mut buf).await.unwrap();
            let request = String::from_utf8_lossy(&buf[..size]).to_lowercase();
            let range_start = request
                .lines()
                .find_map(|line| line.strip_prefix("range: bytes="))
                .and_then(|line| line.strip_suffix('-'))
                .and_then(|line| line.trim().parse::<usize>().ok());

            if let Some(start) = range_start {
                let body = &content[start..];
                let response = format!(
                    "HTTP/1.1 206 Partial Content\r\n\
                     Content-Length: {}\r\n\
                     Content-Range: bytes {}-{}/{}\r\n\
                     Accept-Ranges: bytes\r\n\r\n",
                    body.len(),
                    start,
                    content.len() - 1,
                    content.len(),
                );
                socket.write_all(response.as_bytes()).await.unwrap();
                socket.write_all(body).await.unwrap();
            } else {
                let response = format!(
                    "HTTP/1.1 200 OK\r\n\
                     Content-Length: {}\r\n\
                     Accept-Ranges: bytes\r\n\r\n",
                    content.len(),
                );
                socket.write_all(response.as_bytes()).await.unwrap();
                socket.write_all(&content).await.unwrap();
            }
        });

        addr
    }

    /// A minimal HTTP server that responds with 200 + full body **iff**
    /// the request actually carries a `Range` header, and 400 otherwise.
    /// This models a mirror / proxy that accepts Range requests but
    /// refuses to honour them (returning a fresh full body), which is
    /// exactly the ResumeUnsupported branch `download_file` needs to
    /// handle. The 400-on-missing-Range behaviour is load-bearing for
    /// the test: it turns "client never sent Range" into a download
    /// failure, so deleting the resume-detection logic causes the test
    /// to fail rather than pass coincidentally through File::create's
    /// truncation semantics.
    async fn spawn_no_range_server(content: Vec<u8>) -> std::net::SocketAddr {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();

        tokio::spawn(async move {
            let (mut socket, _) = listener.accept().await.unwrap();
            let mut buf = vec![0u8; 2048];
            let size = socket.read(&mut buf).await.unwrap();
            let request = String::from_utf8_lossy(&buf[..size]).to_lowercase();

            let saw_range_header = request
                .lines()
                .any(|line| line.trim_start().starts_with("range:"));

            if !saw_range_header {
                let response = "HTTP/1.1 400 Bad Request\r\n\
                                Content-Length: 0\r\n\r\n";
                socket.write_all(response.as_bytes()).await.unwrap();
                return;
            }

            let response = format!(
                "HTTP/1.1 200 OK\r\n\
                 Content-Length: {}\r\n\r\n",
                content.len(),
            );
            socket.write_all(response.as_bytes()).await.unwrap();
            socket.write_all(&content).await.unwrap();
        });

        addr
    }

    /// ModelFile stores `&'static str` fields, so we leak the strings
    /// once per test — tests are one-shot, so the cost is noise.
    fn leak(s: String) -> &'static str {
        Box::leak(s.into_boxed_str())
    }

    #[tokio::test]
    async fn download_file_resumes_from_partial_and_verifies_sha() {
        let body = b"resumable transcription payload".to_vec();
        let expected_sha = format!("{:x}", sha2::Sha256::digest(&body));
        let addr = spawn_range_server(body.clone()).await;

        let dir = tempdir().unwrap();
        let dest = dir.path().join("fixture.bin");
        let part = dest.with_extension("bin.part");
        // Pretend we already downloaded the first 7 bytes.
        std::fs::write(&part, &body[..7]).unwrap();

        let file = ModelFile {
            filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
            url: leak(format!("http://{addr}/fixture.bin")),
            size: lumotia_core::types::Megabytes(0),
            sha256: leak(expected_sha.clone()),
        };
        let id = ModelId::new("test-fixture");

        download_file(&file, &dest, &id, &|_| ()).await.unwrap();

        let bytes = std::fs::read(&dest).unwrap();
        assert_eq!(bytes, body);
        assert!(!part.exists());
        assert_eq!(sha256_of_file(&dest).unwrap(), expected_sha);
    }

    #[tokio::test]
    async fn download_file_restarts_when_server_ignores_range() {
        // Covers the ResumeUnsupported branch documented in `download_file`:
        // when a partial `.part` file exists and the server returns 200
        // (full body) to our Range request, we must discard the stale
        // partial bytes and write the fresh body from offset zero rather
        // than appending on top.
        let body = b"fresh transcription payload that replaces any stale partial".to_vec();
        let expected_sha = format!("{:x}", sha2::Sha256::digest(&body));
        let addr = spawn_no_range_server(body.clone()).await;

        let dir = tempdir().unwrap();
        let dest = dir.path().join("fixture.bin");
        let part = dest.with_extension("bin.part");
        // Pretend a previous attempt downloaded 12 bytes of something
        // entirely unrelated. If the client naively appended the 200
        // body, the final file would start with these bytes.
        std::fs::write(&part, b"STALE_BYTES1").unwrap();

        let file = ModelFile {
            filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
            url: leak(format!("http://{addr}/fixture.bin")),
            size: lumotia_core::types::Megabytes(0),
            sha256: leak(expected_sha),
        };
        let id = ModelId::new("test-fixture");

        download_file(&file, &dest, &id, &|_| ()).await.unwrap();

        let bytes = std::fs::read(&dest).unwrap();
        assert_eq!(
            bytes, body,
            "server returned 200 to Range — downloader must discard stale .part and rewrite from scratch"
        );
        assert!(!part.exists(), ".part → dest rename must run after restart");
    }

    /// Always returns HTTP 500 with a short error body. Used to verify
    /// the non-resume download path validates status codes rather than
    /// writing error bodies into `.part` and renaming them over the
    /// destination.
    async fn spawn_500_server() -> std::net::SocketAddr {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();

        tokio::spawn(async move {
            let (mut socket, _) = listener.accept().await.unwrap();
            let mut buf = vec![0u8; 2048];
            let _ = socket.read(&mut buf).await.unwrap();
            let body = b"internal error";
            let response = format!(
                "HTTP/1.1 500 Internal Server Error\r\n\
                 Content-Length: {}\r\n\r\n",
                body.len()
            );
            socket.write_all(response.as_bytes()).await.unwrap();
            socket.write_all(body).await.unwrap();
        });

        addr
    }

    #[tokio::test]
    async fn download_file_rejects_5xx_on_non_resume_path() {
        // Regression for the 2026-04-22 review: reqwest does not
        // auto-error on 4xx/5xx, and the non-resume branch previously
        // streamed any status' body into `.part` and renamed it over
        // the destination.
        let addr = spawn_500_server().await;

        let dir = tempdir().unwrap();
        let dest = dir.path().join("fixture.bin");
        let part = dest.with_extension("bin.part");

        let file = ModelFile {
            filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
            url: leak(format!("http://{addr}/fixture.bin")),
            size: lumotia_core::types::Megabytes(0),
            sha256: leak("0".repeat(64)),
        };
        let id = ModelId::new("test-fixture");

        let err = download_file(&file, &dest, &id, &|_| ())
            .await
            .expect_err("5xx must fail");
        let msg = err.to_string();
        assert!(
            msg.contains("HTTP 500"),
            "error should name the HTTP status, got: {msg}"
        );
        assert!(!dest.exists(), "5xx must not leave a destination file");
        assert!(!part.exists(), "5xx must not leave a .part file");
    }

    #[tokio::test]
    async fn download_file_fails_on_sha_mismatch_and_cleans_part_file() {
        let body = b"speech-to-text fixture body".to_vec();
        let addr = spawn_range_server(body.clone()).await;

        let dir = tempdir().unwrap();
        let dest = dir.path().join("fixture.bin");

        let file = ModelFile {
            filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
            url: leak(format!("http://{addr}/fixture.bin")),
            size: lumotia_core::types::Megabytes(0),
            sha256: leak("deadbeef".repeat(8)),
        };
        let id = ModelId::new("test-fixture");

        let err = download_file(&file, &dest, &id, &|_| ())
            .await
            .expect_err("mismatched sha must fail");
        let msg = err.to_string();
        assert!(msg.contains("SHA256 mismatch"), "unexpected error: {msg}");
        assert!(
            !dest.exists(),
            ".part → dest rename must not run on mismatch"
        );
        let part = dest.with_extension("bin.part");
        assert!(!part.exists(), "failed hash must clean up the .part file");
    }

    /// Rev-5 regression (atomiser 2026-05-12). The previous `download()`
    /// loop called `remove_file(&dest)` on SHA mismatch BEFORE the
    /// network call, so a failing redownload left the user with
    /// neither the old (corrupt-but-readable) Whisper model nor a
    /// fresh one — 1.5–3 GB redownload over an already-flaky network.
    ///
    /// `download_file` is the per-file primitive that gets invoked
    /// after the outer-loop decides to redownload. It already writes
    /// via `.part` and atomically renames. This test asserts the
    /// invariant the fix relies on: when `download_file` fails (5xx
    /// or SHA mismatch), an existing sentinel file at `dest` is left
    /// in place. The outer-loop no longer deletes pre-emptively, so
    /// this proves end-to-end: a failed redownload preserves the old
    /// file.
    #[tokio::test]
    async fn download_file_failure_preserves_existing_dest_file() {
        let addr = spawn_500_server().await;

        let dir = tempdir().unwrap();
        let dest = dir.path().join("fixture.bin");
        // Existing "old model" file the user already had on disk.
        std::fs::write(&dest, b"OLD").unwrap();

        let file = ModelFile {
            filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
            url: leak(format!("http://{addr}/fixture.bin")),
            size: lumotia_core::types::Megabytes(0),
            sha256: leak("0".repeat(64)),
        };
        let id = ModelId::new("test-fixture");

        let _ = download_file(&file, &dest, &id, &|_| ())
            .await
            .expect_err("5xx must fail");

        assert!(
            dest.exists(),
            "download failure must leave the existing dest in place"
        );
        let preserved = std::fs::read(&dest).unwrap();
        assert_eq!(
            preserved, b"OLD",
            "existing file contents must be untouched on failed download"
        );
    }

    /// Rev-5 atomic-manifest regression (atomiser 2026-05-12). The
    /// previous `write_verified_manifest` called `fs::write` directly,
    /// which truncates-then-writes. A power loss mid-write left an
    /// empty or torn manifest; on next boot the loader couldn't match
    /// the existing GB-sized model files and redownloaded everything.
    ///
    /// The fix writes to a sibling `.tmp` first then `rename`s — POSIX
    /// `rename` is atomic. This test exercises that path: write a
    /// valid manifest, then prove a stale `.tmp` left behind from a
    /// crashed previous attempt does NOT survive a fresh write (the
    /// rename replaces it), and that the final manifest matches.
    #[test]
    fn manifest_write_is_atomic() {
        use lumotia_core::model_registry::{
            AccuracyTier, Engine, LanguageSupport, ModelEntry, ModelFile, SpeedTier,
        };
        use lumotia_core::types::Megabytes;
        use sha2::Digest;

        let dir = tempdir().unwrap();
        let file_path = dir.path().join("model.bin");
        std::fs::write(&file_path, b"payload").unwrap();

        let entry = ModelEntry {
            id: ModelId::new("test-manifest"),
            engine: Engine::Whisper,
            display_name: "test",
            disk_size: Megabytes(0),
            ram_required: Megabytes(0),
            speed_tier: SpeedTier::Instant,
            accuracy_tier: AccuracyTier::Great,
            languages: LanguageSupport::EnglishOnly,
            files: vec![ModelFile {
                filename: leak(
                    file_path
                        .file_name()
                        .unwrap()
                        .to_string_lossy()
                        .into_owned(),
                ),
                url: "http://example.invalid/model.bin",
                size: Megabytes(0),
                sha256: leak(format!("{:x}", sha2::Sha256::digest(b"payload"))),
            }],
            description: "",
        };

        let manifest_path = verified_manifest_path(dir.path());
        let tmp_path = manifest_path.with_extension("tmp");

        // Simulate a crashed previous write leaving a stale .tmp.
        std::fs::write(&tmp_path, b"PARTIAL_GARBAGE").unwrap();
        assert!(tmp_path.exists());

        write_verified_manifest(&entry, dir.path()).expect("write manifest");

        assert!(
            manifest_path.exists(),
            "final manifest must exist after atomic write"
        );
        assert!(!tmp_path.exists(), "stale .tmp must be removed by rename");

        let body = std::fs::read_to_string(&manifest_path).unwrap();
        assert!(body.starts_with("version\t1"));
        assert!(
            body.ends_with('\n'),
            "manifest must be flushed completely with terminating newline"
        );
        assert!(
            verified_manifest_matches(&entry, dir.path()),
            "manifest written via tmp+rename must round-trip verify"
        );
    }
}
