use serde_json::{json, Value};
use smol_workflow_engine::js_runtime::rquickjs::RQuickJSWorkflowRuntime;
use smol_workflow_engine::js_runtime::{
    WorkflowJSRuntime, WorkflowModuleInput, WorkflowModuleOutput, WorkflowRef, WorkflowRuntimeCall,
    WorkflowRuntimePoll, WorkflowRuntimeRequest, WorkflowRuntimeRequestResolution,
};
use std::fs;
use std::time::Duration;

#[derive(Default, Debug)]
struct RunTrace {
    calls: Vec<WorkflowRuntimeCall>,
    requests: Vec<WorkflowRuntimeRequest>,
}

fn run_to_completion(
    input: WorkflowModuleInput,
) -> anyhow::Result<(WorkflowModuleOutput, RunTrace)> {
    let mut execution = RQuickJSWorkflowRuntime::new().start_module(input)?;
    let mut trace = RunTrace::default();

    for _ in 0..100 {
        match execution.poll()? {
            WorkflowRuntimePoll::Call(call) => trace.calls.push(call),
            WorkflowRuntimePoll::Request(request) => {
                let id = request.id().to_string();
                let result = match &request {
                    WorkflowRuntimeRequest::Agent {
                        prompt, options, ..
                    } => echo_agent_result(prompt, options.as_ref()),
                    WorkflowRuntimeRequest::Workflow {
                        workflow_ref, args, ..
                    } => json!({
                        "workflowRef": workflow_ref,
                        "args": args,
                    }),
                    WorkflowRuntimeRequest::Sleep { .. } => {
                        trace.requests.push(request);
                        execution
                            .resolve_request(&id, WorkflowRuntimeRequestResolution::OkUndefined)?;
                        continue;
                    }
                };
                trace.requests.push(request);
                execution.resolve_request(&id, WorkflowRuntimeRequestResolution::Ok(result))?;
            }
            WorkflowRuntimePoll::Complete(output) => return Ok((output, trace)),
            WorkflowRuntimePoll::Pending => continue,
        }
    }

    anyhow::bail!("workflow did not complete within poll limit")
}

fn echo_agent_result(prompt: &str, options: Option<&Value>) -> Value {
    match options {
        Some(options) => json!({ "echo": prompt, "options": options }),
        None => json!({ "echo": prompt }),
    }
}

#[test]
fn rquickjs_exception_messages_include_stack_for_module_errors() {
    let source =
        fs::read_to_string("tests/fixtures/stack-error.workflow.js").expect("fixture should exist");

    let error = run_to_completion(WorkflowModuleInput::new(
        source,
        "stack-error.workflow.js",
        json!({}),
    ))
    .unwrap_err();
    let error = format!("{error:#}");

    assert!(
        error.contains("workflow module evaluation rejected: boom from module"),
        "unexpected error: {error}"
    );
    assert!(
        error.contains("at fail (stack-error.workflow.js:"),
        "expected stack frame in error: {error}"
    );
}

#[test]
fn rquickjs_exception_messages_include_stack_for_default_function_errors() {
    let source = fs::read_to_string("tests/fixtures/function-stack-error.workflow.js")
        .expect("fixture should exist");

    let error = run_to_completion(WorkflowModuleInput::new(
        source,
        "function-stack-error.workflow.js",
        json!({}),
    ))
    .unwrap_err();
    let error = format!("{error:#}");

    assert!(
        error.contains("workflow module rejected: boom from function"),
        "unexpected error: {error}"
    );
    assert!(
        error.contains("at fail (function-stack-error.workflow.js:"),
        "expected stack frame in error: {error}"
    );
}

#[test]
fn rquickjs_exception_messages_render_thrown_strings() {
    let source = fs::read_to_string("tests/fixtures/string-error.workflow.js")
        .expect("fixture should exist");

    let error = run_to_completion(WorkflowModuleInput::new(
        source,
        "string-error.workflow.js",
        json!({}),
    ))
    .unwrap_err();
    let error = format!("{error:#}");

    assert!(
        error.contains("workflow module evaluation rejected: boom from string"),
        "unexpected error: {error}"
    );
}

#[test]
fn rquickjs_executes_default_async_workflow_fixture() {
    let source = fs::read_to_string("tests/fixtures/injected-globals.workflow.js")
        .expect("fixture should exist");
    let (output, trace) = run_to_completion(WorkflowModuleInput::new(
        source,
        "injected-globals.workflow.js",
        json!({ "my-arg1": "alpha", "my-arg2": "beta" }),
    ))
    .expect("workflow should execute");

    assert_eq!(
        trace.calls[0],
        WorkflowRuntimeCall::Phase {
            name: "Research".into(),
            options: None
        }
    );
    assert_eq!(
        trace.calls[1],
        WorkflowRuntimeCall::Log {
            values: vec![
                json!("received"),
                json!({ "my-arg1": "alpha", "my-arg2": "beta" })
            ]
        }
    );
    assert_eq!(trace.requests.len(), 2);
    assert_eq!(output.result["first"]["echo"], "first: alpha");
    assert_eq!(output.result["second"]["echo"], "second: beta");
    assert_eq!(
        output.result["args"],
        json!({ "my-arg1": "alpha", "my-arg2": "beta" })
    );
}

#[test]
fn rquickjs_executes_top_level_module_result_fixture() {
    let source = fs::read_to_string("tests/fixtures/module-result.workflow.js")
        .expect("fixture should exist");
    let (output, trace) = run_to_completion(WorkflowModuleInput::new(
        source,
        "module-result.workflow.js",
        json!({ "my-arg1": "one", "my-arg2": "two" }),
    ))
    .expect("workflow should execute");

    assert_eq!(
        trace.calls[0],
        WorkflowRuntimeCall::Phase {
            name: "ModuleResult".into(),
            options: None
        }
    );
    assert_eq!(
        trace.calls[1],
        WorkflowRuntimeCall::Log {
            values: vec![
                json!("module result args"),
                json!({ "my-arg1": "one", "my-arg2": "two" })
            ]
        }
    );
    assert_eq!(trace.requests.len(), 2);
    assert_eq!(output.result["first"]["echo"], "first: one");
    assert_eq!(output.result["second"]["echo"], "second: two");
    assert_eq!(
        output.result["args"],
        json!({ "my-arg1": "one", "my-arg2": "two" })
    );
}

#[test]
fn rquickjs_blocks_common_host_access_and_randomness() {
    let (output, _trace) = run_to_completion(WorkflowModuleInput::new(
        r#"
export const meta = { name: "sandbox", description: "sandbox" };
const blockedGlobalNames = [
  'eval',
  'Function',
  'AsyncFunction',
  'Date',
  'fetch',
  'XMLHttpRequest',
  'WebSocket',
  'EventSource',
  'navigator',
  'location',
  'Deno',
  'Bun',
  'process',
  'require',
  'Buffer',
  '__dirname',
  '__filename',
];

export default {
  randomBlocked: (() => { try { Math.random(); return false; } catch { return true; } })(),
  randomOverwriteBlocked: (() => { try { Math.random = () => 0; return false; } catch { return true; } })(),
  randomStillBlocked: (() => { try { Math.random(); return false; } catch { return true; } })(),
  blockedGlobals: Object.fromEntries(blockedGlobalNames.map(name => [name, typeof globalThis[name]])),
  blockedGlobalMutationBlocked: Object.fromEntries(blockedGlobalNames.map((name) => {
    try { globalThis[name] = 1; return [name, false]; } catch { return [name, true]; }
  })),
};
"#,
        "sandbox.workflow.js",
        json!({}),
    ))
    .expect("workflow should execute");

    assert_eq!(output.result["randomBlocked"], true);
    assert_eq!(output.result["randomOverwriteBlocked"], true);
    assert_eq!(output.result["randomStillBlocked"], true);
    for name in [
        "eval",
        "Function",
        "AsyncFunction",
        "Date",
        "fetch",
        "XMLHttpRequest",
        "WebSocket",
        "EventSource",
        "navigator",
        "location",
        "Deno",
        "Bun",
        "process",
        "require",
        "Buffer",
        "__dirname",
        "__filename",
    ] {
        assert_eq!(
            output.result["blockedGlobals"][name], "undefined",
            "expected {name} to be blocked"
        );
        assert_eq!(
            output.result["blockedGlobalMutationBlocked"][name], true,
            "expected {name} global binding to be readonly"
        );
    }
}

#[test]
fn rquickjs_exposes_workflow_extra_sleep_request() {
    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(WorkflowModuleInput::new(
            r#"
import { sleep } from "workflow:extra";
export const meta = { name: "sleep", description: "sleep" };
const globalSleepType = typeof SW.extra.sleep;
const genericExtraType = typeof extra;
const value = await sleep(12);
export default { valueType: typeof value, globalSleepType, genericExtraType };
"#,
            "sleep.workflow.js",
            json!({}),
        ))
        .expect("workflow should start");

    let request = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Request(request) => break request,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected sleep request, got {other:?}"),
        }
    };

    let id = match request {
        WorkflowRuntimeRequest::Sleep { id, duration_ms } => {
            assert_eq!(duration_ms, 12);
            id
        }
        other => panic!("expected sleep request, got {other:?}"),
    };

    execution
        .resolve_request(&id, WorkflowRuntimeRequestResolution::OkUndefined)
        .expect("sleep should resolve");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };

    assert_eq!(
        output.result,
        json!({
            "valueType": "undefined",
            "globalSleepType": "function",
            "genericExtraType": "undefined",
        })
    );
}

#[test]
fn rquickjs_exposes_extra_on_workflow_context_without_global_extra() {
    let (output, _trace) = run_to_completion(WorkflowModuleInput::new(
        r#"
export const meta = { name: "ctx-extra", description: "ctx extra" };
export default async function workflow(input, ctx) {
  const value = await ctx.extra.sleep(1);
  return {
    valueType: typeof value,
    ctxExtraSleepType: typeof ctx.extra.sleep,
    ctxSwType: typeof ctx.SW,
    globalExtraType: typeof extra,
  };
}
"#,
        "ctx-extra.workflow.js",
        json!({}),
    ))
    .expect("workflow should execute");

    assert_eq!(
        output.result,
        json!({
            "valueType": "undefined",
            "ctxExtraSleepType": "function",
            "ctxSwType": "undefined",
            "globalExtraType": "undefined",
        })
    );
}

#[test]
fn rquickjs_validates_workflow_extra_sleep_bounds() {
    fn expect_sleep_rejection(source: &str, max_sleep_ms: u64, expected: &str) {
        let mut execution = RQuickJSWorkflowRuntime::new()
            .with_max_sleep_ms(max_sleep_ms)
            .start_module(WorkflowModuleInput::new(
                source,
                "sleep-invalid.workflow.js",
                json!({}),
            ))
            .expect("workflow should start");

        for _ in 0..20 {
            match execution.poll() {
                Ok(WorkflowRuntimePoll::Pending) => continue,
                Ok(other) => panic!("expected sleep validation rejection, got {other:?}"),
                Err(error) => {
                    assert!(
                        format!("{error:#}").contains(expected),
                        "unexpected error: {error:#}"
                    );
                    return;
                }
            }
        }
        panic!("workflow did not reject invalid sleep within poll limit");
    }

    expect_sleep_rejection(
        r#"
import { sleep } from "workflow:extra";
export const meta = { name: "sleep-invalid", description: "sleep-invalid" };
export default await sleep(11);
"#,
        10,
        "duration exceeds the maximum",
    );
    expect_sleep_rejection(
        r#"
import { sleep } from "workflow:extra";
export const meta = { name: "sleep-invalid", description: "sleep-invalid" };
export default await sleep(-1);
"#,
        10,
        "finite non-negative number",
    );
    expect_sleep_rejection(
        r#"
import { sleep } from "workflow:extra";
export const meta = { name: "sleep-invalid", description: "sleep-invalid" };
export default await sleep(Infinity);
"#,
        10,
        "finite non-negative number",
    );
}

#[test]
fn rquickjs_blocks_non_extra_imports() {
    let result = RQuickJSWorkflowRuntime::new().start_module(WorkflowModuleInput::new(
        r#"
import fs from "node:fs";
export const meta = { name: "blocked", description: "blocked" };
export default fs;
"#,
        "blocked.workflow.js",
        json!({}),
    ));
    let error = match result {
        Ok(_) => panic!("workflow should reject non-extra imports"),
        Err(error) => error,
    };

    assert!(
        format!("{error:#}").contains("workflow imports are restricted"),
        "unexpected error: {error:#}"
    );
}

#[test]
fn rquickjs_agent_inherits_current_phase_in_request_options() {
    let (output, trace) = run_to_completion(WorkflowModuleInput::new(
        r#"
export const meta = { name: "phase", description: "phase" };
phase("Research");
export default await agent("hello");
"#,
        "phase.workflow.js",
        json!({}),
    ))
    .expect("workflow should execute");

    assert_eq!(output.result["echo"], "hello");
    assert_eq!(output.result["options"], json!({ "phase": "Research" }));
    assert_eq!(
        trace.requests[0],
        WorkflowRuntimeRequest::Agent {
            id: "1".into(),
            prompt: "hello".into(),
            options: Some(json!({ "phase": "Research" })),
        }
    );
}

#[test]
fn rquickjs_supports_child_workflow_requests() {
    let (output, trace) = run_to_completion(WorkflowModuleInput::new(
        r#"
export const meta = { name: "parent", description: "parent" };
const first = await workflow("child-by-name", { x: 1 });
const second = await workflow({ scriptPath: "./child.workflow.js" }, { y: 2 });
export default { first, second };
"#,
        "parent.workflow.js",
        json!({}),
    ))
    .expect("workflow should execute");

    assert_eq!(trace.requests.len(), 2);
    assert_eq!(
        trace.requests[0],
        WorkflowRuntimeRequest::Workflow {
            id: "1".into(),
            workflow_ref: WorkflowRef::Name("child-by-name".into()),
            args: Some(json!({ "x": 1 })),
        }
    );
    assert_eq!(
        trace.requests[1],
        WorkflowRuntimeRequest::Workflow {
            id: "2".into(),
            workflow_ref: WorkflowRef::ScriptPath {
                script_path: "./child.workflow.js".into()
            },
            args: Some(json!({ "y": 2 })),
        }
    );
    assert_eq!(
        output.result["first"]["workflowRef"],
        json!("child-by-name")
    );
    assert_eq!(
        output.result["second"]["workflowRef"],
        json!({ "scriptPath": "./child.workflow.js" })
    );
}

#[test]
fn rquickjs_exposes_budget_global() {
    let mut input = WorkflowModuleInput::new(
        r#"
export const meta = { name: "budget", description: "budget" };
export default {
  total: budget.total,
  spent: budget.spent(),
  remaining: budget.remaining(),
};
"#,
        "budget.workflow.js",
        json!({}),
    );
    input.budget.total = Some(100);
    input.budget.spent = 40;

    let (output, _trace) = run_to_completion(input).expect("workflow should execute");

    assert_eq!(
        output.result,
        json!({ "total": 100, "spent": 40, "remaining": 60 })
    );
}

#[test]
fn rquickjs_parallel_queues_multiple_agent_requests() {
    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(WorkflowModuleInput::new(
            r#"
export const meta = { name: "parallel", description: "parallel" };
export default await parallel([
  () => agent("first"),
  () => agent("second"),
]);
"#,
            "parallel.workflow.js",
            json!({}),
        ))
        .expect("workflow should start");

    let first = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Request(request) => break request,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected first request, got {other:?}"),
        }
    };
    assert_eq!(
        first,
        WorkflowRuntimeRequest::Agent {
            id: "1".into(),
            prompt: "first".into(),
            options: None,
        }
    );

    let second = execution.poll().expect("workflow should poll");
    assert_eq!(second, WorkflowRuntimePoll::Request(first.clone()));

    execution
        .resolve_request("1", WorkflowRuntimeRequestResolution::Ok(json!("one")))
        .expect("first request should resolve");

    let second = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Request(request) => break request,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected second request, got {other:?}"),
        }
    };
    assert_eq!(
        second,
        WorkflowRuntimeRequest::Agent {
            id: "2".into(),
            prompt: "second".into(),
            options: None,
        }
    );

    execution
        .resolve_request("2", WorkflowRuntimeRequestResolution::Ok(json!("two")))
        .expect("second request should resolve");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };
    assert_eq!(output.result, json!(["one", "two"]));
}

#[test]
fn rquickjs_readonly_proxy_preserves_identity() {
    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(WorkflowModuleInput::new(
            r#"
export const meta = { name: "readonly-identity", description: "readonly identity" };
export default {
  sameNestedObject: args.nested === args.nested,
  sameNestedArray: args.items === args.items,
};
"#,
            "readonly-identity.workflow.js",
            json!({ "nested": { "value": 1 }, "items": [1, 2] }),
        ))
        .expect("workflow should start");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };

    assert_eq!(
        output.result,
        json!({ "sameNestedObject": true, "sameNestedArray": true })
    );
}

#[test]
fn rquickjs_timeout_is_refreshed_after_host_request_wait() {
    let mut input = WorkflowModuleInput::new(
        r#"
export const meta = { name: "timeout-refresh", description: "timeout refresh" };
const value = await agent("wait");
export default { value, afterWait: true };
"#,
        "timeout-refresh.workflow.js",
        json!({}),
    );
    input.sandbox.timeout = Duration::from_millis(10);

    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(input)
        .expect("workflow should start");

    let request = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Request(request) => break request,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected request, got {other:?}"),
        }
    };
    std::thread::sleep(Duration::from_millis(30));
    execution
        .resolve_request(
            request.id(),
            WorkflowRuntimeRequestResolution::Ok(json!("done")),
        )
        .expect("request should resolve after host wait");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };

    assert_eq!(output.result, json!({ "value": "done", "afterWait": true }));
}

#[test]
fn rquickjs_hides_internal_helpers_and_blocks_additional_host_access() {
    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(WorkflowModuleInput::new(
            r#"
export const meta = { name: "sandbox", description: "sandbox" };
const mutationBlocked = [];
for (const name of ['args', 'budget', 'agent', 'workflow', 'log', 'phase', 'parallel', 'pipeline', 'SW']) {
  try { globalThis[name] = null; } catch { mutationBlocked.push(name); }
}

export default {
  dateType: typeof Date,
  readonlyType: typeof __readonly,
  extraType: typeof extra,
  swExtraSleepType: typeof SW.extra.sleep,
  swExtraMutationBlocked: (() => { try { SW.extra.sleep = null; return false; } catch { return true; } })(),
  randomBlocked: (() => { try { Math.random(); return false; } catch { return true; } })(),
  mutationBlocked,
};
"#,
            "sandbox.workflow.js",
            json!({}),
        ))
        .expect("workflow should start");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };

    assert_eq!(
        output.result,
        json!({
            "dateType": "undefined",
            "readonlyType": "undefined",
            "extraType": "undefined",
            "swExtraSleepType": "function",
            "swExtraMutationBlocked": true,
            "randomBlocked": true,
            "mutationBlocked": ["args", "budget", "agent", "workflow", "log", "phase", "parallel", "pipeline", "SW"],
        })
    );
}

#[test]
fn rquickjs_budget_global_is_readonly() {
    let mut execution = RQuickJSWorkflowRuntime::new()
        .start_module(WorkflowModuleInput::new(
            r#"
export const meta = { name: "budget-readonly", description: "budget-readonly" };
const blocked = [];
for (const [label, mutate] of [
  ['set-total', () => { budget.total = 1 }],
  ['set-extra', () => { budget.extra = 'nope' }],
  ['delete-spent', () => { delete budget.spent }],
]) {
  try { mutate(); } catch { blocked.push(label); }
}
export default {
  blocked,
  total: budget.total,
  spent: budget.spent(),
  remaining: budget.remaining(),
  extra: budget.extra ?? null,
};
"#,
            "budget-readonly.workflow.js",
            json!({}),
        ))
        .expect("workflow should start");

    let output = loop {
        match execution.poll().expect("workflow should poll") {
            WorkflowRuntimePoll::Complete(output) => break output,
            WorkflowRuntimePoll::Pending => continue,
            other => panic!("expected completion, got {other:?}"),
        }
    };

    assert_eq!(
        output.result,
        json!({
            "blocked": ["set-total", "set-extra", "delete-spent"],
            "total": null,
            "spent": 0,
            "remaining": null,
            "extra": null,
        })
    );
}
