//! Infrastructure tests for local model-server adapters.

use std::path::PathBuf;
use std::sync::Arc;

use domain::model_server::{
    ExecutablePath, HfModelRef, LlamaCppOptions, LocalModelRef, LocalModelServerConfig,
    LocalModelServerKind, ModelPath, ModelServerEndpoint, ModelSource, StopPolicy,
};
use domain::ports::{
    FileSystem, ModelArtifactCancel, ModelArtifactDownloader, ModelArtifactState,
    ModelServerRegistry, ModelServerRuntime, RemotePath,
};
use domain::LocalModelServerId;
use infrastructure::{
    FsModelServerRegistry, HfModelArtifactDownloader, LlamaCppRuntime, LocalFileSystem,
};
use uuid::Uuid;

struct TempDir(PathBuf);

impl TempDir {
    fn new() -> Self {
        let path = std::env::temp_dir().join(format!("idea-model-server-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&path).unwrap();
        Self(path)
    }

    fn app_data_dir(&self) -> String {
        self.0.to_string_lossy().into_owned()
    }

    fn child(&self, name: &str) -> RemotePath {
        RemotePath::new(self.0.join(name).to_string_lossy().into_owned())
    }

    fn path(&self) -> &std::path::Path {
        &self.0
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

fn sid(n: u128) -> LocalModelServerId {
    LocalModelServerId::from_uuid(Uuid::from_u128(n))
}

fn config(id: LocalModelServerId, port: u16) -> LocalModelServerConfig {
    let binary = std::env::current_exe()
        .unwrap()
        .to_string_lossy()
        .into_owned();
    LocalModelServerConfig::new(
        id,
        LocalModelServerKind::LlamaCpp,
        "Local Qwen",
        ModelServerEndpoint::new(format!("http://localhost:{port}/v1"), port).unwrap(),
        LocalModelRef::new(
            "qwen",
            "Qwen",
            Some(ModelSource::LocalPath {
                path: ModelPath::new("/models/qwen.gguf").unwrap(),
            }),
            "qwen3-coder-30b",
        )
        .unwrap(),
        Some(ExecutablePath::new(binary).unwrap()),
        LlamaCppOptions::new("127.0.0.1", Some(35), Some(8192), true).unwrap(),
        vec!["--threads".to_owned(), "8".to_owned()],
        true,
        StopPolicy::StopOnAppExit,
    )
    .unwrap()
}

#[test]
fn llamacpp_runtime_builds_structured_argv() {
    let argv = LlamaCppRuntime::new()
        .build_argv(&config(sid(1), 8080))
        .unwrap();

    assert_eq!(
        argv.command,
        std::env::current_exe().unwrap().to_string_lossy()
    );
    assert_eq!(
        argv.args,
        vec![
            "--model",
            "/models/qwen.gguf",
            "--port",
            "8080",
            "--host",
            "127.0.0.1",
            "-ngl",
            "35",
            "-c",
            "8192",
            "--jinja",
            "--threads",
            "8"
        ]
    );
}

#[test]
fn llamacpp_runtime_builds_hf_argv() {
    let mut config = config(sid(3), 8083);
    config.model.source = Some(ModelSource::HuggingFace {
        repo: HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap(),
    });
    config.options = LlamaCppOptions::default();
    config.args = Vec::new();

    let spec = LlamaCppRuntime::new().build_spawn_spec(&config).unwrap();

    assert_eq!(
        spec.args,
        vec![
            "-hf",
            "Qwen/Qwen3-Coder:Q4_K_M",
            "--port",
            "8083",
            "--host",
            "127.0.0.1"
        ]
    );
    assert!(spec.context_plan.is_none());
}

#[tokio::test]
async fn fs_model_server_registry_roundtrips_global_json() {
    let tmp = TempDir::new();
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir());

    let first = config(sid(2), 8081)
        .with_warmup_deadline_secs(Some(900))
        .unwrap();
    registry.save(first.clone()).await.unwrap();

    assert_eq!(registry.get(&first.id).await.unwrap(), Some(first.clone()));
    assert_eq!(registry.list().await.unwrap(), vec![first.clone()]);
    assert!(
        fs.exists(&tmp.child("model-servers.json")).await.unwrap(),
        "registry must live in global app-data dir"
    );

    let updated = config(sid(2), 8082);
    registry.save(updated.clone()).await.unwrap();
    assert_eq!(registry.list().await.unwrap(), vec![updated]);
}

#[tokio::test]
async fn fs_model_server_registry_migrates_v1_json_to_v2() {
    let tmp = TempDir::new();
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    fs.create_dir_all(&RemotePath::new(tmp.app_data_dir()))
        .await
        .unwrap();
    fs.write(
        &tmp.child("model-servers.json"),
        br#"{
  "version": 1,
  "servers": [{
    "id": "00000000-0000-0000-0000-000000000004",
    "kind": "llamaCpp",
    "name": "Legacy Qwen",
    "endpoint": { "baseURL": "http://localhost:8084/v1", "port": 8084 },
    "model": {
      "id": "qwen",
      "label": "Qwen",
      "path": "/models/qwen.gguf",
      "servedName": "qwen3-coder"
    },
    "binary": "/usr/bin/llama-server",
    "args": ["--ctx-size", "4096"],
    "autoStart": true,
    "stopPolicy": "stopOnAppExit"
  }]
}"#,
    )
    .await
    .unwrap();
    let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir());

    let servers = registry.list().await.unwrap();

    assert_eq!(servers.len(), 1);
    assert_eq!(servers[0].options, LlamaCppOptions::default());
    assert_eq!(servers[0].warmup_deadline_secs, None);
    assert_eq!(servers[0].args, vec!["--ctx-size", "4096"]);
    assert_eq!(
        servers[0].model.source,
        Some(ModelSource::LocalPath {
            path: ModelPath::new("/models/qwen.gguf").unwrap()
        })
    );
}

#[tokio::test]
async fn hf_model_artifact_downloader_resolves_deterministic_local_cache_hit_without_network() {
    let tmp = TempDir::new();
    let downloader = HfModelArtifactDownloader::new(tmp.path());
    let repo = HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap();
    let path = downloader.cache_path_for(&repo);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, b"gguf").unwrap();

    let resolution = downloader
        .resolve_hf_model(
            &repo,
            Arc::new(|_| panic!("cache hit must not report progress")),
            ModelArtifactCancel::new(),
        )
        .await
        .unwrap();

    assert!(resolution.cache_hit);
    assert_eq!(resolution.path.as_str(), path.to_string_lossy());
    assert_eq!(
        path.strip_prefix(tmp.path()).unwrap(),
        std::path::Path::new("Qwen--Qwen3-Coder").join("Q4_K_M.gguf")
    );
}

#[tokio::test]
async fn hf_model_artifact_downloader_reports_downloaded_cache_state() {
    let tmp = TempDir::new();
    let downloader = HfModelArtifactDownloader::new(tmp.path());
    let repo = HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap();
    let path = downloader.cache_path_for(&repo);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, b"gguf").unwrap();

    let state = downloader.hf_model_state(&repo).await.unwrap();

    assert_eq!(
        state,
        ModelArtifactState::Downloaded {
            path: ModelPath::new(path.to_string_lossy()).unwrap(),
            size_bytes: Some(4),
        }
    );
}

#[tokio::test]
async fn hf_model_artifact_downloader_deletes_merged_cache_without_config_side_effects() {
    let tmp = TempDir::new();
    let downloader = HfModelArtifactDownloader::new(tmp.path());
    let repo = HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap();
    let path = downloader.cache_path_for(&repo);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, b"gguf").unwrap();

    downloader.delete_hf_model(&repo).await.unwrap();

    assert!(!path.exists());
    assert_eq!(
        downloader.hf_model_state(&repo).await.unwrap(),
        ModelArtifactState::Missing
    );
}
