//! L5 tests for the profile/first-run DTO (de)serialisation contract: camelCase
//! on the wire, embedded [`AgentProfile`] shape preserved, and `parse_profile_id`
//! error behaviour.

use app_tauri_lib::dto::{
    parse_delete_profile, parse_profile_id, CloneOpenCodeProfileFromSeedRequestDto,
    CloneProfileFromSeedRequestDto, ConfigureProfilesRequestDto, DetectProfilesRequestDto,
    DetectProfilesResponseDto, FirstRunStateDto, ProfileListDto, ProfileModelCatalogDto,
    SaveProfileRequestDto,
};
use application::{
    CloneOpenCodeProfileFromSeedInput, CloneProfileFromSeedInput, ConfigureProfilesInput,
    DetectProfilesInput, DetectProfilesOutput, FirstRunStateOutput, ProfileAvailability,
    SaveProfileInput,
};
use domain::ids::{LocalModelServerId, ProfileId};
use domain::profile::{AgentProfile, ContextInjection, OpenCodeConfig, StructuredAdapter};
use serde_json::json;
use uuid::Uuid;

fn profile(id: u128, name: &str, command: &str) -> AgentProfile {
    AgentProfile::new(
        ProfileId::from_uuid(Uuid::from_u128(id)),
        name,
        command,
        Vec::new(),
        ContextInjection::convention_file("CLAUDE.md").unwrap(),
        Some(format!("{command} --version")),
        "{projectRoot}",
        None,
    )
    .unwrap()
}

#[test]
fn profile_list_dto_serialises_camelcase_profiles() {
    let dto: ProfileListDto = vec![profile(1, "Claude", "claude")].into();
    let v = serde_json::to_value(&dto).unwrap();
    let arr = v.as_array().expect("transparent array");
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["command"], "claude");
    assert_eq!(arr[0]["cwdTemplate"], "{projectRoot}");
    assert_eq!(arr[0]["contextInjection"]["strategy"], "conventionFile");
}

#[test]
fn detect_request_deserialises_candidates() {
    let raw = json!({
        "candidates": [{
            "id": Uuid::from_u128(1).to_string(),
            "name": "Claude",
            "command": "claude",
            "args": [],
            "contextInjection": { "strategy": "stdin" },
            "detect": "claude --version",
            "cwdTemplate": "{projectRoot}"
        }]
    });
    let dto: DetectProfilesRequestDto = serde_json::from_value(raw).unwrap();
    let input: DetectProfilesInput = dto.into();
    assert_eq!(input.candidates.len(), 1);
    assert_eq!(input.candidates[0].command, "claude");
}

#[test]
fn detect_response_serialises_available_flag_camelcase() {
    let out = DetectProfilesOutput {
        results: vec![ProfileAvailability {
            profile: profile(1, "Claude", "claude"),
            available: true,
        }],
    };
    let dto: DetectProfilesResponseDto = out.into();
    let v = serde_json::to_value(&dto).unwrap();
    let arr = v.as_array().unwrap();
    assert_eq!(arr[0]["available"], true);
    assert_eq!(arr[0]["profile"]["command"], "claude");
}

#[test]
fn save_request_deserialises_profile() {
    let raw = json!({
        "profile": {
            "id": Uuid::from_u128(2).to_string(),
            "name": "Codex",
            "command": "codex",
            "args": ["--foo"],
            "contextInjection": { "strategy": "conventionFile", "target": "AGENTS.md" },
            "detect": null,
            "cwdTemplate": ""
        }
    });
    let dto: SaveProfileRequestDto = serde_json::from_value(raw).unwrap();
    let input: SaveProfileInput = dto.into();
    assert_eq!(input.profile.command, "codex");
    assert_eq!(input.profile.args, vec!["--foo"]);
    assert!(input.profile.detect.is_none());
}

#[test]
fn clone_opencode_profile_from_seed_request_deserialises_camelcase_config() {
    let server_id = LocalModelServerId::from_uuid(Uuid::from_u128(35));
    let raw = json!({
        "name": "Local Qwen",
        "opencode": {
            "baseURL": "http://localhost:8081/v1",
            "model": "qwen3-coder-70b",
            "reasoning": false,
            "attachment": true,
            "localModelServerId": server_id.to_string()
        }
    });

    let dto: CloneOpenCodeProfileFromSeedRequestDto = serde_json::from_value(raw).unwrap();
    let input: CloneOpenCodeProfileFromSeedInput = dto.into();
    assert_eq!(input.name.as_deref(), Some("Local Qwen"));
    let opencode = input.opencode.expect("config override");
    assert_eq!(opencode.base_url, "http://localhost:8081/v1");
    assert_eq!(opencode.api_key, None);
    assert_eq!(opencode.model, "qwen3-coder-70b");
    assert_eq!(opencode.reasoning, Some(false));
    assert_eq!(opencode.attachment, Some(true));
    assert_eq!(opencode.local_model_server_id, Some(server_id));
}

#[test]
fn clone_profile_from_seed_request_deserialises_camelcase_overrides() {
    let seed_id = Uuid::from_u128(42);
    let raw = json!({
        "seedProfileId": seed_id.to_string(),
        "name": "Codex GPT-5",
        "model": "gpt-5-codex"
    });

    let dto: CloneProfileFromSeedRequestDto = serde_json::from_value(raw).unwrap();
    let input: CloneProfileFromSeedInput = dto.into();
    assert_eq!(input.seed_profile_id, ProfileId::from_uuid(seed_id));
    assert_eq!(input.name.as_deref(), Some("Codex GPT-5"));
    assert_eq!(input.model.as_deref(), Some("gpt-5-codex"));
}

#[test]
fn profile_model_catalogue_dto_serialises_searchable_camelcase_entries() {
    let dto = ProfileModelCatalogDto {
        models: vec![app_tauri_lib::dto::ProfileModelCatalogEntryDto {
            adapter: StructuredAdapter::Codex,
            model_id: "gpt-5-codex".to_owned(),
            display_name: "GPT-5 Codex".to_owned(),
            aliases: vec!["codex".to_owned()],
            recommended: true,
            compatibility: domain::ModelCompatibility::Compatible,
            source: domain::ModelCatalogSource::Catalogue,
        }],
        cli_version: Some("0.45.1".to_owned()),
        warnings: vec!["provider unavailable".to_owned()],
    };

    let value = serde_json::to_value(&dto).unwrap();
    assert_eq!(value["cliVersion"], "0.45.1");
    assert_eq!(value["warnings"], json!(["provider unavailable"]));
    assert_eq!(value["models"][0]["adapter"], "codex");
    assert_eq!(value["models"][0]["modelId"], "gpt-5-codex");
    assert_eq!(value["models"][0]["displayName"], "GPT-5 Codex");
    assert_eq!(value["models"][0]["aliases"], json!(["codex"]));
    assert_eq!(value["models"][0]["recommended"], true);
    assert_eq!(value["models"][0]["compatibility"], "compatible");
    assert_eq!(value["models"][0]["source"], "catalogue");
}

#[test]
fn opencode_config_dto_omits_local_model_server_id_when_none() {
    let config = OpenCodeConfig::new(
        "http://localhost:8080/v1",
        None,
        "qwen3-coder-30b",
        None,
        None,
    )
    .unwrap();

    let value = serde_json::to_value(&config).unwrap();
    assert!(value.get("localModelServerId").is_none());
}

#[test]
fn configure_request_deserialises_profiles() {
    let raw = json!({ "profiles": [] });
    let dto: ConfigureProfilesRequestDto = serde_json::from_value(raw).unwrap();
    let input: ConfigureProfilesInput = dto.into();
    assert!(input.profiles.is_empty());
}

#[test]
fn first_run_state_dto_serialises_camelcase() {
    let out = FirstRunStateOutput {
        is_first_run: true,
        reference_profiles: vec![profile(1, "Claude", "claude")],
    };
    let dto: FirstRunStateDto = out.into();
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["isFirstRun"], true);
    assert!(v.get("is_first_run").is_none(), "no snake_case leak");
    assert_eq!(v["referenceProfiles"][0]["command"], "claude");
}

#[test]
fn parse_profile_id_accepts_uuid_and_rejects_garbage() {
    let id = Uuid::from_u128(7);
    assert_eq!(
        parse_profile_id(&id.to_string()).unwrap(),
        ProfileId::from_uuid(id)
    );
    let err = parse_profile_id("not-a-uuid").expect_err("garbage rejected");
    assert_eq!(err.code, "INVALID");
}

#[test]
fn parse_delete_profile_builds_input() {
    let id = Uuid::from_u128(9);
    let input = parse_delete_profile(&id.to_string()).unwrap();
    assert_eq!(input.id, ProfileId::from_uuid(id));
}
