//! End-to-end theme-store test: a real `solitaire_server` router
//! serving a scanned catalog over a localhost TCP socket, consumed by
//! [`solitaire_data::ThemeStoreClient`].
//!
//! Mirrors the `sync_round_trip` harness: `TcpListener` on port 0,
//! router in a background `tokio::spawn`, no explicit shutdown.

#![cfg(not(target_arch = "wasm32"))]

use std::io::Write as _;
use std::path::{Path, PathBuf};

use solitaire_data::ThemeStoreClient;
use solitaire_server::theme_store::ThemeStore;

/// Minimal theme archive: the catalog scan only reads the `meta`
/// block, so no face SVGs are needed.
fn write_store_zip(dir: &Path, file_name: &str, id: &str, name: &str) -> PathBuf {
    let manifest = format!(
        r#"(
    meta: (
        id: "{id}",
        name: "{name}",
        author: "Round Trip",
        version: "1.0.0",
        card_aspect: (2, 3),
    ),
    faces: {{}},
    back: "back.svg",
)"#
    );
    let path = dir.join(file_name);
    let file = std::fs::File::create(&path).expect("create zip");
    let mut writer = zip::ZipWriter::new(file);
    let options = zip::write::SimpleFileOptions::default();
    writer.start_file("theme.ron", options).expect("start_file");
    writer
        .write_all(manifest.as_bytes())
        .expect("write manifest");
    writer.finish().expect("finish zip");
    path
}

/// Spawn the test server with a theme store scanned from `store_dir`
/// and return its base URL.
async fn spawn_store_server(store_dir: &Path) -> String {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .expect("failed to bind test listener");
    let addr = listener.local_addr().expect("listener has no local addr");

    let pool = solitaire_server::build_test_pool().await;
    let app =
        solitaire_server::build_test_router_with_theme_store(pool, ThemeStore::scan(store_dir));

    tokio::spawn(async move {
        if let Err(e) = axum::serve(listener, app).await {
            eprintln!("test server crashed: {e}");
        }
    });

    format!("http://{addr}")
}

/// Catalog fetch → download → verified bytes match the file the
/// server scanned. This is the exact path the engine's store UI runs.
#[tokio::test]
async fn catalog_fetch_and_verified_download_round_trip() {
    let dir = tempfile::tempdir().expect("tempdir");
    let zip_path = write_store_zip(dir.path(), "neon.zip", "neon", "Neon");
    let base_url = spawn_store_server(dir.path()).await;

    let client = ThemeStoreClient::new(&base_url);
    let catalog = client.fetch_catalog().await.expect("fetch catalog");
    assert_eq!(catalog.len(), 1);
    let entry = &catalog[0];
    assert_eq!(entry.id, "neon");

    let bytes = client.download_theme(entry).await.expect("download");
    assert_eq!(bytes, std::fs::read(&zip_path).expect("read zip"));
}

/// A catalog entry whose hash no longer matches the served file must
/// be rejected client-side — the importer never sees unverified bytes.
#[tokio::test]
async fn download_with_stale_catalog_hash_is_rejected() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_store_zip(dir.path(), "neon.zip", "neon", "Neon");
    let base_url = spawn_store_server(dir.path()).await;

    let client = ThemeStoreClient::new(&base_url);
    let mut entry = client.fetch_catalog().await.expect("fetch catalog")[0].clone();
    entry.sha256 = "0".repeat(64);

    let err = client
        .download_theme(&entry)
        .await
        .expect_err("mismatched hash must fail");
    assert!(
        matches!(
            err,
            solitaire_data::ThemeStoreError::ChecksumMismatch { .. }
        ),
        "expected ChecksumMismatch, got: {err:?}"
    );
}

/// An empty (or absent) store directory serves an empty catalog — the
/// client sees a store with nothing in it, not an error.
#[tokio::test]
async fn empty_store_yields_empty_catalog() {
    let dir = tempfile::tempdir().expect("tempdir");
    let base_url = spawn_store_server(dir.path()).await;

    let client = ThemeStoreClient::new(&base_url);
    let catalog = client.fetch_catalog().await.expect("fetch catalog");
    assert!(catalog.is_empty());
}
