use tempfile::TempDir;

use crate::error::Error;
use crate::store::TTL;
use crate::store::{FollowOption, Frame, ReadOptions, Store};
use std::collections::HashSet;

macro_rules! validate_actor_output_frame {
    ($frame_expr:expr, $expected_topic:expr, $handler:expr, $trigger:expr, $state_frame:expr) => {{
        let frame = $frame_expr; // Capture the expression result into a local variable
        assert_eq!(frame.topic, $expected_topic, "Unexpected topic");
        let meta = frame.meta.as_ref().expect("Meta is None");
        assert_eq!(
            meta["actor_id"],
            $handler.id.to_string(),
            "Unexpected actor_id"
        );
        assert_eq!(
            meta["frame_id"],
            $trigger.id.to_string(),
            "Unexpected frame_id"
        );
        let state_frame: Option<&Frame> = $state_frame; // Ensure the type is Option<&Frame>
        if let Some(state_frame) = state_frame {
            assert_eq!(
                meta["state_id"],
                state_frame.id.to_string(),
                "Unexpected state_id"
            );
        }
    }};
}

macro_rules! validate_actor_output_frames {
    ($recver:expr, $handler:expr, $trigger:expr, $state_frame:expr, [$( $topic:expr ),+ $(,)?]) => {{
        let state_frame: Option<&Frame> = $state_frame; // Explicit type for state_frame
        $(
            validate_actor_output_frame!(
                $recver.recv().await.unwrap(),
                $topic,
                $handler,
                $trigger,
                state_frame
            );
        )+
    }};
}

macro_rules! validate_frame {
    ($frame:expr, { $( $field:ident : $value:expr ),* $(,)? }) => {{
        let frame = $frame;
        $(
            validate_field!(frame, $field : $value);
        )*
    }};
}

macro_rules! validate_field {
    // Validation for the "topic" field
    ($frame:expr, topic : $value:expr) => {{
        assert_eq!(
            $frame.topic, $value,
            "Topic mismatch: expected '{}', got '{}'",
            $value, $frame.topic
        );
    }};
    // Validation for the "error" field
    ($frame:expr, error : $value:expr) => {{
        let meta = $frame.meta.as_ref().expect("Meta is None");
        let error_message = meta["error"]
            .as_str()
            .expect("Expected 'error' to be a string");
        assert!(
            error_message.contains($value),
            "Error message '{}' does not contain expected substring '{}'",
            error_message,
            $value
        );
    }};
    // Validation for meta fields like "actor", "trigger", "state"
    ($frame:expr, $field:ident : $value:expr) => {{
        let meta = $frame.meta.as_ref().expect("Meta is None");
        let key = match stringify!($field) {
            "handler" => "actor_id",
            "trigger" => "frame_id",
            "state" => "state_id",
            _ => panic!("Invalid field: {}", stringify!($field)),
        };
        assert_eq!(
            meta[key],
            $value.id.to_string(),
            "{} mismatch: expected '{}', got '{}'",
            key,
            $value.id.to_string(),
            meta[key]
        );
    }};
}

#[tokio::test]
async fn test_register_invalid_closure_no_args() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.threshold".to_string()
    );

    // Attempt to register a closure with no arguments
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.invalid.create")
                .hash(store.cas_insert(r#"{run: {|| 42}}"#).await.unwrap())
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.invalid.create".to_string()
    );

    validate_frame!(
        recver.recv().await.unwrap(), {
        topic: "xs.actor.invalid.invalid",
        handler: frame_actor,
        error: "Closure must accept exactly 2 params",
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_register_invalid_closure_old_one_arg() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.threshold".to_string()
    );

    // Attempt to register old 1-arg closure shape (no state param)
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.invalid.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame| $frame}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.invalid.create".to_string()
    );

    validate_frame!(
        recver.recv().await.unwrap(), {
        topic: "xs.actor.invalid.invalid",
        handler: frame_actor,
        error: "Closure must accept exactly 2 params",
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_register_parse_error() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.threshold".to_string()
    );

    // Attempt to register a closure which should fail to parse
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.invalid.create")
                .hash(
                    store
                        .cas_insert(
                            r#"
                        {
                          run: {|frame, state = null|
                            .last index.html | .cas
                          }
                        }
                        "#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    // Ensure the register frame is processed
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.invalid.create".to_string()
    );

    // Expect an inactive frame to be appended
    validate_frame!(
        recver.recv().await.unwrap(), {
        topic: "xs.actor.invalid.invalid",
        handler: frame_actor,
        error: "Parse error", // Expecting parse error details
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
// This test is to ensure that an actor does not run its own output
async fn test_no_self_loop() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.threshold".to_string()
    );

    // Register actor that would run its own output if not prevented
    store
        .append(
            Frame::builder("xs.actor.echo.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame, state = null| {out: $frame, next: $state}}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.echo.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.echo.active");

    // note we don't see an echo of the echo.active frame

    // Trigger the actor
    store.append(Frame::builder("a-frame").build()).unwrap();
    // we should see the trigger, and then a single echo
    assert_eq!(recver.recv().await.unwrap().topic, "a-frame");
    assert_eq!(recver.recv().await.unwrap().topic, "echo.out");

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_essentials() {
    let (store, _temp_dir) = setup_test_environment().await;

    // Create initial frames
    let pew1 = store.append(Frame::builder("pew").build()).unwrap();
    let pew2 = store.append(Frame::builder("pew").build()).unwrap();

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "pew");
    assert_eq!(recver.recv().await.unwrap().topic, "pew");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Create a pointer frame that contains indicates we've processed pew1
    let _pointer_frame = store
        .append(
            Frame::builder("action.out")
                .meta(serde_json::json!({
                    "frame_id": pew1.id.to_string()
                }))
                .build(),
        )
        .unwrap();

    // Register actor with start pointing to the content of action.out
    let actor_proto = Frame::builder("xs.actor.action.create")
        .hash(
            store
                .cas_insert(
                    r#"
                    {
                      run: {|frame, state = null|
                        if $frame.topic == "pew" {
                          {out: {status: "processed"}, next: $state}
                        } else {
                          {next: $state}
                        }
                      }

                      start: (.last "action.out" | get meta.frame_id)
                    }"#,
                )
                .await
                .unwrap(),
        )
        .build();

    // Start actor
    let frame_actor = store.append(actor_proto.clone()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "action.out"); // The pointer frame
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.action.create");

    // Assert active frame has the correct meta
    let frame = recver.recv().await.unwrap();
    assert_eq!(frame.topic, "xs.actor.action.active");
    let meta = frame.meta.unwrap();
    assert_eq!(meta["actor_id"], frame_actor.id.to_string());
    assert_eq!(meta["start"]["after"], pew1.id.to_string());

    // Should process frame2 (since pew1 was before the start point)
    validate_frame!(recver.recv().await.unwrap(), {
        topic: "action.out",
        handler: &frame_actor,
        trigger: &pew2,
    });

    assert_no_more_frames(&mut recver).await;

    // Unregister actor and restart - should resume from cursor
    store
        .append(Frame::builder("xs.actor.action.term").build())
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.action.term");
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.action.fin.term"
    );

    assert_no_more_frames(&mut recver).await;

    // Restart actor
    let frame_actor_2 = store.append(actor_proto.clone()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.action.create");

    // Assert active frame has the correct meta
    let frame = recver.recv().await.unwrap();
    assert_eq!(frame.topic, "xs.actor.action.active");
    let meta = frame.meta.unwrap();
    assert_eq!(meta["actor_id"], frame_actor_2.id.to_string());
    assert_eq!(meta["start"]["after"], pew2.id.to_string());

    let pew3 = store.append(Frame::builder("pew").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "pew");

    // Should resume processing from pew3 on
    validate_frame!(recver.recv().await.unwrap(), {
        topic: "action.out",
        handler: &frame_actor_2,
        trigger: &pew3,
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_unregister_on_error() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // This frame will trigger the error when the actor comes online
    let frame_trigger = store.append(Frame::builder("trigger").build()).unwrap();
    validate_frame!(recver.recv().await.unwrap(), {topic: "trigger"});

    // add an additional frame, which shouldn't be processed, as the actor should immediately
    // unregister
    let _ = store.append(Frame::builder("trigger").build()).unwrap();
    validate_frame!(recver.recv().await.unwrap(), {topic: "trigger"});

    // Start actor
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.error.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = null|
                            let x = {"foo": null}
                            $x.foo.bar  # Will error at runtime - null access
                          }

                          start: "first"
                         }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.error.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.error.active");

    // Expect an inactive frame to be appended
    validate_frame!(recver.recv().await.unwrap(), {
        topic: "xs.actor.error.fin.error",
        handler: &frame_actor,
        trigger: &frame_trigger,
        error: "nothing doesn't support cell paths",
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_return_options() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register actor with return_options
    let actor_proto = Frame::builder("xs.actor.echo.create")
        .hash(
            store
                .cas_insert(
                    r#"{
                      return_options: {
                        suffix: ".warble"
                        ttl: "last:1"
                      }

                      run: {|frame, state = null|
                        if $frame.topic == "ping" {
                          {out: {reply: "pong"}, next: $state}
                        } else {
                          {next: $state}
                        }
                      }
                    }"#,
                )
                .await
                .unwrap(),
        )
        .build();

    let frame_actor = store.append(actor_proto).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.echo.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.echo.active");

    // Send first ping
    let frame1 = store.append(Frame::builder("ping").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "ping");

    // Check response has custom suffix and right meta
    let response1 = recver.recv().await.unwrap();
    assert_eq!(response1.topic, "echo.warble");
    assert_eq!(response1.ttl, Some(TTL::Last(1)));
    let meta = response1.meta.unwrap();
    assert_eq!(meta["actor_id"], frame_actor.id.to_string());
    assert_eq!(meta["frame_id"], frame1.id.to_string());

    // Send second ping - should only see newest response due to Last(1)
    let frame2 = store.append(Frame::builder("ping").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "ping");

    let response2 = recver.recv().await.unwrap();
    assert_eq!(response2.topic, "echo.warble");
    let meta = response2.meta.unwrap();
    assert_eq!(meta["frame_id"], frame2.id.to_string());

    // Only newest response should be in store
    store.wait_for_gc().await;
    let options = ReadOptions::default();
    let recver = store.read(options).await;
    use tokio_stream::StreamExt;
    let frames: Vec<_> = tokio_stream::wrappers::ReceiverStream::new(recver)
        .filter(|f| f.topic == "echo.warble")
        .collect::<Vec<_>>()
        .await;
    assert_eq!(frames.len(), 1);
    assert_eq!(
        frames[0].meta.as_ref().unwrap()["frame_id"],
        frame2.id.to_string()
    );
}

#[tokio::test]
async fn test_binary_return_value() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register actor that returns binary msgpack data
    let actor_proto = Frame::builder("xs.actor.binary.create")
        .hash(
            store
                .cas_insert(
                    r#"{
                      return_options: { target: "cas" }
                      run: {|frame, state = null|
                        if $frame.topic == "trigger" {
                          {out: ('test' | to msgpackz), next: $state}
                        } else {
                          {next: $state}
                        }
                      }
                    }"#,
                )
                .await
                .unwrap(),
        )
        .build();

    let frame_actor = store.append(actor_proto).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.binary.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.binary.active");

    // Send trigger frame
    let trigger_frame = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    // Check that the binary output frame was created
    let output_frame = recver.recv().await.unwrap();
    assert_eq!(output_frame.topic, "binary.out");

    // Verify metadata
    let meta = output_frame.meta.unwrap();
    assert_eq!(meta["actor_id"], frame_actor.id.to_string());
    assert_eq!(meta["frame_id"], trigger_frame.id.to_string());

    // Verify the binary content is stored correctly (not "null")
    let stored_content = store.cas_read(&output_frame.hash.unwrap()).await.unwrap();

    // The content should be actual msgpack binary data, not the string "null"
    assert_ne!(stored_content, b"null");
    assert_ne!(stored_content, b"\"null\"");

    // Verify it's actually msgpack binary data by checking it's not empty and not JSON
    assert!(!stored_content.is_empty());

    // Verify it contains the word "test" in the binary data
    let content_str = String::from_utf8_lossy(&stored_content);
    assert!(content_str.contains("test"));

    // Verify it's not the JSON null representation
    assert_ne!(content_str, "null");
    assert_ne!(content_str, "\"null\"");

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_custom_append() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    let actor_proto = Frame::builder("xs.actor.action.create")
        .hash(
            store
                .cas_insert(
                    r#"{
                      run: {|frame, state = null|
                       if $frame.topic != "trigger" { {next: $state} } else {
                         "1" | .append topic1 --meta {"t": "1"}
                         "2" | .append topic2 --meta {"t": "2"}
                         {out: {value: "out"}, next: $state}
                       }
                       }
                    }"#,
                )
                .await
                .unwrap(),
        )
        .build();

    // Start actor
    let frame_actor = store.append(actor_proto.clone()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.action.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.action.active");

    let trigger_frame = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    validate_actor_output_frames!(
        recver,
        frame_actor,
        trigger_frame,
        None,
        ["topic1", "topic2", "action.out"]
    );

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_actor_replacement() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register first actor
    let _ = store
        .append(
            Frame::builder("xs.actor.h.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{run: {|frame, state = null|
                        if $frame.topic == "trigger" {
                          {out: {handler: "handler1"}, next: $state}
                        } else {
                          {next: $state}
                        }
                    }}"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.active");

    // Register second actor for same topic
    let actor2 = store
        .append(
            Frame::builder("xs.actor.h.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{run: {|frame, state = null|
                        if $frame.topic == "trigger" {
                          {out: {handler: "handler2"}, next: $state}
                        } else {
                          {next: $state}
                        }
                    }}"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");
    let topics: HashSet<_> = [
        recver.recv().await.unwrap().topic,
        recver.recv().await.unwrap().topic,
    ]
    .into_iter()
    .collect();
    assert_eq!(
        topics,
        HashSet::from([
            "xs.actor.h.replaced".to_string(),
            "xs.actor.h.active".to_string(),
        ])
    );

    // Send trigger - should be handled by actor2
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    // Verify actor2 processed it
    let response = recver.recv().await.unwrap();
    assert_eq!(response.topic, "h.out");
    let meta = response.meta.as_ref().unwrap();
    assert_eq!(meta["actor_id"], actor2.id.to_string());
    assert_eq!(meta["frame_id"], trigger.id.to_string());
    assert_eq!(meta["handler"], "handler2");

    assert_no_more_frames(&mut recver).await;
}

/// Invariant I3 at the live (dispatcher) level: when a hot-replace .create
/// fails to parse, the previous known-good actor is re-spawned so the
/// system isn't left empty. Closes deficiency #5.
#[tokio::test]
async fn inv3_live_hot_replace_broken_falls_back_to_confirmed() {
    let (store, _temp_dir) = setup_test_environment().await;

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // First .create: a valid actor that echoes its trigger.
    let good_hash = store
        .cas_insert(
            r#"{run: {|frame, state = null|
                if $frame.topic == "trigger" {
                  {out: {who: "good"}, next: $state}
                } else {
                  {next: $state}
                }
            }}"#,
        )
        .await
        .unwrap();
    let _create_good = store
        .append(
            Frame::builder("xs.actor.h.create")
                .hash(good_hash.clone())
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.active");

    // Second .create: a closure with the wrong signature, will parse-fail.
    let bad_hash = store.cas_insert(r#"{run: {|| 42}}"#).await.unwrap();
    let _create_bad = store
        .append(Frame::builder("xs.actor.h.create").hash(bad_hash).build())
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");

    // Expected sequence (order between .replaced and .invalid can vary
    // since they're emitted by different tasks): the running actor sees
    // the new .create and exits with .replaced, the dispatcher's
    // try_start_actor rejects the new one with .invalid, and the live
    // fallback then re-spawns from the confirmed good frame, producing a
    // new .active.
    let mut topics = std::collections::HashSet::new();
    for _ in 0..3 {
        topics.insert(recver.recv().await.unwrap().topic);
    }
    assert!(topics.contains("xs.actor.h.replaced"));
    assert!(topics.contains("xs.actor.h.invalid"));
    assert!(topics.contains("xs.actor.h.active"));

    // The fallback spawned a new instance running the good script, so a
    // trigger should still be handled.
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    let resp = recver.recv().await.unwrap();
    assert_eq!(resp.topic, "h.out");
    let meta = resp.meta.as_ref().unwrap();
    assert_eq!(meta["frame_id"], trigger.id.to_string());
    assert_eq!(meta["who"], "good");

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_actor_with_module() -> Result<(), Error> {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register a VFS module via *.nu topic
    store
        .append(
            Frame::builder("xs.module.mymod")
                .hash(
                    store
                        .cas_insert(
                            r#"
                    # Add two numbers and format result
                    export def add_nums [x, y] {
                        $"sum is ($x + $y)"
                    }
                    "#,
                        )
                        .await?,
                )
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.module.mymod");

    // Create actor that uses the VFS module
    let actor_script = r#"{
            run: {|frame, state = null|
                if $frame.topic == "trigger" {
                  use mymod
                  {out: {result: (mymod add_nums 40 2)}, next: $state}
                } else {
                  {next: $state}
                }
            }
        }"#;
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.test.create")
                .hash(store.cas_insert(&actor_script).await?)
                .build(),
        )
        .unwrap();

    // Wait for actor registration
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.active");

    // Send trigger frame
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    // Get actor output
    let output = recver.recv().await.unwrap();
    validate_actor_output_frame!(&output, "test.out", frame_actor, trigger, None);

    // Verify output content
    assert_eq!(output.meta.as_ref().unwrap()["result"], "sum is 42");

    assert_no_more_frames(&mut recver).await;
    Ok(())
}

#[tokio::test]
async fn test_actor_preserve_env() -> Result<(), Error> {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    let _ = store
        .append(
            Frame::builder("abc.init")
                .hash(store.cas_insert(r#"42"#).await.unwrap())
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "abc.init");

    let _ = store
        .append(
            Frame::builder("abc.delta")
                .hash(store.cas_insert(r#"2"#).await.unwrap())
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "abc.delta");

    let frame_actor = store
        .append(
            Frame::builder("xs.actor.test.create")
                .hash(store.cas_insert_sync(
                    r#"
                    $env.abc = .last abc.init | .cas $in.hash | from json

                    def --env inc-abc [] {
                        $env.abc = $env.abc + (.last abc.delta | .cas $in.hash | from json)
                        $env.abc
                    }

                    {
                        run: {|frame, state = null|
                            if $frame.topic == "trigger" {
                              {out: {value: (inc-abc)}, next: $state}
                            } else {
                              {next: $state}
                            }
                        }
                    }
                    "#,
                )?)
                .build(),
        )
        .unwrap();

    // Wait for actor registration
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.active");

    // Send trigger frame
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    // Get actor output
    let output = recver.recv().await.unwrap();
    validate_actor_output_frame!(&output, "test.out", frame_actor, trigger, None);

    // Verify output content shows the env var value
    assert_eq!(output.meta.as_ref().unwrap()["value"], 44);

    // Send trigger frame
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    // Get actor output
    let output = recver.recv().await.unwrap();
    validate_actor_output_frame!(&output, "test.out", frame_actor, trigger, None);

    // Verify output content shows the env var value
    assert_eq!(output.meta.as_ref().unwrap()["value"], 46);

    assert_no_more_frames(&mut recver).await;
    Ok(())
}

#[tokio::test]
async fn test_state_threading() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor with counter state: emits current state on trigger, increments
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.counter.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = 0|
                            if $frame.topic == "trigger" {
                              {out: {count: $state}, next: ($state + 1)}
                            } else {
                              {next: $state}
                            }
                          }
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.active"
    );

    // First trigger: state=0 emitted, state becomes 1
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 0);

    // Send a non-trigger frame, should be skipped (no output), state preserved
    store.append(Frame::builder("noise").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "noise");

    assert_no_more_frames(&mut recver).await;

    // Second trigger: state=1 emitted, state becomes 2
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 1);

    // Third trigger: state=2 emitted, state becomes 3
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    let meta = output.meta.as_ref().unwrap();
    assert_eq!(meta["actor_id"], frame_actor.id.to_string());
    assert_eq!(meta["count"], 2);

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_out_only_stops() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor returns {out: "goodbye"}, emits output then self-terminates
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.stopper.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = null|
                            {out: {msg: "goodbye"}}
                          }
                          start: "first"
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    // Append a trigger before the actor starts processing (start: "first")
    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.stopper.create"
    );
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.stopper.active"
    );

    // Should see output, then unregistered
    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "stopper.out");
    assert_eq!(output.meta.as_ref().unwrap()["msg"], "goodbye");

    validate_frame!(recver.recv().await.unwrap(), {
        topic: "xs.actor.stopper.fin.ok",
        handler: &frame_actor,
        trigger: &trigger,
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_nothing_stops() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor returns null, self-terminates with no output
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.stopper.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = null|
                            null
                          }
                          start: "first"
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.stopper.create"
    );
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.stopper.active"
    );

    // Should see unregistered (no output frame)
    validate_frame!(recver.recv().await.unwrap(), {
        topic: "xs.actor.stopper.fin.ok",
        handler: &frame_actor,
        trigger: &trigger,
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_extra_keys_error() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor returns record with extra keys, should error
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.bad.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = null|
                            {out: 1, next: 2, bad: 3}
                          }
                          start: "first"
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.bad.create");
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.bad.active");

    validate_frame!(recver.recv().await.unwrap(), {
        topic: "xs.actor.bad.fin.error",
        handler: &frame_actor,
        trigger: &trigger,
        error: "Unexpected key 'bad'",
    });

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_initial_config() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor with initial: 10, closure default state = 0
    // initial overrides the default
    store
        .append(
            Frame::builder("xs.actor.counter.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          initial: 10
                          run: {|frame, state = 0|
                            if $frame.topic == "trigger" {
                              {out: {count: $state}, next: ($state + 1)}
                            } else {
                              {next: $state}
                            }
                          }
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.active"
    );

    // First trigger: should emit 10 (from initial), not 0 (from default)
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 10);

    // Second trigger: should emit 11
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 11);

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_initial_config_required_state() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor with 2 required params and initial provided
    store
        .append(
            Frame::builder("xs.actor.counter.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          initial: 100
                          run: {|frame, state|
                            if $frame.topic == "trigger" {
                              {out: {count: $state}, next: ($state + 1)}
                            } else {
                              {next: $state}
                            }
                          }
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.active"
    );

    // First trigger: should emit 100
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 100);

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_required_state_defaults_to_null() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor with 2 required params but no initial, state defaults to null
    store
        .append(
            Frame::builder("xs.actor.test.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state|
                            if $frame.topic == "trigger" {
                              {out: {is_null: ($state == null)}, next: 42}
                            } else {
                              {next: $state}
                            }
                          }
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.test.active");

    // First trigger: state is null, emits true
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "test.out");
    assert_eq!(output.meta.as_ref().unwrap()["is_null"], true);

    // Second trigger: state is 42, emits false
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "test.out");
    assert_eq!(output.meta.as_ref().unwrap()["is_null"], false);

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_default_param_value() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor with no initial config, closure default state = 42
    store
        .append(
            Frame::builder("xs.actor.counter.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = 42|
                            if $frame.topic == "trigger" {
                              {out: {count: $state}, next: ($state + 1)}
                            } else {
                              {next: $state}
                            }
                          }
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.counter.active"
    );

    // First trigger: should emit 42 (from closure default)
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 42);

    // Second trigger: should emit 43
    store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");

    let output = recver.recv().await.unwrap();
    assert_eq!(output.topic, "counter.out");
    assert_eq!(output.meta.as_ref().unwrap()["count"], 43);

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_non_record_return_error() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Actor returns a bare string, not a valid return shape
    let frame_actor = store
        .append(
            Frame::builder("xs.actor.bad.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                          run: {|frame, state = null|
                            "bare string"
                          }
                          start: "first"
                        }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    let trigger = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.bad.create");
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.bad.active");

    validate_frame!(recver.recv().await.unwrap(), {
        topic: "xs.actor.bad.fin.error",
        handler: &frame_actor,
        trigger: &trigger,
        error: "Closure must return a record",
    });

    assert_no_more_frames(&mut recver).await;
}

async fn assert_no_more_frames(recver: &mut tokio::sync::mpsc::Receiver<Frame>) {
    let timeout = tokio::time::sleep(std::time::Duration::from_millis(50));
    tokio::pin!(timeout);
    tokio::select! {
        Some(frame) = recver.recv() => {
            panic!("Unexpected frame processed: {:?}", frame);
        }
        _ = &mut timeout => {
            // Success - no additional frames were processed
        }
    }
}

#[tokio::test]
async fn test_record_out_goes_to_meta() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register actor that returns a record as out
    store
        .append(
            Frame::builder("xs.actor.metaout.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                              run: {|frame, state = 0|
                                if $frame.topic == "sale" {
                                  let total = $state + $frame.meta.amount
                                  {out: {total: $total}, next: $total}
                                } else {
                                  {next: $state}
                                }
                              }
                            }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.metaout.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.metaout.active"
    );

    // Send a sale frame with amount in meta
    store
        .append(
            Frame::builder("sale")
                .meta(serde_json::json!({"amount": 42}))
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "sale");

    // The output frame should have the record in meta, not in CAS
    let out_frame = recver.recv().await.unwrap();
    assert_eq!(out_frame.topic, "metaout.out");
    assert!(
        out_frame.hash.is_none(),
        "Record output should not be stored in CAS"
    );
    let meta = out_frame.meta.as_ref().unwrap();
    assert_eq!(meta["total"], 42);
    // actor_id and frame_id should still be present
    assert!(meta.get("actor_id").is_some());
    assert!(meta.get("frame_id").is_some());

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_non_record_out_errors_without_cas_target() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register actor that returns a string as out (non-record, no target: "cas")
    store
        .append(
            Frame::builder("xs.actor.strerr.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                              run: {|frame, state = null|
                                {out: "a plain string", next: $state}
                              }
                            }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.strerr.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.strerr.active");

    // Trigger the actor
    store.append(Frame::builder("ping").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "ping");

    // Should unregister with an error because non-record out needs target: "cas"
    let frame = recver.recv().await.unwrap();
    assert_eq!(frame.topic, "xs.actor.strerr.fin.error");
    let meta = frame.meta.as_ref().unwrap();
    let error = meta["error"].as_str().unwrap();
    assert!(
        error.contains("record") || error.contains("target"),
        "Expected error about non-record output, got: {error}"
    );

    assert_no_more_frames(&mut recver).await;
}

#[tokio::test]
async fn test_cas_target_allows_non_record_out() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Register actor with target: "cas" that returns a string
    store
        .append(
            Frame::builder("xs.actor.cascfg.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{
                              run: {|frame, state = null|
                                {out: "stored in cas", next: $state}
                              }
                              return_options: { target: "cas" }
                            }"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();

    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.cascfg.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.cascfg.active");

    // Trigger the actor
    store.append(Frame::builder("ping").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "ping");

    // Output should be in CAS
    let out_frame = recver.recv().await.unwrap();
    assert_eq!(out_frame.topic, "cascfg.out");
    assert!(out_frame.hash.is_some(), "CAS target should store in CAS");
    let content = store.cas_read(&out_frame.hash.unwrap()).await.unwrap();
    let content_str = std::str::from_utf8(&content).unwrap();
    assert!(content_str.contains("stored in cas"));

    assert_no_more_frames(&mut recver).await;
}

// An actor that was registered and then stopped emits an `xs.actor.<name>.fin.*`
// carrying meta.actor_id equal to its `xs.actor.<name>.create` frame id. On
// restart the dispatcher must compact that pair away and not resurrect the
// actor (invariant I1).
#[tokio::test]
async fn test_unregistered_actor_not_restarted_on_replay() {
    let temp_dir = TempDir::new().unwrap();
    let store = Store::new(temp_dir.path().to_path_buf()).unwrap();

    let register = store
        .append(
            Frame::builder("xs.actor.greeter.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame, state = null| $frame}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    store
        .append(
            Frame::builder("xs.actor.greeter.fin.term")
                .meta(serde_json::json!({ "actor_id": register.id.to_string() }))
                .build(),
        )
        .unwrap();

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    {
        let store = store.clone();
        drop(tokio::spawn(async move {
            crate::processor::actor::run(store).await.unwrap();
        }));
    }

    // History replays, then the threshold. The actor must not start again, so
    // no `greeter.active` frame should follow.
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.greeter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.greeter.fin.term"
    );
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");
    assert_no_more_frames(&mut recver).await;
}

// Invariant I2: an actor with an .active in history and no subsequent
// terminal frame is restarted on replay. The fresh dispatcher should
// emit a new .active for the same source create.
#[tokio::test]
async fn inv2_actor_with_active_resumes_on_replay() {
    let temp_dir = TempDir::new().unwrap();
    let store = Store::new(temp_dir.path().to_path_buf()).unwrap();

    let create = store
        .append(
            Frame::builder("xs.actor.greeter.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame, state = null| {next: $state}}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    store
        .append(
            Frame::builder("xs.actor.greeter.active")
                .meta(serde_json::json!({ "actor_id": create.id.to_string() }))
                .build(),
        )
        .unwrap();

    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;

    {
        let store = store.clone();
        drop(tokio::spawn(async move {
            crate::processor::actor::run(store).await.unwrap();
        }));
    }

    // History replays the seeded .create and .active, threshold, then the
    // dispatcher should restart the actor and emit a fresh .active.
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.greeter.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.greeter.active"
    );
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");
    // The fresh dispatcher emits a new .active for the restart.
    let restart_active = recver.recv().await.unwrap();
    assert_eq!(restart_active.topic, "xs.actor.greeter.active");
    let meta = restart_active.meta.unwrap();
    assert_eq!(meta["actor_id"], create.id.to_string());
}

// Invariant I4: an actor accepts xs.actor.<name>.term and emits .fin.term.
#[tokio::test]
async fn inv4_actor_term_emits_fin_term() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    let create = store
        .append(
            Frame::builder("xs.actor.tunable.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame, state = null| {next: $state}}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.tunable.create"
    );
    assert_eq!(
        recver.recv().await.unwrap().topic,
        "xs.actor.tunable.active"
    );

    store
        .append(Frame::builder("xs.actor.tunable.term").build())
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.tunable.term");
    let fin = recver.recv().await.unwrap();
    assert_eq!(fin.topic, "xs.actor.tunable.fin.term");
    let meta = fin.meta.as_ref().unwrap();
    // I6: the ack points back to the originating create.
    assert_eq!(meta["actor_id"], create.id.to_string());
}

// Invariant I6: every runtime-emitted ack frame carries meta.actor_id
// pointing at the originating create. Touch every ack the actor can
// emit and assert the pointer matches the create id.
#[tokio::test]
async fn inv6_actor_acks_carry_source_pointer() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // (1) .active: emitted on successful spawn.
    let create = store
        .append(
            Frame::builder("xs.actor.tr.create")
                .hash(
                    store
                        .cas_insert(r#"{run: {|frame, state = null| {next: $state}}}"#)
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.tr.create");
    let active = recver.recv().await.unwrap();
    assert_eq!(active.topic, "xs.actor.tr.active");
    assert_eq!(
        active.meta.as_ref().unwrap()["actor_id"],
        create.id.to_string()
    );

    // (2) .fin.term: emitted on user .term.
    store
        .append(Frame::builder("xs.actor.tr.term").build())
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.tr.term");
    let fin = recver.recv().await.unwrap();
    assert_eq!(fin.topic, "xs.actor.tr.fin.term");
    assert_eq!(
        fin.meta.as_ref().unwrap()["actor_id"],
        create.id.to_string()
    );

    // (3) .invalid: emitted when a create fails to parse.
    let bad_create = store
        .append(
            Frame::builder("xs.actor.tr.create")
                .hash(store.cas_insert(r#"{run: {|| 42}}"#).await.unwrap())
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.tr.create");
    let invalid = recver.recv().await.unwrap();
    assert_eq!(invalid.topic, "xs.actor.tr.invalid");
    assert_eq!(
        invalid.meta.as_ref().unwrap()["actor_id"],
        bad_create.id.to_string()
    );
}

// Invariant I8: only one actor instance handles events for a given
// <kind>.<name> at a time. After a replacement, the old instance no
// longer processes triggers; only the new one does.
#[tokio::test]
async fn inv8_actor_single_live_instance_per_topic_root() {
    let (store, _temp_dir) = setup_test_environment().await;
    let options = ReadOptions::builder().follow(FollowOption::On).build();
    let mut recver = store.read(options).await;
    assert_eq!(recver.recv().await.unwrap().topic, "xs.threshold");

    // Two creates for the same topic root. Only the second should handle
    // any post-replacement triggers.
    let _first = store
        .append(
            Frame::builder("xs.actor.h.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{run: {|frame, state = null|
                                if $frame.topic == "trigger" {
                                  {out: {who: "first"}, next: $state}
                                } else { {next: $state} }
                            }}"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.active");

    let second = store
        .append(
            Frame::builder("xs.actor.h.create")
                .hash(
                    store
                        .cas_insert(
                            r#"{run: {|frame, state = null|
                                if $frame.topic == "trigger" {
                                  {out: {who: "second"}, next: $state}
                                } else { {next: $state} }
                            }}"#,
                        )
                        .await
                        .unwrap(),
                )
                .build(),
        )
        .unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "xs.actor.h.create");

    // Old emits .replaced, new emits .active (order may interleave).
    let mut acks = std::collections::HashSet::new();
    acks.insert(recver.recv().await.unwrap().topic);
    acks.insert(recver.recv().await.unwrap().topic);
    assert!(acks.contains("xs.actor.h.replaced"));
    assert!(acks.contains("xs.actor.h.active"));

    // Trigger: only ONE response, from the second actor.
    let _trig = store.append(Frame::builder("trigger").build()).unwrap();
    assert_eq!(recver.recv().await.unwrap().topic, "trigger");
    let out = recver.recv().await.unwrap();
    assert_eq!(out.topic, "h.out");
    let meta = out.meta.unwrap();
    assert_eq!(meta["actor_id"], second.id.to_string());
    assert_eq!(meta["who"], "second");

    // No second response from the old actor.
    assert_no_more_frames(&mut recver).await;
}

async fn setup_test_environment() -> (Store, TempDir) {
    let temp_dir = TempDir::new().unwrap();
    let store = Store::new(temp_dir.path().to_path_buf()).unwrap();

    {
        let store = store.clone();
        drop(tokio::spawn(async move {
            crate::processor::actor::run(store).await.unwrap();
        }));
    }

    (store, temp_dir)
}
