//! Reference profile **catalogue** — the pre-filled, *editable* profiles offered
//! by the first-run wizard (CONTEXT §9, ARCHITECTURE §6 `ConfigureProfiles`).
//!
//! These are **data, not domain code**: the catalogue lives in the application
//! layer (a product decision about *which* AIs to suggest), built from the
//! domain's validating constructors. Nothing is imposed — the user picks, edits
//! the pre-filled commands, and may add custom profiles. The single
//! [`domain::ports::AgentRuntime`] adapter consumes whatever profiles result.
//!
//! Reference set (CONTEXT §9):
//! - **Claude Code** — `claude`, context via `CLAUDE.md` (convention file),
//! - **OpenAI Codex CLI** — `codex`, context via `AGENTS.md`,
//! - **Gemini CLI** — `gemini`, context via `GEMINI.md`,
//! - **Aider** — `aider`, context passed as an argument (`--message-file {path}`).
//!
//! The ids are **stable, deterministic UUIDs** (derived from a fixed namespace)
//! so re-deriving the catalogue yields the same id for "claude" every time,
//! making the reference profiles addressable across runs without a registry.

use domain::ids::ProfileId;
use domain::permission::ProjectorKey;
use domain::profile::{
    AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig,
    StructuredAdapter,
};

/// Codex's interactive TUI is sensitive to receiving a large pasted block and the
/// submit key too close together. Keep the default conservative so delegated
/// prompts are actually submitted instead of remaining in the input editor.
pub const CODEX_SUBMIT_DELAY_MS: u32 = 350;

/// A fixed UUID namespace used to derive stable ids for reference profiles.
/// (Random-looking but constant; only its stability matters.)
const REFERENCE_NAMESPACE: uuid::Uuid = uuid::uuid!("6f9b1d2a-7c34-4e58-9a1b-2c3d4e5f6a7b");

/// Derives a stable [`ProfileId`] for a reference profile from its slug.
#[must_use]
fn reference_id(slug: &str) -> ProfileId {
    ProfileId::from_uuid(uuid::Uuid::new_v5(&REFERENCE_NAMESPACE, slug.as_bytes()))
}

/// Returns the stable id a reference profile slug maps to (exposed for tests and
/// callers that need to address a reference profile).
#[must_use]
pub fn reference_profile_id(slug: &str) -> ProfileId {
    reference_id(slug)
}

/// Builds the pre-filled, editable reference profiles (CONTEXT §9).
///
/// # Panics
/// Never in practice: every literal here satisfies the domain invariants, so the
/// constructors cannot fail; the `expect`s document that.
#[must_use]
pub fn reference_profiles() -> Vec<AgentProfile> {
    vec![
        AgentProfile::new(
            reference_id("claude"),
            "Claude Code",
            "claude",
            Vec::new(),
            ContextInjection::convention_file("CLAUDE.md")
                .expect("CLAUDE.md is a valid convention target"),
            Some("claude --version".to_owned()),
            "{agentRunDir}",
            None,
        )
        .expect("claude reference profile is valid")
        .with_structured_adapter(StructuredAdapter::Claude)
        .with_projector(ProjectorKey::Claude)
        .with_mcp(McpCapability::new(
            McpConfigStrategy::config_file(".mcp.json")
                .expect(".mcp.json is a valid relative MCP config target"),
            McpTransport::Stdio,
        )),
        AgentProfile::new(
            reference_id("codex"),
            "OpenAI Codex CLI",
            "codex",
            Vec::new(),
            ContextInjection::convention_file("AGENTS.md")
                .expect("AGENTS.md is a valid convention target"),
            Some("codex --version".to_owned()),
            "{agentRunDir}",
            None,
        )
        .expect("codex reference profile is valid")
        .with_structured_adapter(StructuredAdapter::Codex)
        .with_projector(ProjectorKey::Codex)
        .with_submit_delay_ms(CODEX_SUBMIT_DELAY_MS)
        .with_mcp(McpCapability::new(
            // Codex lit ses serveurs MCP dans `$CODEX_HOME/config.toml`, pas `.mcp.json` :
            // IdeA écrit ce TOML DANS le run dir et pointe `CODEX_HOME` dessus pour
            // isoler l'agent du `~/.codex` global (miroir du `.mcp.json` de Claude).
            McpConfigStrategy::toml_config_home(".codex/config.toml", "CODEX_HOME")
                .expect(".codex/config.toml + CODEX_HOME is a valid MCP config target"),
            McpTransport::Stdio,
        )),
        AgentProfile::new(
            reference_id("opencode-llamacpp"),
            "OpenCode + llama.cpp",
            "opencode",
            Vec::new(),
            ContextInjection::convention_file("AGENTS.md")
                .expect("AGENTS.md is a valid convention target"),
            Some("opencode --version".to_owned()),
            "{agentRunDir}",
            None,
        )
        .expect("OpenCode reference profile is valid")
        .with_structured_adapter(StructuredAdapter::OpenCode)
        .with_opencode(
            OpenCodeConfig::new(
                "http://localhost:8080/v1",
                Some("sk-no-key".to_owned()),
                "qwen3-coder-30b",
                None,
                None,
            )
            .expect("OpenCode config is valid"),
        )
        .with_mcp(McpCapability::new(
            McpConfigStrategy::open_code_config("opencode.json")
                .expect("opencode.json is a valid relative config target"),
            McpTransport::Stdio,
        )),
        AgentProfile::new(
            reference_id("gemini"),
            "Gemini CLI",
            "gemini",
            Vec::new(),
            ContextInjection::convention_file("GEMINI.md")
                .expect("GEMINI.md is a valid convention target"),
            Some("gemini --version".to_owned()),
            "{agentRunDir}",
            None,
        )
        .expect("gemini reference profile is valid"),
        AgentProfile::new(
            reference_id("aider"),
            "Aider",
            "aider",
            Vec::new(),
            ContextInjection::flag("--message-file {path}")
                .expect("aider flag template is non-empty"),
            Some("aider --version".to_owned()),
            "{agentRunDir}",
            None,
        )
        .expect("aider reference profile is valid"),
    ]
}

/// Returns the **selectable** subset of [`reference_profiles`] — the profiles the
/// first-run wizard and the agent-creation menu are allowed to offer (§17.3,
/// lot D7).
///
/// A profile is selectable iff it can be driven in **structured** mode
/// ([`AgentProfile::is_selectable`] = it carries a `structured_adapter`). Today
/// that is Claude + Codex + OpenAI-compatible; Gemini/Aider stay in
/// [`reference_profiles`] (the data catalogue is untouched) but are **not**
/// proposed for selection. There is no
/// custom-profile entry here either: the selection path offers only profiles we
/// know how to pilot.
///
/// This filter is the single selection gate; `is_selectable` is the same
/// predicate the `AgentSessionFactory` uses to decide it `supports` a profile, so
/// the menu and the runtime can never disagree.
#[must_use]
pub fn selectable_reference_profiles() -> Vec<AgentProfile> {
    reference_profiles()
        .into_iter()
        .filter(AgentProfile::is_selectable)
        .collect()
}

#[cfg(test)]
mod mcp_tests {
    use super::*;

    fn profile(slug: &str) -> AgentProfile {
        let id = reference_id(slug);
        reference_profiles()
            .into_iter()
            .find(|p| p.id == id)
            .unwrap_or_else(|| panic!("reference profile `{slug}` exists"))
    }

    #[test]
    fn claude_and_codex_expose_mcp_capability() {
        for slug in ["claude", "codex"] {
            let p = profile(slug);
            assert!(
                p.mcp.is_some(),
                "structured profile `{slug}` must carry an MCP capability"
            );
        }
    }

    #[test]
    fn claude_mcp_uses_config_file_mcp_json() {
        let mcp = profile("claude").mcp.expect("mcp present");
        assert_eq!(
            mcp.config,
            McpConfigStrategy::ConfigFile {
                target: ".mcp.json".to_owned()
            },
            "Claude should declare `.mcp.json`"
        );
        assert_eq!(mcp.transport, McpTransport::Stdio);
    }

    #[test]
    fn codex_mcp_uses_toml_config_home_codex() {
        // Codex lit `$CODEX_HOME/config.toml`, pas `.mcp.json` : le seed doit déclarer
        // la stratégie TOML isolée par `CODEX_HOME` (pendant Codex de Claude).
        let mcp = profile("codex").mcp.expect("mcp present");
        assert_eq!(
            mcp.config,
            McpConfigStrategy::TomlConfigHome {
                target: ".codex/config.toml".to_owned(),
                home_env: "CODEX_HOME".to_owned(),
            },
            "Codex should declare `.codex/config.toml` + CODEX_HOME"
        );
        assert_eq!(mcp.transport, McpTransport::Stdio);
        assert!(
            profile("codex").materializes_idea_bridge(),
            "the Codex seed must materialise the idea bridge"
        );
    }

    #[test]
    fn opencode_llamacpp_seed_replaces_http_ollama_profile() {
        let profile = profile("opencode-llamacpp");
        assert_eq!(
            profile.structured_adapter,
            Some(StructuredAdapter::OpenCode)
        );
        assert_eq!(profile.command, "opencode");
        assert!(profile.chat_http.is_none());
        assert_eq!(
            profile
                .opencode
                .as_ref()
                .map(|config| config.model.as_str()),
            Some("qwen3-coder-30b")
        );
        assert_eq!(
            profile
                .opencode
                .as_ref()
                .map(|config| config.base_url.as_str()),
            Some("http://localhost:8080/v1")
        );
        assert_eq!(
            profile
                .opencode
                .as_ref()
                .and_then(|config| config.api_key.as_deref()),
            Some("sk-no-key")
        );
        assert!(profile.mcp.is_some());
        assert!(profile.materializes_idea_bridge());
        assert!(profile.is_selectable());
    }

    #[test]
    fn gemini_and_aider_have_no_mcp_capability() {
        for slug in ["gemini", "aider"] {
            assert!(
                profile(slug).mcp.is_none(),
                "non-structured profile `{slug}` must NOT carry MCP (file fallback)"
            );
        }
    }

    // -- Lot LP3 : projector (clé du projecteur de permissions par-CLI) ----------

    #[test]
    fn claude_and_codex_seed_their_projector_key() {
        assert_eq!(
            profile("claude").projector,
            Some(ProjectorKey::Claude),
            "the Claude seed must pose the Claude projector"
        );
        assert_eq!(
            profile("codex").projector,
            Some(ProjectorKey::Codex),
            "the Codex seed must pose the Codex projector"
        );
    }

    #[test]
    fn gemini_and_aider_have_no_projector() {
        for slug in ["gemini", "aider"] {
            assert!(
                profile(slug).projector.is_none(),
                "non-structured profile `{slug}` must NOT carry a projector (native prompting)"
            );
        }
    }
}
