//! DTO contract tests for local model-server IPC.

use app_tauri_lib::dto::{
    save_model_server_input, ModelServerConfigDto, ModelServerKindDto, ModelSourceDto,
    SaveModelServerRequestDto, StopPolicyDto,
};
use domain::model_server::{
    ExecutablePath, LlamaCppOptions, LocalModelRef, LocalModelServerConfig, LocalModelServerKind,
    ModelPath, ModelServerEndpoint, ModelSource,
};
use domain::{LocalModelServerId, StopPolicy};
use serde_json::json;
use uuid::Uuid;

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

fn domain_config(id: LocalModelServerId, model_id: &str) -> LocalModelServerConfig {
    LocalModelServerConfig::new(
        id,
        LocalModelServerKind::LlamaCpp,
        "Local Qwen",
        ModelServerEndpoint::new("http://localhost:8080", 8080).unwrap(),
        LocalModelRef::new(
            model_id,
            "Qwen",
            Some(ModelSource::LocalPath {
                path: ModelPath::new("/models/qwen.gguf").unwrap(),
            }),
            "qwen3-coder",
        )
        .unwrap(),
        Some(ExecutablePath::new("/usr/bin/llama-server").unwrap()),
        LlamaCppOptions::new("127.0.0.1", Some(35), Some(4096), true).unwrap(),
        vec!["--threads".to_owned(), "8".to_owned()],
        true,
        StopPolicy::StopOnAppExit,
    )
    .unwrap()
}

#[test]
fn model_server_dto_serialises_flat_camelcase_wire_shape() {
    let dto = ModelServerConfigDto::from_domain(domain_config(sid(35), "internal-model-id"));

    let value = serde_json::to_value(dto).unwrap();

    assert_eq!(value["id"], sid(35).to_string());
    assert_eq!(value["kind"], "llamaCpp");
    assert_eq!(value["baseURL"], "http://localhost:8080/v1");
    assert_eq!(value["modelSource"]["type"], "localPath");
    assert_eq!(value["modelSource"]["path"], "/models/qwen.gguf");
    assert!(value.get("modelPath").is_none());
    assert_eq!(value["servedModelName"], "qwen3-coder");
    assert_eq!(value["binaryPath"], "/usr/bin/llama-server");
    assert_eq!(value["host"], "127.0.0.1");
    assert_eq!(value["gpuLayers"], 35);
    assert_eq!(value["contextSize"], 4096);
    assert_eq!(value["jinja"], true);
    assert_eq!(value["stopPolicy"], "stopOnAppExit");
    assert!(value.get("warmupDeadlineSecs").is_none());
    assert!(value.get("model").is_none(), "domain model must not leak");
    assert!(
        value.get("endpoint").is_none(),
        "domain endpoint must not leak"
    );
}

#[test]
fn model_server_dto_deserialises_flat_wire_shape_and_generates_model_id() {
    let raw = json!({
        "id": sid(36).to_string(),
        "kind": "llamaCpp",
        "name": "Local Qwen",
        "baseURL": "http://localhost:8081",
        "port": 8081,
        "modelPath": "/models/qwen.gguf",
        "servedModelName": "qwen3-coder",
        "binaryPath": "/usr/bin/llama-server",
        "gpuLayers": 35,
        "contextSize": 4096,
        "args": ["--threads", "8"],
        "autoStart": true,
        "stopPolicy": "stopOnAppExit"
    });

    let request = SaveModelServerRequestDto {
        config: serde_json::from_value(raw).unwrap(),
    };
    let input = save_model_server_input(request, None).unwrap();

    assert_eq!(input.config.id, sid(36));
    assert_eq!(input.config.kind, LocalModelServerKind::LlamaCpp);
    assert_eq!(input.config.endpoint.base_url, "http://localhost:8081/v1");
    assert_eq!(
        input.config.model.source,
        Some(ModelSource::LocalPath {
            path: ModelPath::new("/models/qwen.gguf").unwrap()
        })
    );
    assert_eq!(input.config.options.gpu_layers, Some(35));
    assert_eq!(input.config.options.context_size, Some(4096));
    assert_eq!(input.config.options.host, "127.0.0.1");
    assert_eq!(input.config.warmup_deadline_secs, None);
    assert!(!input.config.options.jinja);
    assert_eq!(input.config.model.served_name, "qwen3-coder");
    assert_eq!(input.config.model.label, "qwen3-coder");
    assert!(!input.config.model.id.is_empty());
}

#[test]
fn model_server_dto_preserves_existing_internal_model_id_on_upsert() {
    let existing = domain_config(sid(37), "stable-internal-id");
    let dto = ModelServerConfigDto {
        id: sid(37).to_string(),
        kind: ModelServerKindDto::LlamaCpp,
        name: "Updated Qwen".to_owned(),
        base_url: "http://localhost:8082/v1".to_owned(),
        port: 8082,
        model_source: Some(ModelSourceDto::LocalPath {
            path: "/models/qwen-updated.gguf".to_owned(),
        }),
        model_path: None,
        served_model_name: "qwen3-coder-updated".to_owned(),
        binary_path: Some("/usr/bin/llama-server".to_owned()),
        host: "127.0.0.1".to_owned(),
        gpu_layers: None,
        context_size: None,
        jinja: false,
        args: Vec::new(),
        auto_start: true,
        stop_policy: StopPolicyDto::StopOnAppExit,
        warmup_deadline_secs: Some(900),
    };

    let config = dto.into_domain(Some(&existing)).unwrap();

    assert_eq!(config.model.id, "stable-internal-id");
    assert_eq!(config.model.served_name, "qwen3-coder-updated");
    assert_eq!(config.warmup_deadline_secs, Some(900));
}

#[test]
fn model_server_dto_rejects_invalid_warmup_deadline_secs() {
    let raw = json!({
        "id": sid(40).to_string(),
        "kind": "llamaCpp",
        "name": "Local Qwen",
        "baseURL": "http://localhost:8085",
        "port": 8085,
        "modelPath": "/models/qwen.gguf",
        "servedModelName": "qwen3-coder",
        "host": "127.0.0.1",
        "jinja": false,
        "args": [],
        "autoStart": true,
        "stopPolicy": "stopOnAppExit",
        "warmupDeadlineSecs": 29
    });

    let request = SaveModelServerRequestDto {
        config: serde_json::from_value(raw).unwrap(),
    };

    assert_eq!(
        save_model_server_input(request, None).unwrap_err().code,
        "INVALID"
    );
}

#[test]
fn model_server_dto_roundtrips_v2_huggingface_source() {
    let raw = json!({
        "id": sid(38).to_string(),
        "kind": "llamaCpp",
        "name": "HF Qwen",
        "baseURL": "http://localhost:8083",
        "port": 8083,
        "modelSource": { "type": "huggingFace", "repo": "Qwen/Qwen3-Coder:Q4_K_M" },
        "servedModelName": "qwen3-coder",
        "binaryPath": "/usr/bin/llama-server",
        "host": "127.0.0.1",
        "jinja": false,
        "args": [],
        "autoStart": true,
        "stopPolicy": "stopOnAppExit"
    });

    let request = SaveModelServerRequestDto {
        config: serde_json::from_value(raw).unwrap(),
    };
    let input = save_model_server_input(request, None).unwrap();
    assert_eq!(
        input.config.model.source,
        Some(ModelSource::HuggingFace {
            repo: domain::model_server::HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap()
        })
    );

    let value = serde_json::to_value(ModelServerConfigDto::from_domain(input.config)).unwrap();
    assert_eq!(value["modelSource"]["type"], "huggingFace");
    assert_eq!(value["modelSource"]["repo"], "Qwen/Qwen3-Coder:Q4_K_M");
    assert!(value.get("modelPath").is_none());
}

#[test]
fn model_server_dto_rejects_conflicting_model_path_alias() {
    let raw = json!({
        "id": sid(39).to_string(),
        "kind": "llamaCpp",
        "name": "Local Qwen",
        "baseURL": "http://localhost:8084",
        "port": 8084,
        "modelSource": { "type": "localPath", "path": "/models/a.gguf" },
        "modelPath": "/models/b.gguf",
        "servedModelName": "qwen3-coder",
        "host": "127.0.0.1",
        "jinja": false,
        "args": [],
        "autoStart": true,
        "stopPolicy": "stopOnAppExit"
    });

    let request = SaveModelServerRequestDto {
        config: serde_json::from_value(raw).unwrap(),
    };

    assert_eq!(
        save_model_server_input(request, None).unwrap_err().code,
        "INVALID"
    );
}
