#![cfg(feature = "agent")]

use std::cell::RefCell;
use std::collections::BTreeMap;

use runx_contracts::{AgentActSourceType, JsonNumber, JsonObject, JsonValue, ResolutionRequest};
use runx_parser::SkillSource;
use runx_runtime::adapters::agent::{
    AgentAdapter, AgentExecutionTelemetry, AgentResolution, AgentResolver, AgentResolverError,
    AgentToolExecutionTrace,
};
use runx_runtime::{
    InvocationStatus, ManagedAgentConfig, RuntimeError, RuntimeOptions, SecretString, SkillAdapter,
    SkillInvocation, managed_agent_provider, run_harness_fixture_with_adapter,
};

const FIXTURE_CREATED_AT: &str = "2026-05-18T00:00:00Z";

#[test]
fn agent_task_invocation_id_and_envelope_shape() -> Result<(), Box<dyn std::error::Error>> {
    let resolver = RecordingResolver::success(JsonValue::String("done".to_owned()), None);
    let mut env = BTreeMap::new();
    env.insert(
        "RUNX_TOOL_ROOTS".to_owned(),
        "/tmp/runx-tools:/opt/runx-tools".to_owned(),
    );

    let output = AgentAdapter::agent_task(config(), &resolver).invoke(SkillInvocation {
        env,
        ..invocation(
            runx_parser::SourceKind::AgentStep,
            "fixture.step",
            source(
                runx_parser::SourceKind::AgentStep,
                Some("assistant"),
                Some("draft release notes"),
                None,
            ),
            JsonObject::new(),
        )
    })?;

    assert_eq!(output.status, InvocationStatus::Success);
    let requests = resolver.requests.borrow();
    assert_eq!(requests.len(), 1);
    let ResolutionRequest::AgentAct { id, invocation } = &requests[0] else {
        return Err(std::io::Error::other("missing agent_act request").into());
    };
    assert_eq!(id, "agent_task.draft_release_notes.output");
    assert_eq!(invocation.id, "agent_task.draft_release_notes.output");
    assert_eq!(invocation.source_type, AgentActSourceType::AgentStep);
    assert_eq!(invocation.agent.as_deref(), Some("assistant"));
    assert_eq!(invocation.task.as_deref(), Some("draft release notes"));

    assert_eq!(invocation.envelope.run_id, "rx_pending");
    assert_eq!(invocation.envelope.skill, "fixture.step");
    assert!(!invocation.envelope.instructions.is_empty());
    assert!(invocation.envelope.allowed_tools.is_empty());
    assert!(invocation.envelope.current_context.is_empty());
    assert!(invocation.envelope.historical_context.is_empty());
    assert!(invocation.envelope.provenance.is_empty());
    assert!(!invocation.envelope.trust_boundary.is_empty());
    let execution_location = invocation
        .envelope
        .execution_location
        .as_ref()
        .ok_or_else(|| std::io::Error::other("missing execution location"))?;
    assert_eq!(execution_location.skill_directory.as_ref(), "/tmp/skill");
    let tool_roots = execution_location
        .tool_roots
        .as_ref()
        .ok_or_else(|| std::io::Error::other("missing tool roots"))?;
    assert_eq!(
        tool_roots.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
        vec!["/tmp/runx-tools", "/opt/runx-tools"]
    );

    let agent_hook = object_field(&output.metadata, "agent_hook")?;
    assert_eq!(agent_hook.get("source_type"), Some(&string("agent-task")));
    assert_eq!(agent_hook.get("agent"), Some(&string("assistant")));
    assert_eq!(agent_hook.get("task"), Some(&string("draft release notes")));
    assert_eq!(agent_hook.get("route"), Some(&string("native")));
    assert_eq!(agent_hook.get("provider"), Some(&string("openai")));
    assert_eq!(agent_hook.get("model"), Some(&string("gpt-test")));
    assert_eq!(agent_hook.get("status"), Some(&string("success")));
    Ok(())
}

#[test]
fn agent_plain_text_success() -> Result<(), Box<dyn std::error::Error>> {
    let telemetry = AgentExecutionTelemetry {
        rounds: Some(2),
        tool_calls: Some(1),
        tools: Some(vec!["fs.read".to_owned()]),
        tool_executions: Some(vec![AgentToolExecutionTrace {
            tool: "fs.read".to_owned(),
            status: "success".to_owned(),
            receipt_id: Some("rct_1".to_owned()),
            resolution_kind: None,
        }]),
    };
    let resolver = RecordingResolver::success(
        JsonValue::String("plain final answer".to_owned()),
        Some(telemetry),
    );

    let output = AgentAdapter::agent(config(), &resolver).invoke(invocation(
        runx_parser::SourceKind::Agent,
        "fixture.agent",
        source(
            runx_parser::SourceKind::Agent,
            Some("assistant"),
            Some("summarize"),
            None,
        ),
        JsonObject::new(),
    ))?;

    assert_eq!(output.status, InvocationStatus::Success);
    assert_eq!(output.stdout, "plain final answer");
    assert_eq!(output.stderr, "");
    assert_eq!(output.exit_code, Some(0));
    let agent_runner = object_field(&output.metadata, "agent_runner")?;
    assert_eq!(agent_runner.get("skill"), Some(&string("fixture.agent")));
    assert_eq!(agent_runner.get("route"), Some(&string("native")));
    assert_eq!(agent_runner.get("provider"), Some(&string("openai")));
    assert_eq!(agent_runner.get("model"), Some(&string("gpt-test")));
    assert_eq!(agent_runner.get("status"), Some(&string("success")));
    assert_eq!(
        agent_runner.get("rounds"),
        Some(&JsonValue::Number(JsonNumber::U64(2)))
    );
    assert_eq!(
        agent_runner.get("tool_calls"),
        Some(&JsonValue::Number(JsonNumber::U64(1)))
    );
    assert_eq!(
        agent_runner.get("tools"),
        Some(&JsonValue::Array(vec![string("fs.read")]))
    );
    let tool_executions = array_field(agent_runner, "tool_executions")?;
    assert_eq!(tool_executions.len(), 1);
    Ok(())
}

#[test]
fn agent_task_structured_json_payload_success() -> Result<(), Box<dyn std::error::Error>> {
    let payload = JsonValue::Object(
        [
            ("title".to_owned(), JsonValue::String("Release".to_owned())),
            ("ready".to_owned(), JsonValue::Bool(true)),
        ]
        .into(),
    );
    let resolver = RecordingResolver::success(payload, None);
    let outputs = [
        ("title".to_owned(), JsonValue::String("string".to_owned())),
        ("ready".to_owned(), JsonValue::String("boolean".to_owned())),
    ]
    .into();

    let output = AgentAdapter::agent_task(config(), &resolver).invoke(invocation(
        runx_parser::SourceKind::AgentStep,
        "fixture.structured",
        source(
            runx_parser::SourceKind::AgentStep,
            Some("assistant"),
            Some("structured"),
            Some(outputs),
        ),
        JsonObject::new(),
    ))?;

    assert_eq!(output.status, InvocationStatus::Success);
    assert_eq!(output.stdout, r#"{"ready":true,"title":"Release"}"#);
    let requests = resolver.requests.borrow();
    let ResolutionRequest::AgentAct { invocation, .. } = &requests[0] else {
        return Err(std::io::Error::other("missing agent_act request").into());
    };
    assert_eq!(
        invocation
            .envelope
            .output
            .as_ref()
            .map(|output| output.len()),
        Some(2)
    );
    Ok(())
}

#[test]
fn provider_error_failure_sanitizes_stderr_and_metadata() -> Result<(), Box<dyn std::error::Error>>
{
    let resolver = RecordingResolver::failure("provider leaked sk-secret-value");

    let output = AgentAdapter::agent_task(config(), &resolver).invoke(invocation(
        runx_parser::SourceKind::AgentStep,
        "fixture.fail",
        source(
            runx_parser::SourceKind::AgentStep,
            Some("assistant"),
            Some("fail"),
            None,
        ),
        JsonObject::new(),
    ))?;

    assert_eq!(output.status, InvocationStatus::Failure);
    assert_eq!(output.stdout, "");
    assert_eq!(output.stderr, "Managed agent provider request failed.");
    assert_eq!(output.exit_code, None);
    assert!(!format!("{output:?}").contains("sk-secret-value"));
    let agent_hook = object_field(&output.metadata, "agent_hook")?;
    assert_eq!(agent_hook.get("status"), Some(&string("failure")));
    assert_eq!(agent_hook.get("route"), Some(&string("native")));
    assert_eq!(agent_hook.get("provider"), Some(&string("openai")));
    assert_eq!(agent_hook.get("model"), Some(&string("gpt-test")));
    Ok(())
}

#[test]
fn unsupported_source_type_returns_runtime_error() -> Result<(), Box<dyn std::error::Error>> {
    let resolver = RecordingResolver::success(JsonValue::String("unused".to_owned()), None);
    let error = AgentAdapter::agent(config(), &resolver).invoke(invocation(
        runx_parser::SourceKind::AgentStep,
        "fixture.unsupported",
        source(
            runx_parser::SourceKind::AgentStep,
            Some("assistant"),
            Some("task"),
            None,
        ),
        JsonObject::new(),
    ));

    match error {
        Err(RuntimeError::UnsupportedAdapter { adapter_type }) => {
            assert_eq!(adapter_type, "agent-task");
            Ok(())
        }
        Ok(_) => Err(std::io::Error::other("adapter unexpectedly succeeded").into()),
        Err(other) => Err(std::io::Error::other(format!("unexpected error: {other}")).into()),
    }
}

#[test]
fn harness_replay_runs_agent_skill_fixture() -> Result<(), Box<dyn std::error::Error>> {
    let resolver = RecordingResolver::success(JsonValue::String("agent replayed".to_owned()), None);
    let temp = tempfile::tempdir()?;
    let skill_dir = temp.path().join("skill");
    std::fs::create_dir_all(&skill_dir)?;
    std::fs::write(
        skill_dir.join("SKILL.md"),
        r#"---
name: fixture-agent
description: Fixture agent skill.
source:
  type: agent
  agent: assistant
  task: summarize
inputs:
  topic:
    type: string
    required: true
---
Summarize the topic.
"#,
    )?;
    let fixture_path = temp.path().join("harness.yaml");
    // Agent skills replay from the caller's recorded answer, keyed by the agent
    // act request id `agent.<skill>.output`, rather than from a live adapter
    // resolver; a recorded answer with no refusing closure seals the run.
    std::fs::write(
        &fixture_path,
        r#"
name: fixture-agent
kind: agent
target: skill
inputs:
  topic: harness replay
caller:
  answers:
    agent.fixture-agent.output:
      summary: agent replayed
expect:
  status: sealed
"#,
    )?;

    let replay = run_harness_fixture_with_adapter(
        &fixture_path,
        AgentAdapter::agent(config(), &resolver),
        fixture_runtime_options(),
    )?;

    assert_eq!(replay.status, runx_runtime::HarnessExpectedStatus::Sealed);
    let output = replay
        .skill_output
        .ok_or_else(|| std::io::Error::other("missing replay skill output"))?;
    assert_eq!(output.status, InvocationStatus::Success);
    assert!(output.stdout.contains("agent replayed"));
    Ok(())
}

fn fixture_runtime_options() -> RuntimeOptions {
    RuntimeOptions {
        created_at: FIXTURE_CREATED_AT.to_owned(),
        ..RuntimeOptions::local_development()
    }
}

struct RecordingResolver {
    requests: RefCell<Vec<ResolutionRequest>>,
    result: Result<AgentResolution, AgentResolverError>,
}

impl RecordingResolver {
    fn success(payload: JsonValue, telemetry: Option<AgentExecutionTelemetry>) -> Self {
        Self {
            requests: RefCell::new(Vec::new()),
            result: Ok(AgentResolution::agent(payload, telemetry)),
        }
    }

    fn failure(message: &str) -> Self {
        Self {
            requests: RefCell::new(Vec::new()),
            result: Err(AgentResolverError::provider_error(message)),
        }
    }
}

impl AgentResolver for &RecordingResolver {
    fn resolve(&self, request: ResolutionRequest) -> Result<AgentResolution, AgentResolverError> {
        self.requests.borrow_mut().push(request);
        self.result.clone()
    }
}

fn invocation(
    source_type: runx_parser::SourceKind,
    skill_name: &str,
    source: SkillSource,
    inputs: JsonObject,
) -> SkillInvocation {
    let mut request = SkillInvocation {
        skill_name: skill_name.to_owned(),
        source,
        inputs,
        resolved_inputs: JsonObject::new(),
        current_context: Vec::new(),
        skill_directory: "/tmp/skill".into(),
        env: BTreeMap::new(),
        credential_delivery: runx_runtime::CredentialDelivery::none(),
    };
    request.source.source_type = source_type;
    request
}

fn source(
    source_type: runx_parser::SourceKind,
    agent: Option<&str>,
    task: Option<&str>,
    outputs: Option<JsonObject>,
) -> SkillSource {
    SkillSource {
        act: None,
        source_type,
        command: None,
        args: Vec::new(),
        cwd: None,
        timeout_seconds: None,
        input_mode: None,
        sandbox: None,
        server: None,
        catalog_ref: None,
        tool: None,
        arguments: None,
        agent_card_url: None,
        agent_identity: None,
        agent: agent.map(str::to_owned),
        task: task.map(str::to_owned),
        hook: None,
        outputs,
        graph: None,
        http: None,
        raw: JsonObject::new(),
    }
}

fn config() -> ManagedAgentConfig {
    ManagedAgentConfig {
        provider: managed_agent_provider::OPENAI.into(),
        model: "gpt-test".to_owned(),
        api_key: SecretString::new("sk-test"),
    }
}

fn object<'a>(value: &'a JsonValue, label: &str) -> Result<&'a JsonObject, std::io::Error> {
    let JsonValue::Object(object) = value else {
        return Err(std::io::Error::other(format!("{label} must be an object")));
    };
    Ok(object)
}

fn object_field<'a>(object: &'a JsonObject, key: &str) -> Result<&'a JsonObject, std::io::Error> {
    let Some(value) = object.get(key) else {
        return Err(std::io::Error::other(format!("{key} is missing")));
    };
    self::object(value, key)
}

fn array_field<'a>(
    object: &'a JsonObject,
    key: &str,
) -> Result<&'a Vec<JsonValue>, std::io::Error> {
    let Some(JsonValue::Array(value)) = object.get(key) else {
        return Err(std::io::Error::other(format!("{key} is missing")));
    };
    Ok(value)
}

fn string(value: &str) -> JsonValue {
    JsonValue::String(value.to_owned())
}
