use super::ZedAgent;
use crate::tooling::ToolDescriptor;
use crate::zed::types::{NotificationEnvelope, ToolRuntime};
use assert_fs::TempDir;
use serde_json::json;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::time::Duration;
use tokio::sync::mpsc;
use vtcode_core::config::constants::tools;
use vtcode_core::config::core::PromptCachingConfig;
use vtcode_core::config::tool_call_delay_for_rate;
use vtcode_core::config::types::{
    AgentConfig as CoreAgentConfig, ModelSelectionSource, ReasoningEffortLevel, UiSurfacePreference,
};
use vtcode_core::config::{AgentClientProtocolZedConfig, CommandsConfig, ToolsConfig};
use vtcode_core::core::agent::snapshots::{
    DEFAULT_CHECKPOINTS_ENABLED, DEFAULT_MAX_AGE_DAYS, DEFAULT_MAX_SNAPSHOTS,
};
use vtcode_core::core::interfaces::SessionMode;
use vtcode_core::llm::provider::{MessageRole, ToolDefinition};

async fn build_agent(workspace: &Path) -> ZedAgent {
    build_agent_with_tools_config(workspace, ToolsConfig::default()).await
}

async fn build_agent_with_tools_config(workspace: &Path, tools_config: ToolsConfig) -> ZedAgent {
    let core_config = CoreAgentConfig {
        model: "test-model".to_string(),
        api_key: String::new(),
        provider: "test-provider".to_string(),
        api_key_env: "TEST_API_KEY".to_string(),
        workspace: workspace.to_path_buf(),
        verbose: false,
        quiet: false,
        theme: "test".to_string(),
        reasoning_effort: ReasoningEffortLevel::Low,
        ui_surface: UiSurfacePreference::default(),
        prompt_cache: PromptCachingConfig::default(),
        model_source: ModelSelectionSource::WorkspaceConfig,
        custom_api_keys: BTreeMap::new(),
        checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
        checkpointing_storage_dir: None,
        checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
        checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
        max_conversation_turns: 1000,
        model_behavior: None,
        openai_chatgpt_auth: None,
    };

    let mut zed_config = AgentClientProtocolZedConfig::default();
    zed_config.tools.list_files = true;
    zed_config.tools.read_file = false;

    let (tx, mut rx) = mpsc::unbounded_channel::<NotificationEnvelope>();
    tokio::spawn(async move {
        while let Some(envelope) = rx.recv().await {
            let _ = envelope.completion.send(());
        }
    });

    ZedAgent::new(
        core_config,
        zed_config,
        tools_config,
        CommandsConfig::default(),
        String::new(),
        tx,
        Some("Zed".to_string()),
    )
    .await
}

#[test]
fn tool_call_delay_for_rate_ignores_unset_or_zero_limits() {
    assert_eq!(tool_call_delay_for_rate(None), None);
    assert_eq!(tool_call_delay_for_rate(Some(0)), None);
}

#[test]
fn tool_call_delay_for_rate_uses_per_second_interval() {
    assert_eq!(
        tool_call_delay_for_rate(Some(4)),
        Some(Duration::from_millis(250))
    );
}

#[tokio::test]
async fn tool_loop_limit_uses_tools_config() {
    let temp = TempDir::new().unwrap();
    let tools_config = ToolsConfig {
        max_tool_loops: 2,
        ..ToolsConfig::default()
    };
    let agent = build_agent_with_tools_config(temp.path(), tools_config).await;

    assert!(!agent.tool_loop_limit_reached(0));
    assert!(!agent.tool_loop_limit_reached(1));
    assert!(agent.tool_loop_limit_reached(2));
    assert!(
        agent
            .tool_loop_limit_message()
            .contains("maximum tool loops (2)")
    );
}

fn definition_names(definitions: Vec<ToolDefinition>) -> Vec<String> {
    definitions
        .into_iter()
        .map(|definition| definition.function_name().to_string())
        .collect()
}

#[test]
fn parse_terminal_command_rejects_empty_array() {
    let args = json!({ "command": [] });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "command array cannot be empty");
}

#[test]
fn parse_terminal_command_rejects_empty_string() {
    let args = json!({ "command": "" });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "command string cannot be empty");
}

#[test]
fn parse_terminal_command_rejects_whitespace_only_string() {
    let args = json!({ "command": "   " });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "command string cannot be empty");
}

#[test]
fn parse_terminal_command_rejects_empty_executable_in_array() {
    let args = json!({ "command": ["", "arg1", "arg2"] });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "command executable cannot be empty");
}

#[test]
fn parse_terminal_command_rejects_whitespace_only_executable_in_array() {
    let args = json!({ "command": ["  ", "arg1"] });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "command executable cannot be empty");
}

#[test]
fn parse_terminal_command_accepts_valid_array() {
    let args = json!({ "command": ["ls", "-la"] });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_ok());
    let cmd = result.unwrap();
    assert_eq!(cmd, vec!["ls", "-la"]);
}

#[test]
fn parse_terminal_command_accepts_valid_string() {
    let args = json!({ "command": "echo test" });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_ok());
    let cmd = result.unwrap();
    assert_eq!(cmd, vec!["echo", "test"]);
}

#[test]
fn parse_terminal_command_accepts_cmd_alias() {
    let args = json!({ "cmd": "echo test" });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_ok());
    let cmd = result.unwrap();
    assert_eq!(cmd, vec!["echo", "test"]);
}

#[test]
fn parse_terminal_command_rejects_missing_command_field() {
    let args = json!({});
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(
        result.unwrap_err(),
        "command execution requires a 'command' field (string/array or indexed command.N entries)"
    );
}

#[test]
fn parse_terminal_command_accepts_indexed_arguments_zero_based() {
    let args = json!({ "command.0": "python", "command.1": "-c", "command.2": "print('hi')" });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_ok());
    let cmd = result.unwrap();
    assert_eq!(cmd, vec!["python", "-c", "print('hi')"]);
}

#[test]
fn parse_terminal_command_accepts_indexed_arguments_one_based() {
    let args = json!({ "command.1": "ls", "command.2": "-a" });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_ok());
    let cmd = result.unwrap();
    assert_eq!(cmd, vec!["ls", "-a"]);
}

#[test]
fn parse_terminal_command_rejects_non_string_indexed_argument() {
    let args = json!({ "command.0": 1 });
    let result = ZedAgent::parse_terminal_command(&args);
    assert!(result.is_err());
    assert_eq!(
        result.unwrap_err(),
        "command array must contain only strings"
    );
}

#[tokio::test]
async fn resolve_terminal_working_dir_accepts_workdir_alias() {
    let temp = TempDir::new().unwrap();
    let agent = build_agent(temp.path()).await;
    let args = json!({ "workdir": "src" });

    let working_dir = agent
        .resolve_terminal_working_dir(&args)
        .expect("workdir alias should resolve")
        .expect("working directory should be present");

    assert_eq!(working_dir, temp.path().join("src"));
}

#[tokio::test]
async fn read_only_modes_hide_local_tools() {
    let temp = TempDir::new().unwrap();
    let agent = build_agent(temp.path()).await;
    let enabled_tools: Vec<_> = agent
        .tool_availability(true, false, "test-provider", "test-model")
        .into_iter()
        .filter_map(|(tool, runtime)| match runtime {
            ToolRuntime::Enabled => Some(tool),
            ToolRuntime::Disabled(_) => None,
        })
        .collect();

    let ask_names = definition_names(
        agent
            .tool_definitions(true, &enabled_tools, SessionMode::Ask)
            .unwrap(),
    );
    let architect_names = definition_names(
        agent
            .tool_definitions(true, &enabled_tools, SessionMode::Architect)
            .unwrap(),
    );
    let code_names = definition_names(
        agent
            .tool_definitions(true, &enabled_tools, SessionMode::Code)
            .unwrap(),
    );

    assert_eq!(
        ask_names,
        vec![tools::LIST_FILES.to_string(), "switch_mode".to_string()]
    );
    assert_eq!(architect_names, ask_names);
    assert!(code_names.contains(&"switch_mode".to_string()));
    assert!(code_names.contains(&tools::LIST_FILES.to_string()));
    assert!(code_names.contains(&"unified_search".to_string()));
    assert!(code_names.contains(&"unified_file".to_string()));
    assert!(code_names.contains(&"unified_exec".to_string()));
}

#[tokio::test]
async fn local_tool_execution_uses_registry_request_path() {
    let temp = TempDir::new().unwrap();
    fs::create_dir(temp.path().join("src")).unwrap();
    fs::write(temp.path().join("src").join("sample.txt"), "hello").unwrap();
    let agent = build_agent(temp.path()).await;

    let report = agent
        .execute_local_tool(
            tools::UNIFIED_SEARCH,
            &json!({
                "action": "list",
                "path": "src",
            }),
            "call-local-list",
        )
        .await;

    assert_eq!(report.status, crate::acp::ToolCallStatus::Completed);
    let payload = report.raw_output.expect("successful tool output");
    assert_eq!(payload["status"], "success");
    assert_eq!(payload["tool"], tools::UNIFIED_SEARCH);
    assert!(payload["result"].to_string().contains("sample.txt"));
}

#[tokio::test]
async fn local_tool_execution_reports_registry_failure() {
    let temp = TempDir::new().unwrap();
    let agent = build_agent(temp.path()).await;

    let report = agent
        .execute_local_tool("unknown_tool", &json!({}), "call-local-missing")
        .await;

    assert_eq!(report.status, crate::acp::ToolCallStatus::Failed);
    assert!(report.llm_response.contains("unknown_tool"));
}

#[tokio::test]
async fn local_tool_metadata_uses_core_action_labels_and_kinds() {
    let temp = TempDir::new().unwrap();
    let agent = build_agent(temp.path()).await;
    let exec_args = json!({
        "action": "run",
        "command": "cargo check",
    });
    let search_args = json!({
        "action": "list",
        "path": "src",
    });
    let read_args = json!({
        "action": "read",
        "path": "src/lib.rs",
    });
    let write_args = json!({
        "action": "write",
        "path": "src/lib.rs",
        "content": "updated",
    });

    assert_eq!(
        agent.acp_tool_registry.render_title(
            ToolDescriptor::Local,
            tools::UNIFIED_EXEC,
            &exec_args
        ),
        "Run command"
    );
    assert_eq!(
        agent
            .acp_tool_registry
            .tool_kind_for_call(tools::UNIFIED_EXEC, Some(&exec_args)),
        crate::acp::ToolKind::Execute
    );
    assert_eq!(
        agent.acp_tool_registry.render_title(
            ToolDescriptor::Local,
            tools::UNIFIED_SEARCH,
            &search_args
        ),
        "List files"
    );
    assert_eq!(
        agent
            .acp_tool_registry
            .tool_kind_for_call(tools::UNIFIED_SEARCH, Some(&search_args)),
        crate::acp::ToolKind::Search
    );
    assert_eq!(
        agent.acp_tool_registry.render_title(
            ToolDescriptor::Local,
            tools::UNIFIED_FILE,
            &read_args
        ),
        "Read file"
    );
    assert_eq!(
        agent
            .acp_tool_registry
            .tool_kind_for_call(tools::UNIFIED_FILE, Some(&read_args)),
        crate::acp::ToolKind::Read
    );
    assert_eq!(
        agent.acp_tool_registry.render_title(
            ToolDescriptor::Local,
            tools::UNIFIED_FILE,
            &write_args
        ),
        "Write file"
    );
    assert_eq!(
        agent
            .acp_tool_registry
            .tool_kind_for_call(tools::UNIFIED_FILE, Some(&write_args)),
        crate::acp::ToolKind::Edit
    );
}

#[tokio::test]
async fn resolved_messages_include_mode_prompt_for_read_only_modes() {
    let temp = TempDir::new().unwrap();
    let agent = build_agent(temp.path()).await;
    let session_id = agent.register_session();
    let session = agent.session_handle(&session_id).unwrap();

    assert!(agent.resolved_messages(&session).is_empty());

    agent.update_session_mode(&session, SessionMode::Architect);
    let messages = agent.resolved_messages(&session);
    assert_eq!(messages.len(), 1);
    assert_eq!(messages[0].role, MessageRole::System);
    let prompt = messages[0].content.as_text();
    assert!(prompt.contains("Architect mode"));
    assert!(prompt.contains("switch to Code mode"));
}
