mod common;

use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};

use common::MockTransport;
use serde_json::json;
use sgclaw::browser::{BrowserBackend, PipeBrowserBackend};
use sgclaw::compat::browser_script_skill_tool::{
    execute_browser_script_tool, BrowserScriptSkillTool,
};
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing};
use sgclaw::security::MacPolicy;
use zeroclaw::skills::SkillTool;
use zeroclaw::tools::Tool;

fn test_policy() -> MacPolicy {
    MacPolicy::from_json_str(
        r#"{
            "version": "1.0",
            "domains": { "allowed": ["www.zhihu.com"] },
            "pipe_actions": {
                "allowed": ["click", "type", "navigate", "getText", "eval"],
                "blocked": []
            }
        }"#,
    )
    .unwrap()
}

#[tokio::test]
async fn execute_browser_script_tool_runs_packaged_script_with_expected_domain() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(
        scripts_dir.join("extract_hotlist.js"),
        "return { wrapped_args: args, source: \"packaged script\" };\n",
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "sheet_name": "知乎热榜",
                "rows": [[1, "标题", "10条"]]
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    let mut tool_args = HashMap::new();
    tool_args.insert("top_n".to_string(), "How many rows to extract".to_string());
    let skill_tool = SkillTool {
        name: "extract_hotlist".to_string(),
        description: "Extract structured hotlist rows".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/extract_hotlist.js".to_string(),
        args: tool_args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &PipeBrowserBackend::from_inner(browser_tool),
        json!({
            "expected_domain": "https://WWW.ZHIHU.COM/hot?foo=bar",
            "top_n": "10"
        }),
    )
    .await
    .unwrap();

    let sent = transport.sent_messages();
    assert!(result.success);
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&result.output).unwrap(),
        json!({
            "sheet_name": "知乎热榜",
            "rows": [[1, "标题", "10条"]]
        })
    );
    assert!(matches!(
        &sent[0],
        AgentMessage::Command {
            action,
            params,
            security,
            ..
        } if action == &Action::Eval
            && security.expected_domain == "www.zhihu.com"
            && params["script"].as_str().unwrap().contains("\"expected_domain\":\"www.zhihu.com\"")
            && params["script"].as_str().unwrap().contains("\"top_n\":\"10\"")
            && params["script"].as_str().unwrap().contains("source: \"packaged script\"")
    ));
}

#[tokio::test]
async fn execute_browser_script_tool_rejects_non_browser_script_tool_kind() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper-invalid-kind");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(scripts_dir.join("extract_hotlist.js"), "return 'unused';\n").unwrap();

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    let mut tool_args = HashMap::new();
    tool_args.insert("top_n".to_string(), "How many rows to extract".to_string());
    let skill_tool = SkillTool {
        name: "extract_hotlist".to_string(),
        description: "Extract structured hotlist rows".to_string(),
        kind: "shell".to_string(),
        command: "scripts/extract_hotlist.js".to_string(),
        args: tool_args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &PipeBrowserBackend::from_inner(browser_tool),
        json!({
            "expected_domain": "www.zhihu.com",
            "top_n": "10"
        }),
    )
    .await
    .unwrap();

    assert!(!result.success);
    assert_eq!(
        result.error.as_deref(),
        Some("browser script tool kind must be browser_script, got shell")
    );
    assert!(transport.sent_messages().is_empty());
}

#[tokio::test]
async fn execute_browser_script_tool_rejects_missing_expected_domain() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper-invalid-domain");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(scripts_dir.join("extract_hotlist.js"), "return 'unused';\n").unwrap();

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    let mut tool_args = HashMap::new();
    tool_args.insert("top_n".to_string(), "How many rows to extract".to_string());
    let skill_tool = SkillTool {
        name: "extract_hotlist".to_string(),
        description: "Extract structured hotlist rows".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/extract_hotlist.js".to_string(),
        args: tool_args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &PipeBrowserBackend::from_inner(browser_tool),
        json!({
            "expected_domain": "   ",
            "top_n": "10"
        }),
    )
    .await
    .unwrap();

    assert!(!result.success);
    assert_eq!(
        result.error.as_deref(),
        Some("expected_domain must be a non-empty string, got \"   \"")
    );
    assert!(transport.sent_messages().is_empty());
}

#[tokio::test]
async fn browser_script_skill_tool_executes_packaged_script_via_eval() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-skill");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(
        scripts_dir.join("extract_hotlist.js"),
        r#"
const topN = Number(args.top_n || 10);
return {
  sheet_name: "知乎热榜",
  rows: [[1, "标题", `${topN}条`]]
};
"#,
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "sheet_name": "知乎热榜",
                "rows": [[1, "标题", "10条"]]
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let backend: Arc<dyn BrowserBackend> = Arc::new(PipeBrowserBackend::from_inner(browser_tool));

    let mut args = HashMap::new();
    args.insert("top_n".to_string(), "How many rows to extract".to_string());
    let skill_tool = SkillTool {
        name: "extract_hotlist".to_string(),
        description: "Extract structured hotlist rows".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/extract_hotlist.js".to_string(),
        args,
    };
    let tool = BrowserScriptSkillTool::new("zhihu-hotlist", &skill_tool, &skill_dir, backend)
        .unwrap();

    let result = tool
        .execute(json!({
            "expected_domain": "https://www.zhihu.com/hot",
            "top_n": "10"
        }))
        .await
        .unwrap();

    let sent = transport.sent_messages();
    assert!(result.success);
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&result.output).unwrap(),
        json!({
            "sheet_name": "知乎热榜",
            "rows": [[1, "标题", "10条"]]
        })
    );
    assert!(matches!(
        &sent[0],
        AgentMessage::Command {
            action,
            params,
            security,
            ..
        } if action == &Action::Eval
            && security.expected_domain == "www.zhihu.com"
            && params["script"].as_str().unwrap().contains("\"expected_domain\":\"www.zhihu.com\"")
            && params["script"].as_str().unwrap().contains("\"top_n\":\"10\"")
            && params["script"].as_str().unwrap().contains("return {")
    ));
}

#[tokio::test]
async fn browser_script_skill_tool_executes_script_directly_under_skill_root() {
    let skill_root = unique_temp_dir("sgclaw-browser-script-direct-root");
    let script_name = "extract_hotlist_direct.js";
    let script_path = skill_root.join(script_name);
    fs::write(
        &script_path,
        r#"
return {
  sheet_name: "知乎热榜",
  rows: [[1, "标题", args.top_n]]
};
"#,
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "sheet_name": "知乎热榜",
                "rows": [[1, "标题", "10条"]]
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let backend: Arc<dyn BrowserBackend> = Arc::new(PipeBrowserBackend::from_inner(browser_tool));

    let mut args = HashMap::new();
    args.insert("top_n".to_string(), "How many rows to extract".to_string());
    let skill_tool = SkillTool {
        name: "extract_hotlist".to_string(),
        description: "Extract structured hotlist rows".to_string(),
        kind: "browser_script".to_string(),
        command: script_name.to_string(),
        args,
    };
    let tool = BrowserScriptSkillTool::new("zhihu-hotlist", &skill_tool, &skill_root, backend)
        .unwrap();

    let result = tool
        .execute(json!({
            "expected_domain": "https://www.zhihu.com/hot",
            "top_n": "10条"
        }))
        .await
        .unwrap();

    let sent = transport.sent_messages();
    assert!(result.success);
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&result.output).unwrap(),
        json!({
            "sheet_name": "知乎热榜",
            "rows": [[1, "标题", "10条"]]
        })
    );
    assert!(matches!(
        &sent[0],
        AgentMessage::Command {
            action,
            params,
            security,
            ..
        } if action == &Action::Eval
            && security.expected_domain == "www.zhihu.com"
            && params["script"].as_str().unwrap().contains("\"expected_domain\":\"www.zhihu.com\"")
            && params["script"].as_str().unwrap().contains("\"top_n\":\"10条\"")
            && params["script"].as_str().unwrap().contains("rows: [[1, \"标题\", args.top_n]]")
    ));
}

#[tokio::test]
async fn browser_script_helper_executes_packaged_script_via_eval() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper-fault-details");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(
        scripts_dir.join("collect_fault_details.js"),
        r#"
return {
  sheet_name: "故障明细",
  rows: [[args.period, "已完成"]]
};
"#,
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "sheet_name": "故障明细",
                "rows": [["2026-04", "已完成"]]
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let backend = PipeBrowserBackend::from_inner(browser_tool);

    let mut args = HashMap::new();
    args.insert("period".to_string(), "Target report period".to_string());
    let skill_tool = SkillTool {
        name: "collect_fault_details".to_string(),
        description: "Collect fault detail rows".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/collect_fault_details.js".to_string(),
        args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &backend,
        json!({
            "expected_domain": "https://www.zhihu.com/hot",
            "period": "2026-04"
        }),
    )
    .await
    .unwrap();

    let sent = transport.sent_messages();
    assert!(result.success);
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&result.output).unwrap(),
        json!({
            "sheet_name": "故障明细",
            "rows": [["2026-04", "已完成"]]
        })
    );
    assert!(matches!(
        &sent[0],
        AgentMessage::Command {
            action,
            params,
            security,
            ..
        } if action == &Action::Eval
            && security.expected_domain == "www.zhihu.com"
            && params["script"].as_str().unwrap().contains("\"expected_domain\":\"www.zhihu.com\"")
            && params["script"].as_str().unwrap().contains("\"period\":\"2026-04\"")
            && params["script"].as_str().unwrap().contains("sheet_name")
    ));
}

#[tokio::test]
async fn browser_script_helper_requires_expected_domain() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper-missing-domain");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(scripts_dir.join("collect_fault_details.js"), "return { ok: true };\n").unwrap();

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let backend = PipeBrowserBackend::from_inner(browser_tool);

    let mut args = HashMap::new();
    args.insert("period".to_string(), "Target report period".to_string());
    let skill_tool = SkillTool {
        name: "collect_fault_details".to_string(),
        description: "Collect fault detail rows".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/collect_fault_details.js".to_string(),
        args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &backend,
        json!({
            "period": "2026-04"
        }),
    )
    .await
    .unwrap();

    assert!(!result.success);
    assert_eq!(
        result.error.as_deref(),
        Some("missing required field expected_domain")
    );
    assert!(transport.sent_messages().is_empty());
}

#[tokio::test]
async fn execute_browser_script_tool_preserves_structured_report_artifact_payload() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-helper-report-artifact");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    fs::write(
        scripts_dir.join("collect_fault_details.js"),
        r#"
return {
  type: "report-artifact",
  report_name: "fault-details-report",
  period: args.period,
  selected_range: {
    start: "2026-03-08 16:00:00",
    end: "2026-03-09 16:00:00"
  },
  columns: ["qxdbh"],
  rows: [{ qxdbh: "QX-1" }],
  sections: [{ name: "summary-sheet", columns: ["index"], rows: [{ index: 1 }] }],
  counts: { detail_rows: 1, summary_rows: 1 },
  status: "partial",
  partial_reasons: ["report_log_failed"],
  downstream: {
    export: { attempted: true, success: true, path: "http://localhost/export.xlsx" },
    report_log: { attempted: true, success: false, error: "500" }
  }
};
"#,
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "type": "report-artifact",
                "report_name": "fault-details-report",
                "period": "2026-03",
                "selected_range": {
                    "start": "2026-03-08 16:00:00",
                    "end": "2026-03-09 16:00:00"
                },
                "columns": ["qxdbh"],
                "rows": [{ "qxdbh": "QX-1" }],
                "sections": [{ "name": "summary-sheet", "columns": ["index"], "rows": [{ "index": 1 }] }],
                "counts": { "detail_rows": 1, "summary_rows": 1 },
                "status": "partial",
                "partial_reasons": ["report_log_failed"],
                "downstream": {
                    "export": { "attempted": true, "success": true, "path": "http://localhost/export.xlsx" },
                    "report_log": { "attempted": true, "success": false, "error": "500" }
                }
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let backend = PipeBrowserBackend::from_inner(browser_tool);

    let mut tool_args = HashMap::new();
    tool_args.insert("period".to_string(), "YYYY-MM period to collect".to_string());
    let skill_tool = SkillTool {
        name: "collect_fault_details".to_string(),
        description: "Collect structured fault details".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/collect_fault_details.js".to_string(),
        args: tool_args,
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &backend,
        json!({
            "expected_domain": "https://www.zhihu.com/",
            "period": "2026-03"
        }),
    )
    .await
    .unwrap();

    assert!(result.success);
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&result.output).unwrap(),
        json!({
            "type": "report-artifact",
            "report_name": "fault-details-report",
            "period": "2026-03",
            "selected_range": {
                "start": "2026-03-08 16:00:00",
                "end": "2026-03-09 16:00:00"
            },
            "columns": ["qxdbh"],
            "rows": [{ "qxdbh": "QX-1" }],
            "sections": [{ "name": "summary-sheet", "columns": ["index"], "rows": [{ "index": 1 }] }],
            "counts": { "detail_rows": 1, "summary_rows": 1 },
            "status": "partial",
            "partial_reasons": ["report_log_failed"],
            "downstream": {
                "export": { "attempted": true, "success": true, "path": "http://localhost/export.xlsx" },
                "report_log": { "attempted": true, "success": false, "error": "500" }
            }
        })
    );
}

#[tokio::test]
async fn execute_browser_script_tool_awaits_async_script() {
    let skill_dir = unique_temp_dir("sgclaw-browser-script-async");
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir).unwrap();
    // 异步脚本，返回 Promise
    fs::write(
        scripts_dir.join("async_extract.js"),
        "return (async function() { return { async: true, args: args }; })();\n",
    )
    .unwrap();

    let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
        seq: 1,
        success: true,
        data: json!({
            "text": {
                "async": true,
                "args": { "expected_domain": "example.com" }
            }
        }),
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 5,
        },
    }]));

    let policy_json = MacPolicy::from_json_str(
        r#"{
            "version": "1.0",
            "domains": { "allowed": ["www.zhihu.com", "example.com"] },
            "pipe_actions": {
                "allowed": ["click", "type", "navigate", "getText", "eval"],
                "blocked": []
            }
        }"#,
    )
    .unwrap();

    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        policy_json,
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    let skill_tool = SkillTool {
        name: "async_extract".to_string(),
        description: "Extract data asynchronously".to_string(),
        kind: "browser_script".to_string(),
        command: "scripts/async_extract.js".to_string(),
        args: HashMap::new(),
    };

    let result = execute_browser_script_tool(
        &skill_tool,
        &skill_dir,
        &PipeBrowserBackend::from_inner(browser_tool),
        json!({
            "expected_domain": "example.com"
        }),
    )
    .await
    .unwrap();

    assert!(result.success);
    let output = serde_json::from_str::<serde_json::Value>(&result.output).unwrap();
    assert_eq!(output["async"], true);
}

fn unique_temp_dir(prefix: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
    fs::create_dir_all(&path).unwrap();
    path
}
